mirror of
https://github.com/documenso/documenso.git
synced 2026-07-21 15:33:40 +10:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 986030cc38 | |||
| 2815b1a809 | |||
| 960914aeb5 | |||
| d83769b410 | |||
| a0cf2a2c75 | |||
| a30b73ce86 | |||
| 46d163d9d6 | |||
| 681a89cfe1 | |||
| e5f4edc120 | |||
| 25291b64eb | |||
| fe2093fe7c | |||
| 49cddfab38 | |||
| 3e12a05ab8 | |||
| a76504c0a4 | |||
| abab0c0a22 | |||
| f72b669f67 | |||
| 536cafde31 | |||
| d052f02013 | |||
| 4878cf388f | |||
| 149f416be7 | |||
| c432261dd8 | |||
| 1852aa4b05 | |||
| 51608ed390 | |||
| 8ebef831ac | |||
| 20e2976731 | |||
| 748bf6de6b | |||
| d13cf743bf | |||
| cad48236a0 | |||
| c970abc871 | |||
| d5b3df1648 | |||
| 142c1c003e | |||
| a06c628653 | |||
| 7ca3697303 | |||
| 8ac2209493 | |||
| 9c4ec34a3c | |||
| 1f142e334a | |||
| 08f82b23dc | |||
| 747a7b0aea | |||
| 6053a4a40a | |||
| cc090adce0 | |||
| 375df71f5c | |||
| c0bb5205e1 | |||
| 1676f5bf6c | |||
| f514d55d27 | |||
| 927a656c57 | |||
| 751fb5275c | |||
| b2cca9afb6 | |||
| e2fa01509d | |||
| 311c8da8fc | |||
| 49ecfc1a2c | |||
| ffee2b2c9a | |||
| 2f18518961 | |||
| d451a7acce | |||
| d8aecc4092 | |||
| e5c2263e92 | |||
| 4aefb80989 |
@@ -1,4 +1,16 @@
|
||||
#!/usr/bin/env 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
|
||||
|
||||
@@ -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'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2024-01-25
|
||||
Tags:
|
||||
tags:
|
||||
- Vision
|
||||
- Mission
|
||||
- Open Source
|
||||
|
||||
@@ -5,7 +5,7 @@ authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2024-01-10
|
||||
Tags:
|
||||
tags:
|
||||
- GitHub
|
||||
- Backlog
|
||||
- Roadmap
|
||||
|
||||
@@ -5,7 +5,7 @@ authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2024-02-06
|
||||
Tags:
|
||||
tags:
|
||||
- Founders
|
||||
- Mission
|
||||
- Open Source
|
||||
|
||||
@@ -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
|
||||
+1
-56590
File diff suppressed because one or more lines are too long
@@ -12,7 +12,7 @@ export const generateMetadata = ({ params }: { params: { content: string } }) =>
|
||||
const document = allDocuments.find((post) => post._raw.flattenedPath === params.content);
|
||||
|
||||
if (!document) {
|
||||
notFound();
|
||||
return { title: 'Not Found' };
|
||||
}
|
||||
|
||||
return { title: document.title };
|
||||
|
||||
@@ -7,6 +7,8 @@ import { ChevronLeft } from 'lucide-react';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
import { useMDXComponent } from 'next-contentlayer/hooks';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export const generateStaticParams = () =>
|
||||
allBlogPosts.map((post) => ({ post: post._raw.flattenedPath }));
|
||||
|
||||
@@ -14,7 +16,9 @@ export const generateMetadata = ({ params }: { params: { post: string } }) => {
|
||||
const blogPost = allBlogPosts.find((post) => post._raw.flattenedPath === `blog/${params.post}`);
|
||||
|
||||
if (!blogPost) {
|
||||
notFound();
|
||||
return {
|
||||
title: 'Not Found',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { redirect } from 'next/navigation';
|
||||
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { redis } from '@documenso/lib/server-only/redis';
|
||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
@@ -12,6 +13,8 @@ import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import { PasswordReveal } from '~/components/(marketing)/password-reveal';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const fontCaveat = Caveat({
|
||||
weight: ['500'],
|
||||
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.
|
||||
</p>
|
||||
|
||||
<Link
|
||||
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signin`}
|
||||
target="_blank"
|
||||
className="mt-4 block"
|
||||
>
|
||||
<Link href={`${NEXT_PUBLIC_WEBAPP_URL()}/signin`} target="_blank" className="mt-4 block">
|
||||
<Button size="lg" className="text-base">
|
||||
Let's get started!
|
||||
<ArrowRight className="ml-2 h-5 w-5" />
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-lw-2.png';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { AnnouncementBar } from '@documenso/ui/primitives/announcement-bar';
|
||||
|
||||
import { Footer } from '~/components/(marketing)/footer';
|
||||
import { Header } from '~/components/(marketing)/header';
|
||||
@@ -41,14 +38,6 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) {
|
||||
'bg-background/50 backdrop-blur-md': scrollY > 5,
|
||||
})}
|
||||
>
|
||||
<div className="absolute -inset-0 -z-[1] opacity-100">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="h-[2rem] w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<AnnouncementBar className="relative" isShown={true} />
|
||||
<Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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">
|
||||
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:{' '}
|
||||
<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
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -15,6 +15,8 @@ export const metadata: Metadata = {
|
||||
title: 'Pricing',
|
||||
};
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export type PricingPageProps = {
|
||||
searchParams?: {
|
||||
planId?: string;
|
||||
@@ -53,7 +55,7 @@ export default function PricingPage() {
|
||||
|
||||
<div className="mt-4 flex justify-center">
|
||||
<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
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -166,6 +168,7 @@ export default function PricingPage() {
|
||||
<Link
|
||||
className="text-documenso-700 font-bold"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="mailto:support@documenso.com"
|
||||
>
|
||||
support@documenso.com
|
||||
@@ -175,6 +178,7 @@ export default function PricingPage() {
|
||||
className="text-documenso-700 font-bold"
|
||||
href="https://documen.so/discord"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
in our Discord-Support-Channel
|
||||
</a>{' '}
|
||||
|
||||
@@ -6,6 +6,7 @@ import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
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 { putFile } from '@documenso/lib/universal/upload/put-file';
|
||||
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">
|
||||
Create a{' '}
|
||||
<Link
|
||||
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`}
|
||||
href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup`}
|
||||
target="_blank"
|
||||
className="hover:text-foreground/80 font-semibold transition-colors"
|
||||
>
|
||||
|
||||
@@ -7,6 +7,7 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
export const revalidate = 0;
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// !: This entire file is a hack to get around failed prerendering of
|
||||
// !: 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 { 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 { TrpcProvider } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@@ -17,32 +18,35 @@ import './globals.css';
|
||||
const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' });
|
||||
const fontCaveat = Caveat({ subsets: ['latin'], variable: '--font-signature' });
|
||||
|
||||
export const metadata = {
|
||||
title: {
|
||||
template: '%s - Documenso',
|
||||
default: 'Documenso',
|
||||
},
|
||||
description:
|
||||
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
|
||||
keywords:
|
||||
'Documenso, open source, DocuSign alternative, document signing, open signing infrastructure, open-source community, fast signing, beautiful signing, smart templates',
|
||||
authors: { name: 'Documenso, Inc.' },
|
||||
robots: 'index, follow',
|
||||
openGraph: {
|
||||
title: 'Documenso - The Open Source DocuSign Alternative',
|
||||
export function generateMetadata() {
|
||||
return {
|
||||
title: {
|
||||
template: '%s - Documenso',
|
||||
default: 'Documenso',
|
||||
},
|
||||
description:
|
||||
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
|
||||
type: 'website',
|
||||
images: [`${process.env.NEXT_PUBLIC_MARKETING_URL}/opengraph-image.jpg`],
|
||||
},
|
||||
twitter: {
|
||||
site: '@documenso',
|
||||
card: 'summary_large_image',
|
||||
images: [`${process.env.NEXT_PUBLIC_MARKETING_URL}/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.',
|
||||
},
|
||||
};
|
||||
keywords:
|
||||
'Documenso, open source, DocuSign alternative, document signing, open signing infrastructure, open-source community, fast signing, beautiful signing, smart templates',
|
||||
authors: { name: 'Documenso, Inc.' },
|
||||
robots: 'index, follow',
|
||||
metadataBase: new URL(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3000'),
|
||||
openGraph: {
|
||||
title: 'Documenso - The Open Source DocuSign Alternative',
|
||||
description:
|
||||
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
|
||||
type: 'website',
|
||||
images: ['/opengraph-image.jpg'],
|
||||
},
|
||||
twitter: {
|
||||
site: '@documenso',
|
||||
card: 'summary_large_image',
|
||||
images: ['/opengraph-image.jpg'],
|
||||
description:
|
||||
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const flags = await getAllAnonymousFlags();
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function NotFound() {
|
||||
|
||||
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
variant="ghost"
|
||||
className="w-32"
|
||||
onClick={() => {
|
||||
void router.back();
|
||||
|
||||
@@ -40,9 +40,9 @@ export const Callout = ({ starCount }: CalloutProps) => {
|
||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||
onClick={onSignUpClick}
|
||||
>
|
||||
Claim Community Plan
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs font-medium">
|
||||
-80%
|
||||
Get the Early Adopters Plan
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||
$30/mo. forever!
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { HTMLAttributes } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
|
||||
@@ -74,15 +74,6 @@ export const Header = ({ className, ...props }: HeaderProps) => {
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
<Link
|
||||
href="https://app.documenso.com/signin"
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
<span className="bg-primary dark:text-background rounded-full px-3 py-2 text-xs">
|
||||
Sign up
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<HamburgerMenu
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import type { Variants } from 'framer-motion';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Variants, motion } from 'framer-motion';
|
||||
import { usePlausible } from 'next-plausible';
|
||||
import { LuGithub } from 'react-icons/lu';
|
||||
import { match } from 'ts-pattern';
|
||||
@@ -114,9 +113,9 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||
onClick={onSignUpClick}
|
||||
>
|
||||
Claim Community Plan
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs font-medium">
|
||||
-80%
|
||||
Get the Early Adopters Plan
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||
$30/mo. forever!
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
@@ -191,32 +190,42 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
||||
<Widget className="mt-12">
|
||||
<strong>Documenso Supporter Pledge</strong>
|
||||
<p className="w-full max-w-[70ch]">
|
||||
Our mission is to create an open signing infrastructure that empowers the world. We
|
||||
believe openness and cooperation are the way every business should be conducted.
|
||||
Our mission is to create an open signing infrastructure that empowers the world,
|
||||
enabling businesses to embrace openness, cooperation, and transparency. We believe
|
||||
that signing, as a fundamental act, should embody these values. By offering an
|
||||
open-source signing solution, we aim to make document signing accessible, transparent,
|
||||
and trustworthy.
|
||||
</p>
|
||||
|
||||
<p className="w-full max-w-[70ch]">
|
||||
By creating an open source signing solution we want to bring these values to
|
||||
businesses' most fundamental act: signing. Document Signing should be open and
|
||||
transparent, as should all trust based products.
|
||||
Through our platform, called Documenso, we strive to earn your trust by allowing
|
||||
self-hosting and providing complete visibility into its inner workings. We value
|
||||
inclusivity and foster an environment where diverse perspectives and contributions are
|
||||
welcomed, even though we may not implement them all.
|
||||
</p>
|
||||
|
||||
<p className="w-full max-w-[70ch]">
|
||||
We aim to earn this trust by enabling everyone to self-host Documenso and inspect it’s
|
||||
inner workings. We openly share our source, knowledge, and progress while creating
|
||||
Documenso.
|
||||
At Documenso, we envision a web-enabled future for business and contracts, and we are
|
||||
committed to being the leading provider of open signing infrastructure. By combining
|
||||
exceptional product design with open-source principles, we aim to deliver a robust and
|
||||
well-designed application that exceeds your expectations.
|
||||
</p>
|
||||
|
||||
<p className="w-full max-w-[70ch]">
|
||||
Exceptional products are the results of exceptional communities and we strive to
|
||||
create an inclusive, creative environment, open to all who choose to support our
|
||||
mission. We value the inputs, contributions, and perspectives of everyone in our
|
||||
community, even though we can't apply them all.
|
||||
We understand that exceptional products are born from exceptional communities, and we
|
||||
invite you to join our open-source community. Your contributions, whether technical or
|
||||
non-technical, will help shape the future of signing. Together, we can create a better
|
||||
future for everyone.
|
||||
</p>
|
||||
|
||||
<p className="w-full max-w-[70ch]">
|
||||
We are building the next generation of trust software and community the way it’s meant
|
||||
to be: Beautifully designed and open for all to join.
|
||||
Today we invite you to join us on this journey: By signing this mission statement you
|
||||
signal your support of Documenso's mission{' '}
|
||||
<span className="bg-primary text-black">
|
||||
(in a non-legally binding, but heartfelt way)
|
||||
</span>{' '}
|
||||
and lock in the early supporter plan for forever, including everything we build this
|
||||
year.
|
||||
</p>
|
||||
|
||||
<div className="flex h-24 items-center">
|
||||
|
||||
@@ -50,10 +50,6 @@ export const MENU_NAVIGATION_LINKS = [
|
||||
href: 'https://app.documenso.com/signin',
|
||||
text: 'Sign in',
|
||||
},
|
||||
{
|
||||
href: 'https://app.documenso.com/signup',
|
||||
text: 'Sign up',
|
||||
},
|
||||
];
|
||||
|
||||
export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => {
|
||||
@@ -108,13 +104,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
||||
onClick={() => handleMenuItemClick()}
|
||||
target={target}
|
||||
>
|
||||
{href === 'https://app.documenso.com/signup' ? (
|
||||
<span className="bg-primary dark:text-background rounded-full px-3 py-2 text-xl">
|
||||
{text}
|
||||
</span>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
{text}
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
@@ -8,6 +8,7 @@ import Link from 'next/link';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { usePlausible } from 'next-plausible';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
@@ -82,11 +83,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
</p>
|
||||
|
||||
<Button className="rounded-full text-base" asChild>
|
||||
<Link
|
||||
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`}
|
||||
target="_blank"
|
||||
className="mt-6"
|
||||
>
|
||||
<Link href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup`} target="_blank" className="mt-6">
|
||||
Signup Now
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -117,13 +114,13 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
</p>
|
||||
|
||||
<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>
|
||||
|
||||
<div className="mt-8 flex w-full flex-col divide-y">
|
||||
<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:
|
||||
</a>
|
||||
</p>
|
||||
@@ -133,7 +130,11 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
<p className="text-foreground py-4">
|
||||
<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
|
||||
</a>
|
||||
</strong>
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ import Link from 'next/link';
|
||||
|
||||
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
|
||||
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 { DocumentStatus } from '@documenso/prisma/client';
|
||||
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">
|
||||
Create a{' '}
|
||||
<Link
|
||||
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL}/signup`}
|
||||
href={`${NEXT_PUBLIC_WEBAPP_URL()}/signup`}
|
||||
target="_blank"
|
||||
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 { Loader } from 'lucide-react';
|
||||
import { usePlausible } from 'next-plausible';
|
||||
import { env } from 'next-runtime-env';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -144,7 +145,11 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
||||
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
|
||||
? {
|
||||
@@ -194,7 +199,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
||||
className="bg-foreground/5 col-span-12 flex flex-col rounded-2xl p-6 lg:col-span-5"
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
>
|
||||
<h3 className="text-xl font-semibold">Sign up to Community Plan</h3>
|
||||
<h3 className="text-2xl font-semibold">Sign up for the early adopters plan</h3>
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
with Timur Ercan & Lucas Smith from Documenso
|
||||
</p>
|
||||
@@ -215,7 +220,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="your@example.com"
|
||||
placeholder=""
|
||||
className="bg-background w-full pr-16"
|
||||
disabled={isSubmitting}
|
||||
onKeyDown={(e) =>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
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 { stripe } from '@documenso/lib/server-only/stripe';
|
||||
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(
|
||||
req: NextApiRequest,
|
||||
@@ -40,7 +42,7 @@ export default async function handler(
|
||||
|
||||
if (user) {
|
||||
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',
|
||||
metadata,
|
||||
allow_promotion_codes: true,
|
||||
success_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${process.env.NEXT_PUBLIC_MARKETING_URL}`,
|
||||
success_url: `${NEXT_PUBLIC_MARKETING_URL()}/claimed?sessionId={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${NEXT_PUBLIC_MARKETING_URL()}`,
|
||||
});
|
||||
|
||||
if (!checkout.url) {
|
||||
|
||||
@@ -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
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { HTMLAttributes } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
@@ -108,88 +108,86 @@ export const ResendDocumentActionItem = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem disabled={isDisabled} onSelect={(e) => e.preventDefault()}>
|
||||
<History className="mr-2 h-4 w-4" />
|
||||
Resend
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem disabled={isDisabled} onSelect={(e) => e.preventDefault()}>
|
||||
<History className="mr-2 h-4 w-4" />
|
||||
Resend
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="sm:max-w-sm" hideClose>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<h1 className="text-center text-xl">Who do you want to remind?</h1>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogContent className="sm:max-w-sm" hideClose>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<h1 className="text-center text-xl">Who do you want to remind?</h1>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form id={FORM_ID} onSubmit={handleSubmit(onFormSubmit)} className="px-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="recipients"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
{recipients.map((recipient) => (
|
||||
<FormItem
|
||||
key={recipient.id}
|
||||
className="flex flex-row items-center justify-between gap-x-3"
|
||||
<Form {...form}>
|
||||
<form id={FORM_ID} onSubmit={handleSubmit(onFormSubmit)} className="px-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="recipients"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
{recipients.map((recipient) => (
|
||||
<FormItem
|
||||
key={recipient.id}
|
||||
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
|
||||
className={cn('my-2 flex items-center gap-2 font-normal', {
|
||||
'opacity-50': !value.includes(recipient.id),
|
||||
})}
|
||||
>
|
||||
<StackAvatar
|
||||
key={recipient.id}
|
||||
type={getRecipientType(recipient)}
|
||||
fallbackText={recipientAbbreviation(recipient)}
|
||||
/>
|
||||
{recipient.email}
|
||||
</FormLabel>
|
||||
<StackAvatar
|
||||
key={recipient.id}
|
||||
type={getRecipientType(recipient)}
|
||||
fallbackText={recipientAbbreviation(recipient)}
|
||||
/>
|
||||
{recipient.email}
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
className="h-5 w-5 rounded-full data-[state=checked]:border-black data-[state=checked]:bg-black "
|
||||
checkClassName="text-white"
|
||||
value={recipient.id}
|
||||
checked={value.includes(recipient.id)}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
checked
|
||||
? onChange([...value, recipient.id])
|
||||
: onChange(value.filter((v) => v !== recipient.id))
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
className="h-5 w-5 rounded-full data-[state=checked]:border-black data-[state=checked]:bg-black "
|
||||
checkClassName="text-white"
|
||||
value={recipient.id}
|
||||
checked={value.includes(recipient.id)}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
checked
|
||||
? onChange([...value, recipient.id])
|
||||
: onChange(value.filter((v) => v !== recipient.id))
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<DialogFooter>
|
||||
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
type="button"
|
||||
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
||||
variant="secondary"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
<Button className="flex-1" loading={isSubmitting} type="submit" form={FORM_ID}>
|
||||
Send reminder
|
||||
<DialogFooter>
|
||||
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
type="button"
|
||||
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
||||
variant="secondary"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
</DialogClose>
|
||||
|
||||
<Button className="flex-1" loading={isSubmitting} type="submit" form={FORM_ID}>
|
||||
Send reminder
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
|
||||
import type { DocumentsPageViewProps } from './documents-page-view';
|
||||
import { DocumentsPageView } from './documents-page-view';
|
||||
import { PublicProfileIntro } from './username-claim/public-profile-intro';
|
||||
|
||||
export type DocumentsPageProps = {
|
||||
searchParams?: DocumentsPageViewProps['searchParams'];
|
||||
@@ -14,12 +11,6 @@ export const metadata: Metadata = {
|
||||
title: 'Documents',
|
||||
};
|
||||
|
||||
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
return (
|
||||
<>
|
||||
<PublicProfileIntro user={user} />
|
||||
<DocumentsPageView searchParams={searchParams} />
|
||||
</>
|
||||
);
|
||||
export default function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
||||
return <DocumentsPageView searchParams={searchParams} />;
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ export const UploadDocument = ({ className, team }: UploadDocumentProps) => {
|
||||
return (
|
||||
<div className={cn('relative', className)}>
|
||||
<DocumentDropzone
|
||||
className="min-h-[40vh]"
|
||||
className="h-[min(400px,50vh)]"
|
||||
disabled={remaining.documents === 0 || !session?.user.emailVerified}
|
||||
disabledMessage={disabledMessage}
|
||||
onDrop={onFileDrop}
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { File } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import Check from '@documenso/assets/Check.svg';
|
||||
import Lucas from '@documenso/assets/images/Lucas.png';
|
||||
import Timur from '@documenso/assets/images/Timur.png';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import { TRPCClientError } from '@documenso/trpc/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardHeader } from '@documenso/ui/primitives/card';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export const ZPublicProfileFormSchema = z.object({
|
||||
profileURL: z.string().trim().min(1, { message: 'Please enter a valid URL slug.' }),
|
||||
});
|
||||
|
||||
export type TPublicProfileFormSchema = z.infer<typeof ZPublicProfileFormSchema>;
|
||||
|
||||
export type PublicProfileIntroProps = {
|
||||
user: User;
|
||||
};
|
||||
|
||||
export const PublicProfileIntro = ({ user }: PublicProfileIntroProps) => {
|
||||
const form = useForm<TPublicProfileFormSchema>({
|
||||
values: {
|
||||
profileURL: user.profileURL || '',
|
||||
},
|
||||
resolver: zodResolver(ZPublicProfileFormSchema),
|
||||
});
|
||||
const textRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
const { toast } = useToast();
|
||||
const { mutateAsync: updatePublicProfile } = trpc.profile.updatePublicProfile.useMutation();
|
||||
const isSaving = form.formState.isSubmitting;
|
||||
|
||||
const isProfileURLClaimed = user.profileURL ? false : true;
|
||||
const [showClaimingDialog, setShowClaimingDialog] = useState(isProfileURLClaimed);
|
||||
const [showClaimedDialog, setShowClaimedDialog] = useState(true);
|
||||
|
||||
const onFormSubmit = async ({ profileURL }: TPublicProfileFormSchema) => {
|
||||
try {
|
||||
await updatePublicProfile({
|
||||
profileURL,
|
||||
});
|
||||
setShowClaimingDialog(false);
|
||||
setShowClaimedDialog(true);
|
||||
} catch (err) {
|
||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
||||
toast({
|
||||
title: 'An error occurred',
|
||||
description: err.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: 'An unknown error occurred',
|
||||
variant: 'destructive',
|
||||
description:
|
||||
'We encountered an unknown error while attempting to save your details. Please try again later.',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={showClaimingDialog} onOpenChange={setShowClaimingDialog}>
|
||||
<DialogContent position="center" className="pb-4">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="font-semi-bold text-center text-2xl">
|
||||
Introducing public profile!
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground/60 text-center text-sm">
|
||||
Reserve your Documenso public profile username
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Card className="relative flex flex-col items-center border-none bg-gray-50 px-6 py-6 pb-0 shadow-none">
|
||||
<code className="rounded-md border-2 border-gray-200 px-1 py-1 text-sm">
|
||||
<span>documenso.com/u/timur</span>
|
||||
</code>
|
||||
<Avatar className="dark:border-border mt-2 h-20 w-20 border-2 border-solid border-white bg-white">
|
||||
<AvatarImage className="AvatarImage" src={Timur.src} alt="Timur" />
|
||||
<AvatarFallback className="text-xs text-gray-400">Timur</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-row gap-x-2">
|
||||
Timur Ercan <Image alt="Check" src={Check} />
|
||||
</div>
|
||||
<span className="text-muted-foreground/60 text-center">
|
||||
Hey I’m Timur <br /> Pick any of the following agreements below and start signing to
|
||||
get started
|
||||
</span>
|
||||
<Card className="bg mt-2 w-full items-center shadow-none">
|
||||
<CardHeader className="p-4 text-gray-500">Documents</CardHeader>
|
||||
<hr className="mb-2" />
|
||||
<div className="mb-2 flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<File className="ml-3" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-md">NDA.pdf</span>
|
||||
<span className="text-muted-foreground mt-0.5 text-xs">
|
||||
Like to discuss about my work?
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="mr-3" variant="default">
|
||||
Sign
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<div
|
||||
className="fade-overlay bg-black-100 absolute bottom-0 h-1/4 w-full"
|
||||
style={{
|
||||
background: `linear-gradient(180deg, rgba(255, 255, 255, 0.06) 0%, white 75%, white 100%)`,
|
||||
}}
|
||||
></div>
|
||||
</Card>
|
||||
|
||||
<Form {...form}>
|
||||
<form className={cn('flex w-full flex-col')} onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
<fieldset className="flex w-full flex-col gap-y-4" disabled={isSaving}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="profileURL"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Public profile URL</FormLabel>
|
||||
<FormControl>
|
||||
<>
|
||||
<Input type="text" className="mb-2 mt-2" {...field} />
|
||||
<div className="mt-2">
|
||||
<code className="bg-muted rounded-md px-1 py-1 text-sm">
|
||||
<span ref={textRef} id="textToCopy">
|
||||
documenso.com/u/
|
||||
</span>
|
||||
</code>
|
||||
</div>
|
||||
</>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<Button type="submit" loading={isSaving}>
|
||||
Claim your username
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog open={false} onOpenChange={setShowClaimedDialog}>
|
||||
<DialogContent position="center" className="pb-4">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="font-semi-bold text-center text-xl">All set!</DialogTitle>
|
||||
<DialogDescription className="text-center">
|
||||
We will let you know as soon as this feature is launched
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Card className="relative px-6 py-6">
|
||||
<Card className="flex flex-col items-center px-6 py-6">
|
||||
<code className="bg-muted rounded-md px-1 py-1 text-sm">
|
||||
<span>documenso.com/u/lucas</span>
|
||||
</code>
|
||||
<Avatar className="dark:border-border mt-2 h-12 w-12 border-2 border-solid border-white">
|
||||
<AvatarImage className="AvatarImage" src={Lucas.src} alt="Lucas" />
|
||||
<AvatarFallback className="text-xs text-gray-400">Timur</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-row gap-x-2">
|
||||
Lucas Smith <Image alt="Check" src={Check} />
|
||||
</div>
|
||||
<div className="flex inline-flex h-full w-full flex-col items-center justify-center gap-3 py-2">
|
||||
<Skeleton className="w-75 h-4 animate-none rounded-full" />
|
||||
<Skeleton className="w-50 h-4 animate-none rounded-full" />
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="mt-2 items-center">
|
||||
<CardHeader className="p-2">Documents</CardHeader>
|
||||
<hr className="mb-2" />
|
||||
<div className="mb-2 flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<File className="ml-3" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-md">NDA.pdf</span>
|
||||
<span className="text-muted-foregroun mt-0.5 text-xs">
|
||||
Like to discuss about my work?
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="mr-3" variant="default">
|
||||
Sign
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
|
||||
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
|
||||
export const createBillingPortal = async () => {
|
||||
@@ -11,6 +12,6 @@ export const createBillingPortal = async () => {
|
||||
|
||||
return getPortalSession({
|
||||
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 { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
|
||||
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscription/get-subscriptions-by-user-id';
|
||||
|
||||
@@ -27,13 +28,13 @@ export const createCheckout = async ({ priceId }: CreateCheckoutOptions) => {
|
||||
if (foundSubscription) {
|
||||
return getPortalSession({
|
||||
customerId: stripeCustomer.id,
|
||||
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
|
||||
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing`,
|
||||
});
|
||||
}
|
||||
|
||||
return getCheckoutSession({
|
||||
customerId: stripeCustomer.id,
|
||||
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 { 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 { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
|
||||
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);
|
||||
}
|
||||
|
||||
const [subscriptions, prices, communityPlanPrices] = await Promise.all([
|
||||
const [subscriptions, prices, primaryAccountPlanPrices] = await Promise.all([
|
||||
getSubscriptionsByUserId({ userId: user.id }),
|
||||
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;
|
||||
|
||||
const communityPlanUserSubscriptions = subscriptions.filter(({ priceId }) =>
|
||||
communityPlanPriceIds.includes(priceId),
|
||||
const primaryAccountPlanSubscriptions = subscriptions.filter(({ priceId }) =>
|
||||
primaryAccountPlanPriceIds.includes(priceId),
|
||||
);
|
||||
|
||||
const subscription =
|
||||
communityPlanUserSubscriptions.find(({ status }) => status === SubscriptionStatus.ACTIVE) ??
|
||||
communityPlanUserSubscriptions[0];
|
||||
primaryAccountPlanSubscriptions.find(({ status }) => status === SubscriptionStatus.ACTIVE) ??
|
||||
primaryAccountPlanSubscriptions[0];
|
||||
|
||||
if (subscription?.priceId) {
|
||||
subscriptionProduct = await getProductByPriceId({ priceId: subscription.priceId }).catch(
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export type PublicProfileSettingsLayout = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function PublicProfileSettingsLayout({ children }: PublicProfileSettingsLayout) {
|
||||
return <div className="col-span-12 md:col-span-9">{children}</div>;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Public Profile',
|
||||
};
|
||||
|
||||
export default function PublicProfilePage() {
|
||||
return (
|
||||
<>
|
||||
<SettingsHeader
|
||||
title="Public profile"
|
||||
subtitle=""
|
||||
className="max-w-xl"
|
||||
titleChildren={
|
||||
<Link
|
||||
href="#"
|
||||
className="bg-primary dark:text-background ml-2 rounded-full px-2 py-1 text-xs font-semibold sm:px-3"
|
||||
>
|
||||
Coming soon!
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
||||
|
||||
import ActivityPageBackButton from '../../../../../components/(dashboard)/settings/layout/activity-back';
|
||||
import { UserSecurityActivityDataTable } from './user-security-activity-data-table';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -12,11 +9,11 @@ export const metadata: Metadata = {
|
||||
export default function SettingsSecurityActivityPage() {
|
||||
return (
|
||||
<div>
|
||||
<SettingsHeader
|
||||
title="Security activity"
|
||||
subtitle="View all recent security activity related to your account."
|
||||
titleChildren={<ActivityPageBackButton />}
|
||||
/>
|
||||
<h3 className="text-2xl font-semibold">Security activity</h3>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
View all recent security activity related to your account.
|
||||
</p>
|
||||
|
||||
<hr className="my-4" />
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { NextResponse } from 'next/server';
|
||||
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
|
||||
import type { ShareHandlerAPIResponse } from '~/pages/api/share';
|
||||
|
||||
export const runtime = 'edge';
|
||||
@@ -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(
|
||||
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 { 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 = {
|
||||
params: { slug: string };
|
||||
@@ -16,12 +16,12 @@ export function generateMetadata({ params: { slug } }: SharePageProps) {
|
||||
title: 'Documenso - Join the open source signing revolution',
|
||||
description: 'I just signed with Documenso!',
|
||||
type: 'website',
|
||||
images: [`${APP_BASE_URL}/share/${slug}/opengraph`],
|
||||
images: [`/share/${slug}/opengraph`],
|
||||
},
|
||||
twitter: {
|
||||
site: '@documenso',
|
||||
card: 'summary_large_image',
|
||||
images: [`${APP_BASE_URL}/share/${slug}/opengraph`],
|
||||
images: [`/share/${slug}/opengraph`],
|
||||
description: 'I just signed with Documenso!',
|
||||
},
|
||||
} satisfies Metadata;
|
||||
@@ -35,5 +35,5 @@ export default function SharePage() {
|
||||
return null;
|
||||
}
|
||||
|
||||
redirect(process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001');
|
||||
redirect(NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001');
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function ErrorPage({ error }: ErrorProps) {
|
||||
|
||||
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
variant="ghost"
|
||||
className="w-32"
|
||||
onClick={() => {
|
||||
void router.back();
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function NotFound() {
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
|
||||
<Button variant="secondary" asChild className="w-32">
|
||||
<Button asChild className="w-32">
|
||||
<Link href="/settings/teams">
|
||||
<ChevronLeft className="mr-2 h-4 w-4" />
|
||||
Go Back
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import { Card } from '@documenso/ui/primitives/card';
|
||||
|
||||
import { NewHeader } from '../../../components/(dashboard)/layout/new/new-header';
|
||||
|
||||
type CheckEmailLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function CheckEmailLayout({ children }: CheckEmailLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<NewHeader className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||
<main className="bg-sand-100 relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-24">
|
||||
<div className="relative flex w-full max-w-md items-center gap-x-24">
|
||||
<div className="absolute -inset-96 -z-[1] flex items-center justify-center opacity-50">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
|
||||
/>
|
||||
</div>
|
||||
<Card className="px-6 py-6">
|
||||
<div className="w-full">{children}</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import { Card } from '@documenso/ui/primitives/card';
|
||||
|
||||
import { NewHeader } from '../../../components/(dashboard)/layout/new/new-header';
|
||||
|
||||
type ForgotPasswordLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function ForgotPasswordLayout({ children }: ForgotPasswordLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<NewHeader className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||
<main className="bg-sand-100 relative flex flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-44">
|
||||
<div className="absolute -inset-96 -z-[1] flex items-center justify-center bg-contain opacity-50">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex w-full max-w-md items-center gap-x-24">
|
||||
<Card className="px-6 py-6">
|
||||
<div className="w-full">{children}</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export const metadata: Metadata = {
|
||||
export default function ForgotPasswordPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold">Forgot your password?</h1>
|
||||
<h1 className="text-4xl font-semibold">Forgot your password?</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
No worries, it happens! Enter your email and we'll email you a special link to reset your
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
|
||||
type UnauthenticatedLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function UnauthenticatedLayout({ children }: UnauthenticatedLayoutProps) {
|
||||
return (
|
||||
<main className="bg-sand-100 relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-24">
|
||||
<div className="relative flex w-full max-w-md items-center gap-x-24">
|
||||
<div className="absolute -inset-96 -z-[1] flex items-center justify-center opacity-50">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full">{children}</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import { Card } from '@documenso/ui/primitives/card';
|
||||
|
||||
import { NewHeader } from '../../../components/(dashboard)/layout/new/new-header';
|
||||
|
||||
type ResetPasswordLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function ResetPasswordLayout({ children }: ResetPasswordLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<NewHeader className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||
<main className="bg-sand-100 relative flex flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-56">
|
||||
<div className="relative flex w-full max-w-md items-center gap-x-24">
|
||||
<div className="absolute -inset-96 -z-[1] flex items-center justify-center opacity-50">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
|
||||
/>
|
||||
</div>
|
||||
<Card className="px-6 py-6">
|
||||
<div className="w-full">{children}</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export const metadata: Metadata = {
|
||||
export default function ResetPasswordPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold">Unable to reset password</h1>
|
||||
<h1 className="text-4xl font-semibold">Unable to reset password</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
The token you have used to reset your password is either expired or it never existed. If you
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import { Card } from '@documenso/ui/primitives/card';
|
||||
|
||||
import { NewHeader } from '../../../components/(dashboard)/layout/new/new-header';
|
||||
|
||||
type SignInLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function SignInLayout({ children }: SignInLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<NewHeader className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||
<main className="bg-sand-100 relative flex flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-[7.2rem]">
|
||||
<div className="absolute -inset-96 -z-[1] flex items-center justify-center bg-contain opacity-50">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="flex min-h-screen flex-col overflow-hidden dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex w-full max-w-md items-center gap-x-24">
|
||||
<Card className="px-6 py-6">
|
||||
<div className="w-full">{children}</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { env } from 'next-runtime-env';
|
||||
|
||||
import { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth';
|
||||
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
||||
|
||||
@@ -18,6 +20,8 @@ type 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 email = rawEmail ? decryptSecondaryData(rawEmail) : null;
|
||||
|
||||
@@ -26,31 +30,36 @@ export default function SignInPage({ searchParams }: SignInPageProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold">Sign in to your account</h1>
|
||||
<div>
|
||||
<h1 className="text-4xl font-semibold">Sign in to your account</h1>
|
||||
|
||||
<p className="text-muted-foreground/60 mt-2 text-sm">
|
||||
Welcome back, we are lucky to have you.
|
||||
<p className="text-muted-foreground/60 mt-2 text-sm">
|
||||
Welcome back, we are lucky to have you.
|
||||
</p>
|
||||
|
||||
<SignInForm
|
||||
className="mt-4"
|
||||
initialEmail={email || undefined}
|
||||
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
||||
/>
|
||||
|
||||
{NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && (
|
||||
<p className="text-muted-foreground mt-6 text-center text-sm">
|
||||
Don't have an account?{' '}
|
||||
<Link href="/signup" className="text-primary duration-200 hover:opacity-70">
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<hr className="my-4" />
|
||||
|
||||
<SignInForm
|
||||
className="mt-0"
|
||||
initialEmail={email || undefined}
|
||||
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
||||
/>
|
||||
|
||||
{process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && (
|
||||
<p className="text-muted-foreground mt-6 text-center text-sm">
|
||||
Don't have an account?{' '}
|
||||
<Link href="/signup" className="text-primary duration-200 hover:opacity-70">
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
<p className="mt-2.5 text-center">
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="text-muted-foreground text-sm duration-200 hover:opacity-70"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { NewHeader } from '../../../components/(dashboard)/layout/new/new-header';
|
||||
|
||||
type SignUpLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function SignUpLayout({ children }: SignUpLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<NewHeader className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||
<main
|
||||
className="bg-sand-100 scale-90 items-center justify-center px-4 md:h-20 lg:mx-28 lg:px-8"
|
||||
style={{ height: 'calc(100vh - 80px)' }}
|
||||
>
|
||||
<div className="grid grid-cols-12 gap-y-8 overflow-hidden p-2 lg:gap-x-8">{children}</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { env } from 'next-runtime-env';
|
||||
|
||||
import { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth';
|
||||
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
||||
|
||||
import { SignUpForm } from '~/components/forms/signup';
|
||||
@@ -16,7 +20,9 @@ type 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');
|
||||
}
|
||||
|
||||
@@ -28,8 +34,26 @@ export default function SignUpPage({ searchParams }: SignUpPageProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SignUpForm className="mt-1" initialEmail={email || undefined} isGoogleSSOEnabled={true} />
|
||||
</>
|
||||
<div>
|
||||
<h1 className="text-4xl font-semibold">Create a new account</h1>
|
||||
|
||||
<p className="text-muted-foreground/60 mt-2 text-sm">
|
||||
Create your account and start using state-of-the-art document signing. Open and beautiful
|
||||
signing is within your grasp.
|
||||
</p>
|
||||
|
||||
<SignUpForm
|
||||
className="mt-4"
|
||||
initialEmail={email || undefined}
|
||||
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
|
||||
/>
|
||||
|
||||
<p className="text-muted-foreground mt-6 text-center text-sm">
|
||||
Already have an account?{' '}
|
||||
<Link href="/signin" className="text-primary duration-200 hover:opacity-70">
|
||||
Sign in instead
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import { Card } from '@documenso/ui/primitives/card';
|
||||
|
||||
import { NewHeader } from '../../../components/(dashboard)/layout/new/new-header';
|
||||
|
||||
type TeamLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function TeamLayout({ children }: TeamLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<NewHeader className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||
<main className="bg-sand-100 relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-24">
|
||||
<div className="relative flex w-full max-w-md items-center gap-x-24">
|
||||
<div className="absolute -inset-96 -z-[1] flex items-center justify-center opacity-50">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
|
||||
/>
|
||||
</div>
|
||||
<Card className="px-6 py-6">
|
||||
<div className="w-full">{children}</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import backgroundPattern from '@documenso/assets/images/background-pattern.png';
|
||||
import { Card } from '@documenso/ui/primitives/card';
|
||||
|
||||
import { NewHeader } from '../../../components/(dashboard)/layout/new/new-header';
|
||||
|
||||
type VerifyEmailLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function VerifyEmailLayout({ children }: VerifyEmailLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<NewHeader className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||
<main className="bg-sand-100 relative flex flex-col items-center justify-center overflow-hidden px-4 py-12 md:p-12 lg:p-[11.2rem]">
|
||||
<div className="absolute -inset-96 -z-[1] flex items-center justify-center bg-contain opacity-50">
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="dark:brightness-95 dark:contrast-[70%] dark:invert dark:sepia"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex w-full max-w-md items-center gap-x-24">
|
||||
<Card className="px-6 py-6">
|
||||
<div className="w-full">{children}</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+31
-24
@@ -2,8 +2,11 @@ import { Suspense } from 'react';
|
||||
|
||||
import { Caveat, Inter } from 'next/font/google';
|
||||
|
||||
import { PublicEnvScript } from 'next-runtime-env';
|
||||
|
||||
import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag';
|
||||
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 { getLocale } from '@documenso/lib/server-only/headers/get-locale';
|
||||
import { TrpcProvider } from '@documenso/trpc/react';
|
||||
@@ -19,32 +22,35 @@ import './globals.css';
|
||||
const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' });
|
||||
const fontCaveat = Caveat({ subsets: ['latin'], variable: '--font-signature' });
|
||||
|
||||
export const metadata = {
|
||||
title: {
|
||||
template: '%s - Documenso',
|
||||
default: 'Documenso',
|
||||
},
|
||||
description:
|
||||
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
|
||||
keywords:
|
||||
'Documenso, open source, DocuSign alternative, document signing, open signing infrastructure, open-source community, fast signing, beautiful signing, smart templates',
|
||||
authors: { name: 'Documenso, Inc.' },
|
||||
robots: 'index, follow',
|
||||
openGraph: {
|
||||
title: 'Documenso - The Open Source DocuSign Alternative',
|
||||
export function generateMetadata() {
|
||||
return {
|
||||
title: {
|
||||
template: '%s - Documenso',
|
||||
default: 'Documenso',
|
||||
},
|
||||
description:
|
||||
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
|
||||
type: 'website',
|
||||
images: [`${process.env.NEXT_PUBLIC_WEBAPP_URL}/opengraph-image.jpg`],
|
||||
},
|
||||
twitter: {
|
||||
site: '@documenso',
|
||||
card: 'summary_large_image',
|
||||
images: [`${process.env.NEXT_PUBLIC_WEBAPP_URL}/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.',
|
||||
},
|
||||
};
|
||||
keywords:
|
||||
'Documenso, open source, DocuSign alternative, document signing, open signing infrastructure, open-source community, fast signing, beautiful signing, smart templates',
|
||||
authors: { name: 'Documenso, Inc.' },
|
||||
robots: 'index, follow',
|
||||
metadataBase: new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000'),
|
||||
openGraph: {
|
||||
title: 'Documenso - The Open Source DocuSign Alternative',
|
||||
description:
|
||||
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
|
||||
type: 'website',
|
||||
images: ['/opengraph-image.jpg'],
|
||||
},
|
||||
twitter: {
|
||||
site: '@documenso',
|
||||
card: 'summary_large_image',
|
||||
images: ['/opengraph-image.jpg'],
|
||||
description:
|
||||
'Join Documenso, the open signing infrastructure, and get a 10x better signing experience. Pricing starts at $30/mo. forever! Sign in now and enjoy a faster, smarter, and more beautiful document signing process. Integrates with your favorite tools, customizable, and expandable. Support our mission and become a part of our open-source community.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const flags = await getServerComponentAllFlags();
|
||||
@@ -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="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<PublicEnvScript />
|
||||
</head>
|
||||
|
||||
<Suspense>
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from 'react';
|
||||
|
||||
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
@@ -25,7 +26,7 @@ export function AvatarWithRecipient({ recipient }: AvatarWithRecipientProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
void copy(`${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${recipient.token}`).then(() => {
|
||||
void copy(`${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`).then(() => {
|
||||
toast({
|
||||
title: 'Copied to clipboard',
|
||||
description: 'The signing link has been copied to your clipboard.',
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
import { File } from 'lucide-react';
|
||||
|
||||
import Check from '@documenso/assets/Check.svg';
|
||||
import Timur from '@documenso/assets/images/Timur.png';
|
||||
import backgroundPattern from '@documenso/assets/images/background-blog-og.png';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardFooter, CardHeader } from '@documenso/ui/primitives/card';
|
||||
|
||||
type ClaimUsernameCardProps = {
|
||||
className: string;
|
||||
};
|
||||
|
||||
export default function ClaimUsernameCard({ className }: ClaimUsernameCardProps) {
|
||||
const onSignUpClick = () => {};
|
||||
return (
|
||||
<div className={cn('relative', className)}>
|
||||
<Card className={cn('relative h-full overflow-hidden px-16 py-16 shadow-none')}>
|
||||
<Image
|
||||
src={backgroundPattern}
|
||||
alt="background pattern"
|
||||
className="absolute left-0 top-0 h-full w-full bg-cover opacity-50 dark:brightness-95 dark:contrast-[100%] dark:invert"
|
||||
/>
|
||||
<Card className="mt-28 flex flex-col items-center px-6 py-6 shadow-none">
|
||||
<code className="rounded-md border-2 border-gray-200 px-1 py-1 text-sm">
|
||||
<span>documenso.com/u/timur</span>
|
||||
</code>
|
||||
<Avatar className="dark:border-border mt-2 h-20 w-20 border-2 border-solid border-white">
|
||||
<AvatarImage className="AvatarImage" src={Timur.src} alt="Timur" />
|
||||
<AvatarFallback className="text-xs text-gray-400">Timur</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="mb-2 flex flex-row gap-x-2">
|
||||
Timur Ercan <Image alt="Check" src={Check} />
|
||||
</div>
|
||||
<span className="text-muted-foreground/60 text-center ">
|
||||
Hey I’m Timur <br /> Pick any of the following agreements below and <br /> start signing
|
||||
to get started
|
||||
</span>
|
||||
<Card className="mt-2 w-full items-center shadow-none">
|
||||
<CardHeader className="p-4 text-gray-500">Documents</CardHeader>
|
||||
<hr className="mb-2" />
|
||||
<div className="mb-2 flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<File className="text-muted-foreground ml-3" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-md">NDA.pdf</span>
|
||||
<span className="text-muted-foreground mt-0.5 text-xs">
|
||||
Like to discuss about my work?
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="mr-3 px-6" variant="default">
|
||||
Sign
|
||||
</Button>
|
||||
</div>
|
||||
<hr className="mb-2" />
|
||||
<div className="mb-2 flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<File className="text-muted-foreground ml-3" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-md">NDA.pdf</span>
|
||||
<span className="text-muted-foreground mt-0.5 text-xs">
|
||||
Like to discuss about my work?
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="mr-3 px-6" variant="default">
|
||||
Sign
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
|
||||
<CardFooter className="mt-32 justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||
onClick={onSignUpClick}
|
||||
>
|
||||
Claim Community Plan
|
||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs font-medium">
|
||||
-80%
|
||||
</span>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import LogoImage from '@documenso/assets/logo.png';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
import { NewHamburgerMenu } from './new-mobile-hamburger';
|
||||
import { NewMobileNavigation } from './new-mobile-navigation';
|
||||
|
||||
export type HeaderProps = HTMLAttributes<HTMLElement>;
|
||||
|
||||
export const NewHeader = ({ className, ...props }: HeaderProps) => {
|
||||
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<header className={cn('flex items-center justify-between', className)} {...props}>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link href="/" className="z-10" onClick={() => setIsHamburgerMenuOpen(false)}>
|
||||
<Image
|
||||
src={LogoImage}
|
||||
alt="Documenso Logo"
|
||||
className="dark:invert"
|
||||
width={170}
|
||||
height={25}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center gap-x-6 md:flex">
|
||||
<Link
|
||||
href="https://documenso.com/pricing"
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
Pricing
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="https://documenso.com/blog"
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
Blog
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="https://documenso.com/open"
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
Open Startup
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/signin"
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
<Link
|
||||
href="/signup"
|
||||
target="_blank"
|
||||
className="text-muted-foreground hover:text-muted-foreground/80 text-sm font-semibold"
|
||||
>
|
||||
<span className="bg-primary dark:text-background rounded-full px-3 py-2 text-xs">
|
||||
Sign up
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<NewHamburgerMenu
|
||||
onToggleMenuOpen={() => setIsHamburgerMenuOpen((v) => !v)}
|
||||
isMenuOpen={isHamburgerMenuOpen}
|
||||
/>
|
||||
<NewMobileNavigation
|
||||
isMenuOpen={isHamburgerMenuOpen}
|
||||
onMenuOpenChange={setIsHamburgerMenuOpen}
|
||||
/>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Menu, X } from 'lucide-react';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
export interface HamburgerMenuProps {
|
||||
isMenuOpen: boolean;
|
||||
onToggleMenuOpen?: () => void;
|
||||
}
|
||||
|
||||
export const NewHamburgerMenu = ({ isMenuOpen, onToggleMenuOpen }: HamburgerMenuProps) => {
|
||||
return (
|
||||
<div className="flex md:hidden">
|
||||
<Button variant="outline" className="z-20 w-10 p-0" onClick={onToggleMenuOpen}>
|
||||
{isMenuOpen ? <X /> : <Menu />}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,151 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
import { FaXTwitter } from 'react-icons/fa6';
|
||||
import { LiaDiscord } from 'react-icons/lia';
|
||||
import { LuGithub } from 'react-icons/lu';
|
||||
|
||||
import LogoImage from '@documenso/assets/logo.png';
|
||||
import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
|
||||
|
||||
export type MobileNavigationProps = {
|
||||
isMenuOpen: boolean;
|
||||
onMenuOpenChange?: (_value: boolean) => void;
|
||||
};
|
||||
|
||||
export const MENU_NAVIGATION_LINKS = [
|
||||
{
|
||||
href: 'https://documenso.com/singleplayer',
|
||||
text: 'Singleplayer',
|
||||
},
|
||||
{
|
||||
href: 'https://documenso.com/blog',
|
||||
text: 'Blog',
|
||||
},
|
||||
{
|
||||
href: 'https://documenso.com/pricing',
|
||||
text: 'Pricing',
|
||||
},
|
||||
{
|
||||
href: 'https://documenso.com/open',
|
||||
text: 'Open Startup',
|
||||
},
|
||||
{
|
||||
href: 'https://status.documenso.com',
|
||||
text: 'Status',
|
||||
},
|
||||
{
|
||||
href: 'mailto:support@documenso.com',
|
||||
text: 'Support',
|
||||
target: '_blank',
|
||||
},
|
||||
{
|
||||
href: 'https://documenso.com/privacy',
|
||||
text: 'Privacy',
|
||||
},
|
||||
{
|
||||
href: '/signin',
|
||||
text: 'Sign in',
|
||||
},
|
||||
{
|
||||
href: '/signup',
|
||||
text: 'Sign up',
|
||||
},
|
||||
];
|
||||
|
||||
export const NewMobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigationProps) => {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
const handleMenuItemClick = () => {
|
||||
onMenuOpenChange?.(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
|
||||
<SheetContent className="w-full max-w-[400px]">
|
||||
<Link href="/" className="z-10" onClick={handleMenuItemClick}>
|
||||
<Image
|
||||
src={LogoImage}
|
||||
alt="Documenso Logo"
|
||||
className="dark:invert"
|
||||
width={170}
|
||||
height={25}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<motion.div
|
||||
className="mt-12 flex w-full flex-col items-start gap-y-4"
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
transition={{
|
||||
staggerChildren: 0.03,
|
||||
}}
|
||||
>
|
||||
{MENU_NAVIGATION_LINKS.map(({ href, text, target }) => (
|
||||
<motion.div
|
||||
key={href}
|
||||
variants={{
|
||||
initial: {
|
||||
opacity: 0,
|
||||
x: shouldReduceMotion ? 0 : 100,
|
||||
},
|
||||
animate: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
ease: 'backInOut',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
|
||||
href={href}
|
||||
onClick={() => handleMenuItemClick()}
|
||||
target={target}
|
||||
>
|
||||
{href === 'https://app.documenso.com/signup' ? (
|
||||
<span className="bg-primary dark:text-background rounded-full px-3 py-2 text-xl">
|
||||
{text}
|
||||
</span>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<div className="mx-auto mt-8 flex w-full flex-wrap items-center gap-x-4 gap-y-4 ">
|
||||
<Link
|
||||
href="https://twitter.com/documenso"
|
||||
target="_blank"
|
||||
className="text-foreground hover:text-foreground/80"
|
||||
>
|
||||
<FaXTwitter className="h-6 w-6" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="https://github.com/documenso/documenso"
|
||||
target="_blank"
|
||||
className="text-foreground hover:text-foreground/80"
|
||||
>
|
||||
<LuGithub className="h-6 w-6" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="https://documen.so/discord"
|
||||
target="_blank"
|
||||
className="text-foreground hover:text-foreground/80"
|
||||
>
|
||||
<LiaDiscord className="h-7 w-7" />
|
||||
</Link>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
export default function ActivityPageBackButton() {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
className="flex-shrink-0"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
void router.back();
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { CreditCard, Globe2, Lock, User, Users } from 'lucide-react';
|
||||
import { CreditCard, Lock, User, Users } from 'lucide-react';
|
||||
|
||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@@ -78,25 +78,6 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Link href="#">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/public-profile') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Globe2 className="mr-2 h-5 w-5" />
|
||||
Public profile
|
||||
<Link
|
||||
href="#"
|
||||
className="bg-primary dark:text-background ml-2 rounded-full px-2 py-1 text-xs font-semibold sm:px-3"
|
||||
>
|
||||
Coming soon!
|
||||
</Link>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,33 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
export type SettingsHeaderProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
children?: React.ReactNode;
|
||||
titleChildren?: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const SettingsHeader = ({
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
titleChildren,
|
||||
className,
|
||||
}: SettingsHeaderProps) => {
|
||||
export const SettingsHeader = ({ children, title, subtitle }: SettingsHeaderProps) => {
|
||||
return (
|
||||
<>
|
||||
<div className={cn('flex flex-row items-center justify-between', className)}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">{title}</h3>
|
||||
|
||||
<p className="text-muted-foreground text-sm md:mt-2">{subtitle}</p>
|
||||
</div>
|
||||
|
||||
<div>{titleChildren}</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { CreditCard, Globe2, Lock, User, Users } from 'lucide-react';
|
||||
import { CreditCard, Lock, User, Users } from 'lucide-react';
|
||||
|
||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@@ -81,25 +81,6 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Link href="#">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/public-profile') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Globe2 className="mr-2 h-5 w-5" />
|
||||
Public profile
|
||||
<Link
|
||||
href="#"
|
||||
className="bg-primary dark:text-background ml-2 rounded-full px-2 py-1 text-xs font-semibold sm:px-3"
|
||||
>
|
||||
Coming soon!
|
||||
</Link>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -238,7 +238,7 @@ export const TransferTeamDialog = ({
|
||||
<Alert variant="neutral">
|
||||
<AlertDescription>
|
||||
<ul className="list-outside list-disc space-y-2 pl-4">
|
||||
{IS_BILLING_ENABLED && (
|
||||
{IS_BILLING_ENABLED() && (
|
||||
// Temporary removed.
|
||||
// <li>
|
||||
// {form.getValues('clearPaymentMethods')
|
||||
|
||||
@@ -48,7 +48,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{IS_BILLING_ENABLED && (
|
||||
{IS_BILLING_ENABLED() && (
|
||||
<Link href={billingPath}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -56,7 +56,7 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{IS_BILLING_ENABLED && (
|
||||
{IS_BILLING_ENABLED() && (
|
||||
<Link href={billingPath}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -67,7 +67,7 @@ export const TeamsMemberPageDataTable = ({
|
||||
<Tabs value={currentTab} className="flex-shrink-0 overflow-x-auto">
|
||||
<TabsList>
|
||||
<TabsTrigger className="min-w-[60px]" value="members" asChild>
|
||||
<Link href={pathname ?? '/'}>All</Link>
|
||||
<Link href={pathname ?? '/'}>Active</Link>
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger className="min-w-[60px]" value="invites" asChild>
|
||||
|
||||
@@ -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,7 +2,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { signIn } from 'next-auth/react';
|
||||
@@ -40,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',
|
||||
[ErrorCode.INCORRECT_TWO_FACTOR_CODE]: 'The two-factor authentication code provided is incorrect',
|
||||
[ErrorCode.INCORRECT_TWO_FACTOR_BACKUP_CODE]: 'The backup code provided is incorrect',
|
||||
[ErrorCode.UNVERIFIED_EMAIL]:
|
||||
'This account has not been verified. Please verify your account before signing in.',
|
||||
};
|
||||
|
||||
const TwoFactorEnabledErrorCode = ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS;
|
||||
@@ -65,6 +67,7 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
const { toast } = useToast();
|
||||
const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] =
|
||||
useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const [twoFactorAuthenticationMethod, setTwoFactorAuthenticationMethod] = useState<
|
||||
'totp' | 'backup'
|
||||
@@ -132,6 +135,17 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
|
||||
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({
|
||||
variant: 'destructive',
|
||||
title: 'Unable to sign in',
|
||||
@@ -169,147 +183,135 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('mt-1')}>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className={cn('flex w-full flex-col gap-y-4', className)}
|
||||
onSubmit={form.handleSubmit(onFormSubmit)}
|
||||
<Form {...form}>
|
||||
<form
|
||||
className={cn('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</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
loading={isSubmitting}
|
||||
className="dark:bg-documenso dark:hover:opacity-90"
|
||||
>
|
||||
<fieldset className="flex w-full flex-col gap-y-4" disabled={isSubmitting}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" placeholder="john@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
|
||||
{isGoogleSSOEnabled && (
|
||||
<>
|
||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||
<div className="bg-border h-px flex-1" />
|
||||
<span className="text-muted-foreground bg-transparent">Or continue with</span>
|
||||
<div className="bg-border h-px flex-1" />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="bg-background text-muted-foreground border"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignInWithGoogleClick}
|
||||
>
|
||||
<FcGoogle className="mr-2 h-5 w-5" />
|
||||
Google
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<Dialog
|
||||
open={isTwoFactorAuthenticationDialogOpen}
|
||||
onOpenChange={onCloseTwoFactorAuthenticationDialog}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Two-Factor Authentication</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
<fieldset disabled={isSubmitting}>
|
||||
{twoFactorAuthenticationMethod === 'totp' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="totpCode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Authentication Token</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<>
|
||||
<PasswordInput placeholder="Password" {...field} />
|
||||
<p className="mt-2.5 text-right">
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="text-muted-foreground/60 text-sm duration-200 hover:opacity-70"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
{twoFactorAuthenticationMethod === 'backup' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="backupCode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel> Backup Code</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
loading={isSubmitting}
|
||||
className="dark:bg-documenso dark:hover:opacity-90"
|
||||
>
|
||||
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
<DialogFooter className="mt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={onToggleTwoFactorAuthenticationMethodClick}
|
||||
>
|
||||
{twoFactorAuthenticationMethod === 'totp'
|
||||
? 'Use Backup Code'
|
||||
: 'Use Authenticator'}
|
||||
</Button>
|
||||
|
||||
{isGoogleSSOEnabled && (
|
||||
<>
|
||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||
<div className="bg-border h-px flex-1" />
|
||||
<span className="text-muted-foreground bg-transparent">Or continue with</span>
|
||||
<div className="bg-border h-px flex-1" />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="bg-background text-muted-foreground border"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignInWithGoogleClick}
|
||||
>
|
||||
<FcGoogle className="mr-2 h-5 w-5" />
|
||||
Google
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<Dialog
|
||||
open={isTwoFactorAuthenticationDialogOpen}
|
||||
onOpenChange={onCloseTwoFactorAuthenticationDialog}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Two-Factor Authentication</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
<fieldset disabled={isSubmitting}>
|
||||
{twoFactorAuthenticationMethod === 'totp' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="totpCode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Authentication Token</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{twoFactorAuthenticationMethod === 'backup' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="backupCode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel> Backup Code</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DialogFooter className="mt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={onToggleTwoFactorAuthenticationMethodClick}
|
||||
>
|
||||
{twoFactorAuthenticationMethod === 'totp'
|
||||
? 'Use Backup Code'
|
||||
: 'Use Authenticator'}
|
||||
</Button>
|
||||
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Form>
|
||||
</div>
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { signIn } from 'next-auth/react';
|
||||
@@ -17,7 +14,6 @@ import { trpc } from '@documenso/trpc/react';
|
||||
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card } from '@documenso/ui/primitives/card';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -31,16 +27,6 @@ import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import ClaimUsernameCard from '../(dashboard)/claim-username-card/claim-username-card';
|
||||
|
||||
export const STEP = {
|
||||
SIGNUP: 'SIGNUP',
|
||||
CLAIM: 'CLAIM',
|
||||
} as const;
|
||||
|
||||
type StepKeys = keyof typeof STEP;
|
||||
type StepValues = (typeof STEP)[StepKeys];
|
||||
|
||||
const SIGN_UP_REDIRECT_PATH = '/documents';
|
||||
|
||||
export const ZSignUpFormSchema = z
|
||||
@@ -49,7 +35,6 @@ export const ZSignUpFormSchema = z
|
||||
email: z.string().email().min(1),
|
||||
password: ZPasswordSchema,
|
||||
signature: z.string().min(1, { message: 'We need your signature to sign documents' }),
|
||||
profileURL: z.string().trim().min(1, { message: 'Please enter a valid URL slug.' }),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -72,13 +57,7 @@ export type SignUpFormProps = {
|
||||
export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: SignUpFormProps) => {
|
||||
const { toast } = useToast();
|
||||
const analytics = useAnalytics();
|
||||
const searchParams = useSearchParams();
|
||||
const [step, setStep] = useState<StepValues>(STEP.SIGNUP);
|
||||
|
||||
let src: string | null = null;
|
||||
if (searchParams) {
|
||||
src = searchParams.get('src');
|
||||
}
|
||||
const router = useRouter();
|
||||
|
||||
const form = useForm<TSignUpFormSchema>({
|
||||
values: {
|
||||
@@ -86,43 +65,30 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
email: initialEmail ?? '',
|
||||
password: '',
|
||||
signature: '',
|
||||
profileURL: '',
|
||||
},
|
||||
resolver: zodResolver(ZSignUpFormSchema),
|
||||
});
|
||||
|
||||
const isSubmitting = form.formState.isSubmitting;
|
||||
const isValid = form.formState.isValid;
|
||||
|
||||
const signature = form.watch('signature');
|
||||
|
||||
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
|
||||
const { mutateAsync: updatePublicProfile } = trpc.profile.updatePublicProfile.useMutation();
|
||||
|
||||
const onFormSubmit = async ({
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
signature,
|
||||
profileURL,
|
||||
}: TSignUpFormSchema) => {
|
||||
const onFormSubmit = async ({ name, email, password, signature }: TSignUpFormSchema) => {
|
||||
try {
|
||||
await signup({ name, email, password, signature });
|
||||
|
||||
await updatePublicProfile({
|
||||
profileURL,
|
||||
});
|
||||
router.push(`/unverified-account`);
|
||||
|
||||
await signIn('credentials', {
|
||||
email,
|
||||
password,
|
||||
callbackUrl: SIGN_UP_REDIRECT_PATH,
|
||||
toast({
|
||||
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', {
|
||||
email,
|
||||
timestamp: new Date().toISOString(),
|
||||
custom_campaign_params: { src },
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
||||
@@ -142,16 +108,6 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
}
|
||||
};
|
||||
|
||||
const onNextStepClick = () => {
|
||||
if (step === STEP.SIGNUP) {
|
||||
setStep(STEP.CLAIM);
|
||||
|
||||
setTimeout(() => {
|
||||
document.querySelector<HTMLElement>('#signature')?.focus();
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const onSignUpWithGoogleClick = async () => {
|
||||
try {
|
||||
await signIn('google', { callbackUrl: SIGN_UP_REDIRECT_PATH });
|
||||
@@ -165,199 +121,107 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
}
|
||||
};
|
||||
|
||||
const stepsRemaining = useMemo(() => {
|
||||
if (step === STEP.CLAIM) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}, [step]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ClaimUsernameCard className="col-span-12 gap-y-4 lg:col-span-7" />
|
||||
<Card className="col-span-12 gap-y-4 bg-gray-50 px-6 py-6 shadow-none lg:col-span-5">
|
||||
<div className="w-full">
|
||||
{step === STEP.SIGNUP && (
|
||||
<>
|
||||
<h1 className="text-3xl font-semibold">Create a new account</h1>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className={cn('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="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<p className="text-muted-foreground/60 mt-2 text-sm">
|
||||
Create your account and start using state-of-the-art document signing. Open and
|
||||
beautiful signing is within your grasp.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{step === STEP.CLAIM && (
|
||||
<>
|
||||
<h1 className="text-3xl font-semibold">Claim your username now</h1>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<p className="text-muted-foreground/60 mt-2 text-sm">
|
||||
You will get notified & be able to set up your documenso public profile when we
|
||||
launch the feature
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<hr className="mb-6 mt-4" />
|
||||
<Form {...form}>
|
||||
<form
|
||||
className={cn('flex h-full w-full flex-col gap-y-4', className)}
|
||||
onSubmit={form.handleSubmit(onFormSubmit)}
|
||||
>
|
||||
<fieldset className="flex w-full flex-col gap-y-4" disabled={isSubmitting}>
|
||||
<div className={cn(step === STEP.SIGNUP && 'hidden')}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="profileURL"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Public profile URL</FormLabel>
|
||||
<FormControl>
|
||||
<>
|
||||
<Input id="username" type="text" className="mb-2 mt-2" {...field} />
|
||||
<div className="mt-2">
|
||||
<code className="bg-muted rounded-md px-1 py-1 text-sm">
|
||||
documenso.com/u/
|
||||
</code>
|
||||
</div>
|
||||
</>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn('space-y-2', step === STEP.CLAIM && 'invisible')}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Full Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" className="bg-white" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email Address</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" className="bg-white" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput className="bg-white" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="signature"
|
||||
render={({ field: { onChange } }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Sign Here</FormLabel>
|
||||
<FormControl>
|
||||
<SignaturePad
|
||||
id="signatureText"
|
||||
className="w-full"
|
||||
disabled={isSubmitting}
|
||||
containerClassName="mt-2 rounded-lg border bg-background"
|
||||
onChange={(v) => onChange(v ?? '')}
|
||||
height={200}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{isGoogleSSOEnabled && (
|
||||
<>
|
||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||
<div className="bg-border h-px flex-1" />
|
||||
<span className="text-muted-foreground bg-transparent">Or</span>
|
||||
<div className="bg-border h-px flex-1" />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant={'outline'}
|
||||
className="bg-background text-muted-foreground border"
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="signature"
|
||||
render={({ field: { onChange } }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Sign Here</FormLabel>
|
||||
<FormControl>
|
||||
<SignaturePad
|
||||
className="h-36 w-full"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignUpWithGoogleClick}
|
||||
>
|
||||
<FcGoogle className="mr-2 h-5 w-5" />
|
||||
Sign Up with Google
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<p className="text-muted-foreground text-left text-sm">
|
||||
Already have an account?{' '}
|
||||
<Link href="/signin" className="text-primary duration-200 hover:opacity-70">
|
||||
Sign in instead
|
||||
</Link>
|
||||
</p>
|
||||
<div className="mt-6 flex items-center justify-between">
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{isValid ? 'Claim username' : `Basic details ${stepsRemaining}/2`}
|
||||
</p>
|
||||
containerClassName="mt-2 rounded-lg border bg-background"
|
||||
onChange={(v) => onChange(v ?? '')}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<p className="text-muted-foreground block text-xs md:hidden">Minimise contract</p>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<div className="bg-background relative h-[2px] w-full">
|
||||
<div
|
||||
className={cn('bg-primary/60 absolute inset-y-0 left-0 duration-200', {
|
||||
'w-1/2': stepsRemaining === 1,
|
||||
'w-full': isValid,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{!isValid && (
|
||||
<Button
|
||||
loading={isSubmitting}
|
||||
className="dark:bg-documenso ml-auto w-52 dark:hover:opacity-90"
|
||||
onClick={() => onNextStepClick()}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
)}
|
||||
{isValid && (
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
className="dark:bg-documenso ml-auto w-52 dark:hover:opacity-90"
|
||||
>
|
||||
Complete
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
loading={isSubmitting}
|
||||
className="dark:bg-documenso dark:hover:opacity-90"
|
||||
>
|
||||
{isSubmitting ? 'Signing up...' : 'Sign Up'}
|
||||
</Button>
|
||||
|
||||
{isGoogleSSOEnabled && (
|
||||
<>
|
||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||
<div className="bg-border h-px flex-1" />
|
||||
<span className="text-muted-foreground bg-transparent">Or</span>
|
||||
<div className="bg-border h-px flex-1" />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant={'outline'}
|
||||
className="bg-background text-muted-foreground border"
|
||||
disabled={isSubmitting}
|
||||
onClick={onSignUpWithGoogleClick}
|
||||
>
|
||||
<FcGoogle className="mr-2 h-5 w-5" />
|
||||
Sign Up with Google
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function NotFoundPartial({ children }: NotFoundPartialProps) {
|
||||
|
||||
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
variant="ghost"
|
||||
className="w-32"
|
||||
onClick={() => {
|
||||
void router.back();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
|
||||
/**
|
||||
* getAssetBuffer is used to retrieve array buffers for various assets
|
||||
* that are hosted in the `public` folder.
|
||||
@@ -8,7 +10,7 @@
|
||||
* @param path The path to the asset, relative to the `public` folder.
|
||||
*/
|
||||
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());
|
||||
};
|
||||
|
||||
@@ -32,7 +32,6 @@ export function PostHogPageview() {
|
||||
// Do nothing.
|
||||
});
|
||||
},
|
||||
custom_campaign_params: ['src'],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @type {import('lint-staged').Config} */
|
||||
module.exports = {
|
||||
'**/*.{ts,tsx,cts,mts}': (files) => `eslint --fix ${files.join(' ')}`,
|
||||
'**/*.{js,jsx,cjs,mjs}': (files) => `prettier --write ${files.join(' ')}`,
|
||||
'**/*.{yml,mdx}': (files) => `prettier --write ${files.join(' ')}`,
|
||||
'**/*.{ts,tsx,cts,mts}': (files) => files.map((file) => `eslint --fix ${file}`),
|
||||
'**/*.{js,jsx,cjs,mjs}': (files) => files.map((file) => `prettier --write ${file}`),
|
||||
'**/*.{yml,mdx}': (files) => files.map((file) => `prettier --write ${file}`),
|
||||
'**/*/package.json': 'npm run precommit',
|
||||
};
|
||||
|
||||
Generated
+16
@@ -9,6 +9,9 @@
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"next-runtime-env": "^3.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.7.1",
|
||||
"@commitlint/config-conventional": "^17.7.0",
|
||||
@@ -14404,6 +14407,19 @@
|
||||
"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": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz",
|
||||
|
||||
+3
-1
@@ -47,7 +47,9 @@
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"next-runtime-env": "^3.2.0"
|
||||
},
|
||||
"overrides": {
|
||||
"next-auth": {
|
||||
"next": "14.0.3"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.54572 1.10579C9.57786 -0.0821356 11.4223 -0.0821393 12.4545 1.10579L13.2585 2.03112C13.3917 2.18452 13.5947 2.25838 13.7954 2.22654L15.0061 2.03449C16.5603 1.78794 17.9732 2.97353 18.0003 4.54698L18.0214 5.77262C18.0249 5.97581 18.1329 6.16284 18.3071 6.26746L19.358 6.89855C20.7071 7.70873 21.0274 9.52517 20.0368 10.7479L19.2651 11.7004C19.1372 11.8583 19.0997 12.0709 19.1659 12.2631L19.5652 13.422C20.0779 14.9098 19.1557 16.5072 17.6109 16.8071L16.4075 17.0407C16.208 17.0794 16.0426 17.2182 15.9698 17.408L15.5308 18.5525C14.9672 20.0218 13.234 20.6526 11.8578 19.8893L10.7858 19.2948C10.6081 19.1962 10.3921 19.1962 10.2144 19.2948L9.14242 19.8893C7.76623 20.6526 6.033 20.0218 5.4694 18.5525L5.03038 17.408C4.9576 17.2182 4.79216 17.0794 4.59268 17.0407L3.38932 16.8071C1.84448 16.5072 0.922245 14.9098 1.43495 13.422L1.83431 12.2631C1.90052 12.0709 1.86302 11.8583 1.7351 11.7004L0.963432 10.7479C-0.0272148 9.52517 0.293068 7.70873 1.64218 6.89855L2.69306 6.26746C2.86728 6.16284 2.97526 5.97581 2.97875 5.77263L2.99985 4.54699C3.02694 2.97354 4.43987 1.78794 5.99413 2.03449L7.20481 2.22654C7.40551 2.25838 7.60845 2.18452 7.74173 2.03112L8.54572 1.10579ZM13.7072 9.42195C14.0977 9.03143 14.0977 8.39826 13.7072 8.00774C13.3167 7.61721 12.6835 7.61721 12.293 8.00774L9.5001 10.8006L8.7072 10.0077C8.31667 9.61721 7.68351 9.61721 7.29298 10.0077C6.90246 10.3983 6.90246 11.0314 7.29298 11.422L8.43944 12.5684C9.02522 13.1542 9.97497 13.1542 10.5608 12.5684L13.7072 9.42195Z" fill="#7AC455"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 104 KiB |
@@ -12,7 +12,7 @@ export type GetLimitsOptions = {
|
||||
export const getLimits = async ({ headers, teamId }: GetLimitsOptions = {}) => {
|
||||
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) {
|
||||
requestHeaders['team-id'] = teamId.toString();
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
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 { ERROR_CODES } from './errors';
|
||||
import { ZLimitsSchema } from './schema';
|
||||
@@ -16,7 +15,7 @@ export type GetServerLimitsOptions = {
|
||||
};
|
||||
|
||||
export const getServerLimits = async ({ email, teamId }: GetServerLimitsOptions) => {
|
||||
if (!IS_BILLING_ENABLED) {
|
||||
if (!IS_BILLING_ENABLED()) {
|
||||
return {
|
||||
quota: SELFHOSTED_PLAN_LIMITS,
|
||||
remaining: SELFHOSTED_PLAN_LIMITS,
|
||||
@@ -56,10 +55,11 @@ const handleUserLimits = async ({ email }: HandleUserLimitsOptions) => {
|
||||
);
|
||||
|
||||
if (activeSubscriptions.length > 0) {
|
||||
const communityPlanPrices = await getPricesByPlan(STRIPE_PLAN_TYPE.COMMUNITY);
|
||||
const documentPlanPrices = await getDocumentRelatedPrices();
|
||||
|
||||
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) {
|
||||
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]);
|
||||
};
|
||||
@@ -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 { stripe } from '@documenso/lib/server-only/stripe';
|
||||
|
||||
export const getPricesByPlan = async (
|
||||
plan: (typeof STRIPE_PLAN_TYPE)[keyof typeof STRIPE_PLAN_TYPE],
|
||||
) => {
|
||||
type PlanType = (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({
|
||||
query: `metadata['plan']:'${plan}' type:'recurring'`,
|
||||
query,
|
||||
expand: ['data.product'],
|
||||
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]);
|
||||
};
|
||||
@@ -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 { 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 { type Subscription, type Team, type User } from '@documenso/prisma/client';
|
||||
|
||||
import { deleteCustomerPaymentMethods } from './delete-customer-payment-methods';
|
||||
import { getCommunityPlanPriceIds } from './get-community-plan-prices';
|
||||
import { getTeamPrices } from './get-team-prices';
|
||||
import { getTeamRelatedPriceIds } from './get-team-related-prices';
|
||||
|
||||
type TransferStripeSubscriptionOptions = {
|
||||
/**
|
||||
@@ -46,14 +46,14 @@ export const transferTeamSubscription = async ({
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, 'Missing customer ID.');
|
||||
}
|
||||
|
||||
const [communityPlanIds, teamSeatPrices] = await Promise.all([
|
||||
getCommunityPlanPriceIds(),
|
||||
const [teamRelatedPlanPriceIds, teamSeatPrices] = await Promise.all([
|
||||
getTeamRelatedPriceIds(),
|
||||
getTeamPrices(),
|
||||
]);
|
||||
|
||||
const teamSubscriptionRequired = !subscriptionsContainsActiveCommunityPlan(
|
||||
const teamSubscriptionRequired = !subscriptionsContainsActivePlan(
|
||||
user.Subscription,
|
||||
communityPlanIds,
|
||||
teamRelatedPlanPriceIds,
|
||||
);
|
||||
|
||||
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 { TemplateDocumentImage } from './template-document-image';
|
||||
|
||||
@@ -10,7 +12,9 @@ export const TemplateDocumentSelfSigned = ({
|
||||
documentName,
|
||||
assetBaseUrl,
|
||||
}: 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) => {
|
||||
return new URL(path, assetBaseUrl).toString();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { env } from 'next-runtime-env';
|
||||
|
||||
import { Button, Section, Text } from '../components';
|
||||
import { TemplateDocumentImage } from './template-document-image';
|
||||
|
||||
@@ -8,6 +10,8 @@ export interface TemplateResetPasswordProps {
|
||||
}
|
||||
|
||||
export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordProps) => {
|
||||
const NEXT_PUBLIC_WEBAPP_URL = env('NEXT_PUBLIC_WEBAPP_URL');
|
||||
|
||||
return (
|
||||
<>
|
||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||
@@ -24,7 +28,7 @@ export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordPro
|
||||
<Section className="mb-6 mt-8 text-center">
|
||||
<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"
|
||||
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signin`}
|
||||
href={`${NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signin`}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
|
||||
@@ -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';
|
||||
export const IS_APP_WEB = process.env.NEXT_PUBLIC_PROJECT === 'web';
|
||||
export const IS_BILLING_ENABLED = process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED === 'true';
|
||||
import { env } from 'next-runtime-env';
|
||||
|
||||
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
|
||||
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
|
||||
? process.env.NEXT_PUBLIC_WEBAPP_URL
|
||||
: process.env.NEXT_PUBLIC_MARKETING_URL;
|
||||
export const IS_APP_MARKETING = process.env.NEXT_PUBLIC_PROJECT === 'marketing';
|
||||
export const IS_APP_WEB = process.env.NEXT_PUBLIC_PROJECT === 'web';
|
||||
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 {
|
||||
TEAM = 'team',
|
||||
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';
|
||||
|
||||
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.
|
||||
*/
|
||||
@@ -16,8 +21,7 @@ export const FEATURE_FLAG_POLL_INTERVAL = 30000;
|
||||
* Does not take any person or group properties into account.
|
||||
*/
|
||||
export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
|
||||
app_billing: process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED === 'true',
|
||||
app_teams: true,
|
||||
app_billing: NEXT_PUBLIC_FEATURE_BILLING_ENABLED() === 'true',
|
||||
marketing_header_single_player_mode: false,
|
||||
} as const;
|
||||
|
||||
@@ -25,8 +29,8 @@ export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
|
||||
* Extract the PostHog configuration from the environment.
|
||||
*/
|
||||
export function extractPostHogConfig(): { key: string; host: string } | null {
|
||||
const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
||||
const postHogHost = `${APP_BASE_URL}/ingest`;
|
||||
const postHogKey = NEXT_PUBLIC_POSTHOG_KEY();
|
||||
const postHogHost = `${APP_BASE_URL()}/ingest`;
|
||||
|
||||
if (!postHogKey || !postHogHost) {
|
||||
return null;
|
||||
|
||||
@@ -6,4 +6,4 @@ export const DEFAULT_HANDWRITING_FONT_SIZE = 50;
|
||||
export const MIN_STANDARD_FONT_SIZE = 8;
|
||||
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 type { GoogleProfile } from 'next-auth/providers/google';
|
||||
import GoogleProvider from 'next-auth/providers/google';
|
||||
import { env } from 'next-runtime-env';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { IdentityProvider, UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
|
||||
import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
|
||||
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 { sendConfirmationToken } from '../server-only/user/send-confirmation-token';
|
||||
import { extractNextAuthRequestMetadata } from '../universal/extract-request-metadata';
|
||||
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 {
|
||||
id: Number(user.id),
|
||||
email: user.email,
|
||||
@@ -203,7 +222,7 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
||||
async signIn({ user }) {
|
||||
// We do this to stop OAuth providers from creating an account
|
||||
// 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! });
|
||||
|
||||
return !!userData;
|
||||
|
||||
@@ -19,4 +19,5 @@ export const ErrorCode = {
|
||||
INCORRECT_PASSWORD: 'INCORRECT_PASSWORD',
|
||||
MISSING_ENCRYPTION_KEY: 'MISSING_ENCRYPTION_KEY',
|
||||
MISSING_BACKUP_CODE: 'MISSING_BACKUP_CODE',
|
||||
UNVERIFIED_EMAIL: 'UNVERIFIED_EMAIL',
|
||||
} as const;
|
||||
|
||||
@@ -43,7 +43,7 @@ export const setupTwoFactorAuthentication = async ({
|
||||
|
||||
const secret = crypto.randomBytes(10);
|
||||
|
||||
const backupCodes = new Array(10)
|
||||
const backupCodes = Array.from({ length: 10 })
|
||||
.fill(null)
|
||||
.map(() => crypto.randomBytes(5).toString('hex'))
|
||||
.map((code) => `${code.slice(0, 5)}-${code.slice(5)}`.toUpperCase());
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user