diff --git a/apps/marketing/src/app/(marketing)/layout.tsx b/apps/marketing/src/app/(marketing)/layout.tsx index ab9de03d5..c5f761853 100644 --- a/apps/marketing/src/app/(marketing)/layout.tsx +++ b/apps/marketing/src/app/(marketing)/layout.tsx @@ -5,9 +5,10 @@ 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 launchWeekTwoImage from '@documenso/assets/images/background-lw-2.png'; +import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag'; +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; 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'; @@ -20,6 +21,10 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) { const [scrollY, setScrollY] = useState(0); const pathname = usePathname(); + const { getFlag } = useFeatureFlags(); + + const showProfilesAnnouncementBar = getFlag('marketing_profiles_announcement_bar'); + useEffect(() => { const onScroll = () => { setScrollY(window.scrollY); @@ -41,14 +46,31 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) { 'bg-background/50 backdrop-blur-md': scrollY > 5, })} > -
- background pattern -
- + {showProfilesAnnouncementBar && ( +
+
+ Launch Week 2 +
+ +
+ Claim your documenso public profile username now!{' '} + documenso.com/u/yourname +
+ + Claim Now + +
+
+
+ )} +
diff --git a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx index 9f1ebb289..4c1162599 100644 --- a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx +++ b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx @@ -191,7 +191,7 @@ export const SinglePlayerClient = () => {

Create a{' '} diff --git a/apps/marketing/src/app/not-found.tsx b/apps/marketing/src/app/not-found.tsx index a54f8ea34..d85cdb62f 100644 --- a/apps/marketing/src/app/not-found.tsx +++ b/apps/marketing/src/app/not-found.tsx @@ -43,7 +43,7 @@ export default function NotFound() {

diff --git a/apps/marketing/src/components/(marketing)/header.tsx b/apps/marketing/src/components/(marketing)/header.tsx index 038185031..915c13852 100644 --- a/apps/marketing/src/components/(marketing)/header.tsx +++ b/apps/marketing/src/components/(marketing)/header.tsx @@ -9,6 +9,7 @@ import Link from 'next/link'; import LogoImage from '@documenso/assets/logo.png'; import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag'; import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; import { HamburgerMenu } from './mobile-hamburger'; import { MobileNavigation } from './mobile-navigation'; @@ -68,21 +69,18 @@ export const Header = ({ className, ...props }: HeaderProps) => { Sign in - - + +
{ > Claim Community Plan - -80% + $30/mo @@ -191,32 +191,41 @@ export const Hero = ({ className, ...props }: HeroProps) => { 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) => {

@@ -114,7 +118,10 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {

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) => { -

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) {
-
-
-
-
- -
- NDA.pdf - - Like to discuss about my work? - -
-
- -
- - - - - - - ); -} 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)}> - Documenso Logo - -
- -
- - 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 ( -
- -
- ); -}; 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 ( - - - - Documenso Logo - - - - {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 ( +
+
+
+ community-cards +
+ +
+ +
+
+ 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. +

+
+ )} + +
+ +
+ + {step === 'BASIC_DETAILS' && ( +
+ ( + + Full Name + + + + + + )} + /> + + ( + + Email Address + + + + + + )} + /> + + ( + + Password + + + + + + + + )} + /> + + ( + + Sign Here + + onChange(v ?? '')} + /> + + + + + )} + /> + + {isGoogleSSOEnabled && ( + <> +
+
+ Or +
+
+ + + + )} + +

+ Already have an account?{' '} + + Sign in instead + +

+
+ )} + + {step === 'CLAIM_USERNAME' && ( +
+ ( + + Public profile username + + + + + + + +
+ {baseUrl.host}/u/{field.value || ''} +
+
+ )} + /> +
+ )} + +
+ {step === 'BASIC_DETAILS' && ( +

+ Basic details 1/2 +

+ )} + + {step === 'CLAIM_USERNAME' && ( +

+ Claim username 2/2 +

+ )} + +
+ +
+
+ +
+ {/* Go back button, disabled if step is basic details */} + + + {/* Continue button */} + {step === 'BASIC_DETAILS' && ( + + )} + + {/* Sign up button */} + {step === 'CLAIM_USERNAME' && ( + + )} +
+
+ +
+
+ ); +}; 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) {
+
+
+ ))} +
+ + + ); +}; 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 -
- -
-
- Claim now -
-
-
- ) - ); -};