Documenso Supporter Pledge
- 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.
- 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.
- We aim to earn this trust by enabling everyone to self-host Documenso and inspect it’s
- inner workings. We openly share our source, knowledge, and progress while creating
- Documenso.
+ At Documenso, we envision a web-enabled future for business and contracts, and we are
+ committed to being the leading provider of open signing infrastructure. By combining
+ exceptional product design with open-source principles, we aim to deliver a robust and
+ well-designed application that exceeds your expectations.
- 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.
- We are building the next generation of trust software and community the way it’s meant
- to be: Beautifully designed and open for all to join.
+ Today we invite you to join us on this journey: By signing this mission statement you
+ signal your support of Documenso's mission{' '}
+
+ (in a non-legally binding, but heartfelt way)
+ {' '}
+ and lock in the community plan for forever, including everything we build this year.
diff --git a/apps/marketing/src/components/(marketing)/mobile-navigation.tsx b/apps/marketing/src/components/(marketing)/mobile-navigation.tsx
index 1c71da78a..434b30053 100644
--- a/apps/marketing/src/components/(marketing)/mobile-navigation.tsx
+++ b/apps/marketing/src/components/(marketing)/mobile-navigation.tsx
@@ -47,11 +47,11 @@ export const MENU_NAVIGATION_LINKS = [
text: 'Privacy',
},
{
- href: 'https://app.documenso.com/signin',
+ href: 'https://app.documenso.com/signin?utm_source=marketing-header',
text: 'Sign in',
},
{
- href: 'https://app.documenso.com/signup',
+ href: 'https://app.documenso.com/signup?utm_source=marketing-header',
text: 'Sign up',
},
];
@@ -108,13 +108,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
onClick={() => handleMenuItemClick()}
target={target}
>
- {href === 'https://app.documenso.com/signup' ? (
-
- {text}
-
- ) : (
- text
- )}
+ {text}
))}
diff --git a/apps/marketing/src/components/(marketing)/pricing-table.tsx b/apps/marketing/src/components/(marketing)/pricing-table.tsx
index 748f7307f..ab35bcc90 100644
--- a/apps/marketing/src/components/(marketing)/pricing-table.tsx
+++ b/apps/marketing/src/components/(marketing)/pricing-table.tsx
@@ -83,7 +83,11 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
-
+
Signup Now
@@ -114,7 +118,10 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
-
+
Signup Now
diff --git a/apps/marketing/src/components/(marketing)/single-player-mode/single-player-mode-success.tsx b/apps/marketing/src/components/(marketing)/single-player-mode/single-player-mode-success.tsx
index d8a8e2c53..32983d1f3 100644
--- a/apps/marketing/src/components/(marketing)/single-player-mode/single-player-mode-success.tsx
+++ b/apps/marketing/src/components/(marketing)/single-player-mode/single-player-mode-success.tsx
@@ -86,7 +86,7 @@ export const SinglePlayerModeSuccess = ({
Create a{' '}
diff --git a/apps/marketing/src/components/(marketing)/widget.tsx b/apps/marketing/src/components/(marketing)/widget.tsx
index 88b7f47c9..15e3fbdeb 100644
--- a/apps/marketing/src/components/(marketing)/widget.tsx
+++ b/apps/marketing/src/components/(marketing)/widget.tsx
@@ -208,7 +208,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
-
+
What’s your email?
@@ -265,11 +265,8 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
transform: 'translateX(25%)',
}}
>
-
- and your name?
+
+ And your name?
Claim your profile
- Profiles are coming soon! Claim your profile URL now to reserve your corner of the
+ Profiles are coming soon! Claim your profile username now to reserve your corner of the
signing revolution.
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/error.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/error.tsx
index 2c31195f9..1e1eb9921 100644
--- a/apps/web/src/app/(teams)/t/[teamUrl]/error.tsx
+++ b/apps/web/src/app/(teams)/t/[teamUrl]/error.tsx
@@ -34,7 +34,7 @@ export default function ErrorPage({ error }: ErrorProps) {
{
void router.back();
diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/not-found.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/not-found.tsx
index 6d172f090..35962e264 100644
--- a/apps/web/src/app/(teams)/t/[teamUrl]/not-found.tsx
+++ b/apps/web/src/app/(teams)/t/[teamUrl]/not-found.tsx
@@ -19,7 +19,7 @@ export default function NotFound() {
-
+
Go Back
diff --git a/apps/web/src/app/(unauthenticated)/signup/page.tsx b/apps/web/src/app/(unauthenticated)/signup/page.tsx
index 0c1c1fae0..ad758a8e9 100644
--- a/apps/web/src/app/(unauthenticated)/signup/page.tsx
+++ b/apps/web/src/app/(unauthenticated)/signup/page.tsx
@@ -1,16 +1,12 @@
import type { Metadata } from 'next';
-import Image from 'next/image';
-import Link from 'next/link';
import { redirect } from 'next/navigation';
import { env } from 'next-runtime-env';
-import communityCardsImage from '@documenso/assets/images/community-cards.png';
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';
-import { UserProfileSkeleton } from '~/components/ui/user-profile-skeleton';
+import { SignUpFormV2 } from '~/components/forms/v2/signup';
export const metadata: Metadata = {
title: 'Sign Up',
@@ -37,57 +33,10 @@ export default function SignUpPage({ searchParams }: SignUpPageProps) {
}
return (
-
-
-
-
-
-
-
-
-
-
- User profiles are coming soon!
-
-
-
-
-
-
-
-
-
-
Create a new account
-
-
- Create your account and start using state-of-the-art document signing. Open and beautiful
- signing is within your grasp.
-
-
-
-
-
-
-
- Already have an account?{' '}
-
- Sign in instead
-
-
-
-
+
);
}
diff --git a/apps/web/src/app/(unauthenticated)/signup/view.tsx b/apps/web/src/app/(unauthenticated)/signup/view.tsx
new file mode 100644
index 000000000..e94fb0344
--- /dev/null
+++ b/apps/web/src/app/(unauthenticated)/signup/view.tsx
@@ -0,0 +1,80 @@
+'use client';
+
+import { useState } from 'react';
+
+import Image from 'next/image';
+import Link from 'next/link';
+
+import communityCardsImage from '@documenso/assets/images/community-cards.png';
+import { Stepper } from '@documenso/ui/primitives/stepper';
+
+import { SignUpFormV2 } from '~/components/forms/v2/signup';
+import { UserProfileTimur } from '~/components/ui/user-profile-timur';
+
+type SignUpPageViewProps = {
+ email?: string;
+ isGoogleSSOEnabled?: boolean;
+};
+
+export const SignUpPageView = ({ email, isGoogleSSOEnabled }: SignUpPageViewProps) => {
+ const [step, setStep] = useState(1);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ User profiles are coming soon!
+
+
+ {
+
+ }
+
+
+
+
+
+
+
+ <>
+ Create a new account
+
+
+ Create your account and start using state-of-the-art document signing. Open and
+ beautiful signing is within your grasp.
+
+
+
+
+
+
+
+ Already have an account?{' '}
+
+ Sign in instead
+
+
+ >
+
+
+
+ );
+};
diff --git a/apps/web/src/components/(dashboard)/claim-username-card/claim-username-card.tsx b/apps/web/src/components/(dashboard)/claim-username-card/claim-username-card.tsx
deleted file mode 100644
index d215e91f0..000000000
--- a/apps/web/src/components/(dashboard)/claim-username-card/claim-username-card.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-'use client';
-
-import React from 'react';
-
-import Image from 'next/image';
-
-import { BadgeCheck, File } from 'lucide-react';
-
-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';
-
-export default function ClaimUsernameCard() {
- const onSignUpClick = () => {};
- return (
-
-
-
-
-
-
-
- documenso.com/u/timur
-
-
-
- Timur
-
-
- Timur Ercan
-
-
- Hey I’m Timur Pick any of the following agreements below and start signing to get
- started
-
-
-
- Documents
-
-
-
-
-
- NDA.pdf
-
- Like to discuss about my work?
-
-
-
-
- Sign
-
-
-
-
-
-
-
- NDA.pdf
-
- Like to discuss about my work?
-
-
-
-
- Sign
-
-
-
-
-
- Claim Community Plan
-
- -80%
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/components/(dashboard)/layout/new/new-header.tsx b/apps/web/src/components/(dashboard)/layout/new/new-header.tsx
deleted file mode 100644
index 66036359c..000000000
--- a/apps/web/src/components/(dashboard)/layout/new/new-header.tsx
+++ /dev/null
@@ -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;
-
-export const NewHeader = ({ className, ...props }: HeaderProps) => {
- const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
-
- return (
-
-
- setIsHamburgerMenuOpen(false)}>
-
-
-
-
-
-
- Pricing
-
-
-
- Blog
-
-
-
- Open Startup
-
-
-
- Sign in
-
-
-
- Sign up
-
-
-
-
- setIsHamburgerMenuOpen((v) => !v)}
- isMenuOpen={isHamburgerMenuOpen}
- />
-
-
- );
-};
diff --git a/apps/web/src/components/(dashboard)/layout/new/new-mobile-hamburger.tsx b/apps/web/src/components/(dashboard)/layout/new/new-mobile-hamburger.tsx
deleted file mode 100644
index 8b7666df4..000000000
--- a/apps/web/src/components/(dashboard)/layout/new/new-mobile-hamburger.tsx
+++ /dev/null
@@ -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 (
-
-
- {isMenuOpen ? : }
-
-
- );
-};
diff --git a/apps/web/src/components/(dashboard)/layout/new/new-mobile-navigation.tsx b/apps/web/src/components/(dashboard)/layout/new/new-mobile-navigation.tsx
deleted file mode 100644
index 0d104eeb6..000000000
--- a/apps/web/src/components/(dashboard)/layout/new/new-mobile-navigation.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
-
-
- {MENU_NAVIGATION_LINKS.map(({ href, text, target }) => (
-
- handleMenuItemClick()}
- target={target}
- >
- {href === 'https://app.documenso.com/signup' ? (
-
- {text}
-
- ) : (
- text
- )}
-
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/forms/public-profile-claim-dialog.tsx b/apps/web/src/components/forms/public-profile-claim-dialog.tsx
index 54a602dee..dbd52fd27 100644
--- a/apps/web/src/components/forms/public-profile-claim-dialog.tsx
+++ b/apps/web/src/components/forms/public-profile-claim-dialog.tsx
@@ -9,6 +9,7 @@ import { useForm } from 'react-hook-form';
import { z } from 'zod';
import profileClaimTeaserImage from '@documenso/assets/images/profile-claim-teaser.png';
+import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { User } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
@@ -35,7 +36,14 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { UserProfileSkeleton } from '../ui/user-profile-skeleton';
export const ZClaimPublicProfileFormSchema = z.object({
- url: z.string().trim().min(1, { message: 'Please enter a valid URL slug.' }),
+ url: z
+ .string()
+ .trim()
+ .toLowerCase()
+ .min(1, { message: 'Please enter a valid username.' })
+ .regex(/^[a-z0-9-]+$/, {
+ message: 'Username can only container alphanumeric characters and dashes.',
+ }),
});
export type TClaimPublicProfileFormSchema = z.infer;
@@ -57,6 +65,8 @@ export const ClaimPublicProfileDialogForm = ({
const [claimed, setClaimed] = useState(false);
+ const baseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000');
+
const form = useForm({
values: {
url: user.url || '',
@@ -82,12 +92,17 @@ export const ClaimPublicProfileDialogForm = ({
if (error.code === AppErrorCode.PROFILE_URL_TAKEN) {
form.setError('url', {
type: 'manual',
- message: 'This URL is already taken',
+ message: 'This username is already taken',
+ });
+ } else if (error.code === AppErrorCode.PREMIUM_PROFILE_URL) {
+ form.setError('url', {
+ type: 'manual',
+ message: error.message,
});
} else if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
toast({
title: 'An error occurred',
- description: err.message,
+ description: error.userMessage ?? error.message,
variant: 'destructive',
});
} else {
@@ -131,7 +146,7 @@ export const ClaimPublicProfileDialogForm = ({
name="url"
render={({ field }) => (
- Public profile URL
+ Public profile username
@@ -140,7 +155,7 @@ export const ClaimPublicProfileDialogForm = ({
- documenso.com/u/{field.value || ''}
+ {baseUrl.host}/u/{field.value || ''}
)}
diff --git a/apps/web/src/components/forms/signup.tsx b/apps/web/src/components/forms/signup.tsx
index 0ec562d7d..9b39c31db 100644
--- a/apps/web/src/components/forms/signup.tsx
+++ b/apps/web/src/components/forms/signup.tsx
@@ -61,7 +61,7 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
const router = useRouter();
const searchParams = useSearchParams();
- const src = searchParams?.get('src') ?? null;
+ const utmSrc = searchParams?.get('utm_source') ?? null;
const form = useForm({
values: {
@@ -93,7 +93,7 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
analytics.capture('App: User Sign Up', {
email,
timestamp: new Date().toISOString(),
- custom_campaign_params: { src },
+ custom_campaign_params: { src: utmSrc },
});
} catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
diff --git a/apps/web/src/components/forms/v2/signup.tsx b/apps/web/src/components/forms/v2/signup.tsx
new file mode 100644
index 000000000..713bde4b4
--- /dev/null
+++ b/apps/web/src/components/forms/v2/signup.tsx
@@ -0,0 +1,463 @@
+'use client';
+
+import { useState } from 'react';
+
+import Image from 'next/image';
+import Link from 'next/link';
+import { useRouter, useSearchParams } from 'next/navigation';
+
+import { zodResolver } from '@hookform/resolvers/zod';
+import { AnimatePresence, motion } from 'framer-motion';
+import { signIn } from 'next-auth/react';
+import { useForm } from 'react-hook-form';
+import { FcGoogle } from 'react-icons/fc';
+import { z } from 'zod';
+
+import communityCardsImage from '@documenso/assets/images/community-cards.png';
+import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
+import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { TRPCClientError } from '@documenso/trpc/client';
+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 {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@documenso/ui/primitives/form/form';
+import { Input } from '@documenso/ui/primitives/input';
+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 { UserProfileSkeleton } from '~/components/ui/user-profile-skeleton';
+import { UserProfileTimur } from '~/components/ui/user-profile-timur';
+
+const SIGN_UP_REDIRECT_PATH = '/documents';
+
+type SignUpStep = 'BASIC_DETAILS' | 'CLAIM_USERNAME';
+
+export const ZSignUpFormV2Schema = z
+ .object({
+ name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
+ email: z.string().email().min(1),
+ password: ZPasswordSchema,
+ signature: z.string().min(1, { message: 'We need your signature to sign documents' }),
+ url: z
+ .string()
+ .trim()
+ .toLowerCase()
+ .min(1, { message: 'We need a username to create your profile' })
+ .regex(/^[a-z0-9-]+$/, {
+ message: 'Username can only container alphanumeric characters and dashes.',
+ }),
+ })
+ .refine(
+ (data) => {
+ const { name, email, password } = data;
+ return !password.includes(name) && !password.includes(email.split('@')[0]);
+ },
+ {
+ message: 'Password should not be common or based on personal information',
+ },
+ );
+
+export type TSignUpFormV2Schema = z.infer;
+
+export type SignUpFormV2Props = {
+ className?: string;
+ initialEmail?: string;
+ isGoogleSSOEnabled?: boolean;
+};
+
+export const SignUpFormV2 = ({
+ className,
+ initialEmail,
+ isGoogleSSOEnabled,
+}: SignUpFormV2Props) => {
+ const { toast } = useToast();
+ const analytics = useAnalytics();
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const [step, setStep] = useState('BASIC_DETAILS');
+
+ const utmSrc = searchParams?.get('utm_source') ?? null;
+
+ const baseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000');
+
+ const form = useForm({
+ values: {
+ name: '',
+ email: initialEmail ?? '',
+ password: '',
+ signature: '',
+ url: '',
+ },
+ mode: 'onBlur',
+ resolver: zodResolver(ZSignUpFormV2Schema),
+ });
+
+ const isSubmitting = form.formState.isSubmitting;
+
+ const name = form.watch('name');
+ const url = form.watch('url');
+
+ // To continue we need to make sure name, email, password and signature are valid
+ const canContinue =
+ form.formState.dirtyFields.name &&
+ form.formState.errors.name === undefined &&
+ form.formState.dirtyFields.email &&
+ form.formState.errors.email === undefined &&
+ form.formState.dirtyFields.password &&
+ form.formState.errors.password === undefined &&
+ form.formState.dirtyFields.signature &&
+ form.formState.errors.signature === undefined;
+
+ console.log({ formSTate: form.formState });
+
+ const { mutateAsync: signup } = trpc.auth.signup.useMutation();
+
+ const onFormSubmit = async ({ name, email, password, signature, url }: TSignUpFormV2Schema) => {
+ try {
+ await signup({ name, email, password, signature, url });
+
+ router.push(`/unverified-account`);
+
+ 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: utmSrc },
+ });
+ } catch (err) {
+ const error = AppError.parseError(err);
+
+ if (error.code === AppErrorCode.PROFILE_URL_TAKEN) {
+ form.setError('url', {
+ type: 'manual',
+ message: 'This username has already been taken',
+ });
+ } else if (error.code === AppErrorCode.PREMIUM_PROFILE_URL) {
+ form.setError('url', {
+ type: 'manual',
+ message: error.message,
+ });
+ } else 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',
+ description:
+ 'We encountered an unknown error while attempting to sign you up. Please try again later.',
+ variant: 'destructive',
+ });
+ }
+ }
+ };
+
+ const onSignUpWithGoogleClick = async () => {
+ try {
+ await signIn('google', { callbackUrl: SIGN_UP_REDIRECT_PATH });
+ } catch (err) {
+ toast({
+ title: 'An unknown error occurred',
+ description:
+ 'We encountered an unknown error while attempting to sign you Up. Please try again later.',
+ variant: 'destructive',
+ });
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ User profiles are coming soon!
+
+
+
+ {step === 'BASIC_DETAILS' ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+
+
+
+ {step === 'BASIC_DETAILS' && (
+
+
Create a new account
+
+
+ Create your account and start using state-of-the-art document signing. Open and
+ beautiful signing is within your grasp.
+
+
+ )}
+
+ {step === 'CLAIM_USERNAME' && (
+
+
Claim your username now
+
+
+ You will get notified & be able to set up your documenso public profile when we launch
+ the feature.
+
+
+ )}
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/web/src/components/partials/not-found.tsx b/apps/web/src/components/partials/not-found.tsx
index dc059a324..b80c6fea8 100644
--- a/apps/web/src/components/partials/not-found.tsx
+++ b/apps/web/src/components/partials/not-found.tsx
@@ -46,7 +46,7 @@ export default function NotFoundPartial({ children }: NotFoundPartialProps) {
{
void router.back();
diff --git a/apps/web/src/components/ui/user-profile-skeleton.tsx b/apps/web/src/components/ui/user-profile-skeleton.tsx
index 9b5ce1f61..1c8f35b64 100644
--- a/apps/web/src/components/ui/user-profile-skeleton.tsx
+++ b/apps/web/src/components/ui/user-profile-skeleton.tsx
@@ -1,18 +1,16 @@
'use client';
-import { File } from 'lucide-react';
+import { File, User2 } from 'lucide-react';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import type { User } from '@documenso/prisma/client';
import { VerifiedIcon } from '@documenso/ui/icons/verified';
import { cn } from '@documenso/ui/lib/utils';
-import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
export type UserProfileSkeletonProps = {
className?: string;
- user: Pick;
+ user: Pick;
rows?: number;
};
@@ -26,28 +24,27 @@ export const UserProfileSkeleton = ({ className, user, rows = 2 }: UserProfileSk
className,
)}
>
-
- {baseUrl.host}/u/{user.url}
+
+ {baseUrl.host}/u/
+ {user.url}
-
-
-
- {user.name ? extractInitials(user.name) : user.email.slice(0, 1).toUpperCase()}
-
-
+
-
-
{user.name}
+
+
{user.name}
-
+
@@ -68,7 +65,7 @@ export const UserProfileSkeleton = ({ className, user, rows = 2 }: UserProfileSk
diff --git a/apps/web/src/components/ui/user-profile-timur.tsx b/apps/web/src/components/ui/user-profile-timur.tsx
new file mode 100644
index 000000000..e99a314b4
--- /dev/null
+++ b/apps/web/src/components/ui/user-profile-timur.tsx
@@ -0,0 +1,87 @@
+'use client';
+
+import Image from 'next/image';
+
+import { File } from 'lucide-react';
+
+import timurImage from '@documenso/assets/images/timur.png';
+import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
+import { VerifiedIcon } from '@documenso/ui/icons/verified';
+import { cn } from '@documenso/ui/lib/utils';
+import { Button } from '@documenso/ui/primitives/button';
+
+export type UserProfileTimurProps = {
+ className?: string;
+ rows?: number;
+};
+
+export const UserProfileTimur = ({ className, rows = 2 }: UserProfileTimurProps) => {
+ const baseUrl = new URL(NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000');
+
+ return (
+
+
+ {baseUrl.host}/u/timur
+
+
+
+
+
+
+
+
+
Timur Ercan
+
+
+
+
+
Hey I’m Timur
+
+
+ Pick any of the following agreements below and start signing to get started
+
+
+
+
+
+
+ Documents
+
+
+ {Array(rows)
+ .fill(0)
+ .map((_, index) => (
+
+ ))}
+
+
+
+ );
+};
diff --git a/packages/assets/images/Timur.png b/packages/assets/images/Timur.png
deleted file mode 100644
index 8f901044e..000000000
Binary files a/packages/assets/images/Timur.png and /dev/null differ
diff --git a/packages/assets/images/timur.png b/packages/assets/images/timur.png
new file mode 100644
index 000000000..2adf31596
Binary files /dev/null and b/packages/assets/images/timur.png differ
diff --git a/packages/lib/constants/feature-flags.ts b/packages/lib/constants/feature-flags.ts
index ac476bc70..ebd09c73a 100644
--- a/packages/lib/constants/feature-flags.ts
+++ b/packages/lib/constants/feature-flags.ts
@@ -25,6 +25,7 @@ export const LOCAL_FEATURE_FLAGS: Record
= {
app_teams: true,
app_document_page_view_history_sheet: false,
marketing_header_single_player_mode: false,
+ marketing_profiles_announcement_bar: true,
} as const;
/**
diff --git a/packages/lib/errors/app-error.ts b/packages/lib/errors/app-error.ts
index bc2db70c2..f43f9c3ba 100644
--- a/packages/lib/errors/app-error.ts
+++ b/packages/lib/errors/app-error.ts
@@ -19,6 +19,7 @@ export enum AppErrorCode {
'SCHEMA_FAILED' = 'SchemaFailed',
'TOO_MANY_REQUESTS' = 'TooManyRequests',
'PROFILE_URL_TAKEN' = 'ProfileUrlTaken',
+ 'PREMIUM_PROFILE_URL' = 'PremiumProfileUrl',
}
const genericErrorCodeToTrpcErrorCodeMap: Record = {
@@ -34,6 +35,7 @@ const genericErrorCodeToTrpcErrorCodeMap: Record = {
[AppErrorCode.SCHEMA_FAILED]: 'INTERNAL_SERVER_ERROR',
[AppErrorCode.TOO_MANY_REQUESTS]: 'TOO_MANY_REQUESTS',
[AppErrorCode.PROFILE_URL_TAKEN]: 'BAD_REQUEST',
+ [AppErrorCode.PREMIUM_PROFILE_URL]: 'BAD_REQUEST',
};
export const ZAppErrorJsonSchema = z.object({
diff --git a/packages/lib/server-only/user/create-user.ts b/packages/lib/server-only/user/create-user.ts
index 1852dc12e..dbcec9efb 100644
--- a/packages/lib/server-only/user/create-user.ts
+++ b/packages/lib/server-only/user/create-user.ts
@@ -7,15 +7,17 @@ import { IdentityProvider, Prisma, TeamMemberInviteStatus } from '@documenso/pri
import { IS_BILLING_ENABLED } from '../../constants/app';
import { SALT_ROUNDS } from '../../constants/auth';
+import { AppError, AppErrorCode } from '../../errors/app-error';
export interface CreateUserOptions {
name: string;
email: string;
password: string;
signature?: string | null;
+ url?: string;
}
-export const createUser = async ({ name, email, password, signature }: CreateUserOptions) => {
+export const createUser = async ({ name, email, password, signature, url }: CreateUserOptions) => {
const hashedPassword = await hash(password, SALT_ROUNDS);
const userExists = await prisma.user.findFirst({
@@ -28,6 +30,22 @@ export const createUser = async ({ name, email, password, signature }: CreateUse
throw new Error('User already exists');
}
+ if (url) {
+ const urlExists = await prisma.user.findFirst({
+ where: {
+ url,
+ },
+ });
+
+ if (urlExists) {
+ throw new AppError(
+ AppErrorCode.PROFILE_URL_TAKEN,
+ 'Profile username is taken',
+ 'The profile username is already taken',
+ );
+ }
+ }
+
const user = await prisma.user.create({
data: {
name,
@@ -35,6 +53,7 @@ export const createUser = async ({ name, email, password, signature }: CreateUse
password: hashedPassword,
signature,
identityProvider: IdentityProvider.DOCUMENSO,
+ url,
},
});
diff --git a/packages/lib/server-only/user/update-public-profile.ts b/packages/lib/server-only/user/update-public-profile.ts
index 0aebe3ecf..f70f02cf2 100644
--- a/packages/lib/server-only/user/update-public-profile.ts
+++ b/packages/lib/server-only/user/update-public-profile.ts
@@ -23,8 +23,8 @@ export const updatePublicProfile = async ({ userId, url }: UpdatePublicProfileOp
if (isUrlTaken) {
throw new AppError(
AppErrorCode.PROFILE_URL_TAKEN,
- 'Profile URL is taken',
- 'The profile URL is already taken',
+ 'Profile username is taken',
+ 'The profile username is already taken',
);
}
diff --git a/packages/trpc/server/auth-router/router.ts b/packages/trpc/server/auth-router/router.ts
index 65fe8d296..3f199ac11 100644
--- a/packages/trpc/server/auth-router/router.ts
+++ b/packages/trpc/server/auth-router/router.ts
@@ -1,6 +1,8 @@
import { TRPCError } from '@trpc/server';
import { env } from 'next-runtime-env';
+import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
import { compareSync } from '@documenso/lib/server-only/auth/hash';
import { createUser } from '@documenso/lib/server-only/user/create-user';
@@ -21,14 +23,29 @@ export const authRouter = router({
});
}
- const { name, email, password, signature } = input;
+ const { name, email, password, signature, url } = input;
- const user = await createUser({ name, email, password, signature });
+ if ((true || IS_BILLING_ENABLED()) && url && url.length <= 6) {
+ throw new AppError(
+ AppErrorCode.PREMIUM_PROFILE_URL,
+ 'Only subscribers can have a username shorter than 6 characters',
+ );
+ }
+
+ const user = await createUser({ name, email, password, signature, url });
await sendConfirmationToken({ email: user.email });
return user;
} catch (err) {
+ console.log(err);
+
+ const error = AppError.parseError(err);
+
+ if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
+ throw AppError.parseErrorToTRPCError(error);
+ }
+
let message =
'We were unable to create your account. Please review the information you provided and try again.';
diff --git a/packages/trpc/server/auth-router/schema.ts b/packages/trpc/server/auth-router/schema.ts
index dbe42a25c..9cab2b415 100644
--- a/packages/trpc/server/auth-router/schema.ts
+++ b/packages/trpc/server/auth-router/schema.ts
@@ -21,6 +21,7 @@ export const ZSignUpMutationSchema = z.object({
email: z.string().email(),
password: ZPasswordSchema,
signature: z.string().min(1, { message: 'A signature is required.' }),
+ url: z.string().optional(),
});
export type TSignUpMutationSchema = z.infer;
diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts
index 2b83caa84..f9f409aa6 100644
--- a/packages/trpc/server/profile-router/router.ts
+++ b/packages/trpc/server/profile-router/router.ts
@@ -1,6 +1,8 @@
import { TRPCError } from '@trpc/server';
+import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscription/get-subscriptions-by-user-id';
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
import { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs';
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
@@ -11,6 +13,7 @@ import { updatePassword } from '@documenso/lib/server-only/user/update-password'
import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
import { updatePublicProfile } from '@documenso/lib/server-only/user/update-public-profile';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
+import { SubscriptionStatus } from '@documenso/prisma/client';
import { adminProcedure, authenticatedProcedure, procedure, router } from '../trpc';
import {
@@ -83,6 +86,21 @@ export const profileRouter = router({
try {
const { url } = input;
+ if (IS_BILLING_ENABLED() && url.length <= 6) {
+ const subscriptions = await getSubscriptionsByUserId({
+ userId: ctx.user.id,
+ }).then((subscriptions) =>
+ subscriptions.filter((s) => s.status === SubscriptionStatus.ACTIVE),
+ );
+
+ if (subscriptions.length === 0) {
+ throw new AppError(
+ AppErrorCode.PREMIUM_PROFILE_URL,
+ 'Only subscribers can have a username shorter than 6 characters',
+ );
+ }
+ }
+
const user = await updatePublicProfile({
userId: ctx.user.id,
url,
diff --git a/packages/trpc/server/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts
index ecee47f34..dc62f83ba 100644
--- a/packages/trpc/server/profile-router/schema.ts
+++ b/packages/trpc/server/profile-router/schema.ts
@@ -17,7 +17,14 @@ export const ZUpdateProfileMutationSchema = z.object({
});
export const ZUpdatePublicProfileMutationSchema = z.object({
- url: z.string().min(1),
+ url: z
+ .string()
+ .trim()
+ .toLowerCase()
+ .min(1, { message: 'Please enter a valid username.' })
+ .regex(/^[a-z0-9-]+$/, {
+ message: 'Username can only container alphanumeric characters and dashes.',
+ }),
});
export const ZUpdatePasswordMutationSchema = z.object({
diff --git a/packages/ui/primitives/announcement-bar.tsx b/packages/ui/primitives/announcement-bar.tsx
deleted file mode 100644
index aadd17047..000000000
--- a/packages/ui/primitives/announcement-bar.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import Link from 'next/link';
-
-import { cn } from '../lib/utils';
-
-interface AnnouncementBarProps {
- isShown: boolean;
- className: string;
-}
-
-export const AnnouncementBar: React.FC = ({ isShown, className }) => {
- return (
- isShown && (
-
-
- Claim your documenso public profile URL now! {' '}
- documenso.com/u/yourname
-
-
-
-
- )
- );
-};