Compare commits

..

56 Commits

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

Add support for enterprise billing plans.

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

They will also get additional features in the future.

## Notes

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

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

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

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

See also https://securitytxt.org
2024-02-16 12:34:41 +11:00
Sumit Bisht 25291b64eb fix: highlighting issue in recipient selection 2024-02-15 22:25:23 +05:30
Lucas Smith fe2093fe7c feat: add next-runtime-env (#869)
This PR adds the package
[next-runtime-env](https://github.com/expatfile/next-runtime-env/) to
populate the public environment variables at runtime.
2024-02-15 22:10:21 +11:00
Ephraim Atta-Duncan 49cddfab38 chore: lint with oxc 2024-02-15 06:11:50 +00:00
Timur Ercan 3e12a05ab8 chore: more grammar 2024-02-14 17:19:48 +01:00
Timur Ercan a76504c0a4 Merge branch 'main' into chore-security-text 2024-02-14 17:16:44 +01:00
Timur Ercan abab0c0a22 chore: grammer and format 2024-02-14 17:14:43 +01:00
Lucas Smith f72b669f67 feat: restrict app access for unverified users (#835) 2024-02-13 20:22:43 +11:00
Lucas Smith 536cafde31 Merge branch 'main' into feat/disable-access-unverified-users 2024-02-13 20:19:16 +11:00
Lucas Smith d052f02013 chore: refactor code 2024-02-13 06:01:25 +00:00
Catalin Pit 4878cf388f chore: add the missing signIn function 2024-02-13 07:53:36 +02:00
Catalin Pit 149f416be7 chore: refactor code 2024-02-13 07:50:22 +02:00
Catalin Pit c432261dd8 chore: disable button while form is submitting 2024-02-12 09:49:59 +02:00
Catalin Pit 1852aa4b05 chore: add info 2024-02-12 09:49:59 +02:00
Lucas Smith 51608ed390 fix: lint issue 2024-02-12 02:02:43 +00:00
Lucas Smith 8ebef831ac Merge branch 'main' into feat/add-runtime-env 2024-02-12 12:30:35 +11:00
Lucas Smith 20e2976731 fix: build issues 2024-02-12 01:29:22 +00:00
Lucas Smith 748bf6de6b fix: add dropped constants from merge 2024-02-08 22:12:04 +11:00
Lucas Smith d13cf743bf Merge branch 'main' into feat/add-runtime-env 2024-02-08 22:06:59 +11:00
David Nguyen cad48236a0 Merge branch 'main' into feat/disable-access-unverified-users 2024-02-07 16:30:22 +11:00
apoorv taneja c970abc871 added onchange handler 2024-02-02 20:46:54 +05:30
apoorv taneja d5b3df1648 fixed variable declaration 2024-02-02 19:32:39 +05:30
apoorv taneja 142c1c003e changed useEffect variables 2024-02-02 18:16:54 +05:30
apoorv taneja a06c628653 Merge branch 'main' of https://github.com/plxity/documenso into fix/undo-button-in-canvas 2024-02-02 17:56:58 +05:30
apoorv taneja 7ca3697303 Merge branch 'main' of https://github.com/plxity/documenso into fix/undo-button-in-canvas 2024-02-02 14:49:06 +05:30
Lucas Smith 8ac2209493 Merge branch 'main' into chore-security-text 2024-02-02 16:16:25 +11:00
Lucas Smith 9c4ec34a3c fix: add precommit step for .well-known 2024-02-02 04:00:28 +00:00
Timur Ercan 1f142e334a Merge branch 'main' into chore-security-text 2024-01-31 20:31:34 +01:00
Mythie 08f82b23dc fix: update env entries to evaluate at runtime 2024-01-31 22:32:42 +11:00
Timur Ercan 747a7b0aea chore: security contacts and descr 2024-01-30 16:15:32 +01:00
Catalin Pit 6053a4a40a chore: refactor 2024-01-30 12:56:32 +02:00
Catalin Pit cc090adce0 chore: refactor 2024-01-30 12:54:48 +02:00
Timur Ercan 375df71f5c Merge branch 'main' into chore-security-text 2024-01-29 16:43:57 +01:00
Catalin Pit c0bb5205e1 chore: merged main 2024-01-29 10:14:56 +02:00
Catalin Pit 1676f5bf6c chore: removed unused code 2024-01-29 09:43:38 +02:00
Catalin Pit f514d55d27 chore: removed unused schema 2024-01-29 09:41:02 +02:00
Tangerine Kugelmann 927a656c57 Create security.txt
See also https://securitytxt.org
2024-01-28 01:00:07 +01:00
Catalin Pit 751fb5275c Merge branch 'main' into feat/add-runtime-env 2024-01-26 14:04:54 +02:00
Catalin Pit b2cca9afb6 chore: refactor 2024-01-26 13:27:36 +02:00
Catalin Pit e2fa01509d chore: avoid returning unnecessary info 2024-01-25 17:33:35 +02:00
Catalin Pit 311c8da8fc chore: encrypt and decrypt email addr 2024-01-25 17:24:37 +02:00
Catalin Pit 49ecfc1a2c chore: refactor 2024-01-25 15:42:40 +02:00
Catalin Pit ffee2b2c9a chore: merged main 2024-01-25 13:43:11 +02:00
Catalin Pit 2f18518961 chore: merged main 2024-01-25 10:53:05 +02:00
Catalin Pit d451a7acce feat: add next-runtime-env 2024-01-25 10:48:20 +02:00
apoorv taneja d8aecc4092 fixed undo operation on signature pad 2024-01-25 13:21:55 +05:30
Sumit Bisht e5c2263e92 fix: imporoved document-dropzone ui for small vertical screens 2024-01-23 18:37:02 +05:30
Catalin Pit 4aefb80989 feat: restrict app access for unverified users 2024-01-16 14:25:05 +02:00
147 changed files with 1201 additions and 58457 deletions
+12
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
# General Issues
Contact: https://github.com/documenso/documenso/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml
# Report critical issues privately to let us take appropriate action before publishing.
Contact: mailto:security@documenso.com
Preferred-Languages: en
Canonical: https://documenso.com/.well-known/security.txt
@@ -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
+1 -1
View File
@@ -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
View File
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
+28 -24
View File
@@ -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();
+1 -1
View File
@@ -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 its
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 its 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>
@@ -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) {
+7
View File
@@ -0,0 +1,7 @@
# General Issues
Contact: https://github.com/documenso/documenso/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml
# Report critical issues privately to let us take appropriate action before publishing.
Contact: mailto:security@documenso.com
Preferred-Languages: en
Canonical: https://documenso.com/.well-known/security.txt
+1 -1
View File
@@ -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 Im 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
View File
@@ -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 Im 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>
);
};
+139 -137
View File
@@ -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>
);
};
+104 -240
View File
@@ -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();
+3 -1
View File
@@ -1,3 +1,5 @@
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
/**
* getAssetBuffer is used to retrieve array buffers for various assets
* 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());
};
-1
View File
@@ -32,7 +32,6 @@ export function PostHogPageview() {
// Do nothing.
});
},
custom_campaign_params: ['src'],
});
}
+3 -3
View File
@@ -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',
};
+16
View File
@@ -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
View File
@@ -47,7 +47,9 @@
"apps/*",
"packages/*"
],
"dependencies": {},
"dependencies": {
"next-runtime-env": "^3.2.0"
},
"overrides": {
"next-auth": {
"next": "14.0.3"
-3
View File
@@ -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

+1 -1
View File
@@ -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();
+5 -5
View File
@@ -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, []);
};
+12 -9
View File
@@ -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';
+1 -2
View File
@@ -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';
+8 -4
View File
@@ -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;
+1 -1
View File
@@ -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`;
+20 -1
View File
@@ -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;
+1
View File
@@ -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;
+1 -1
View File
@@ -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