diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9a564b058..74fcb319b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,7 +9,7 @@ updates: labels: - "ci dependencies" - "ci" - open-pull-requests-limit: 10 + open-pull-requests-limit: 2 - package-ecosystem: "npm" directory: "/apps/marketing" @@ -19,7 +19,7 @@ updates: labels: - "npm dependencies" - "frontend" - open-pull-requests-limit: 10 + open-pull-requests-limit: 2 - package-ecosystem: "npm" directory: "/apps/web" @@ -29,4 +29,4 @@ updates: labels: - "npm dependencies" - "frontend" - open-pull-requests-limit: 10 + open-pull-requests-limit: 2 diff --git a/README.md b/README.md index 29ffb0d65..aa00aed54 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- Documenso Logo + Documenso Logo

@@ -26,37 +26,30 @@ Commits-per-month

-> **🚧 We're currently working on a large scale refactor which can be found on the [feat/refresh](https://github.com/documenso/documenso/tree/feat/refresh) branch.** +> 🦺 Documenso 1.0 is deployed to our Staging Environment. > -> **[Read more on why 👀](https://documenso.com/blog/why-were-doing-a-rewrite)** +> The code can be found on the [feat/refresh](https://github.com/documenso/documenso/tree/feat/refresh) branch. +> +> The new Version will be released after the current testing phase. -# Documenso 0.9 - Developer Preview +# Join us in testing Documenso 1.0 during [MALFUNCTION MANIA](https://documenso.com/blog/malfunction-mania)
+ src="https://github.com/documenso/documenso/assets/1309312/67e08c98-c153-4115-aa2d-77979bb12c94)"> + src="https://github.com/documenso/documenso/assets/1309312/040cfbae-3438-4ca3-87f2-ce52c793dcaf"> + src="https://github.com/documenso/documenso/assets/1309312/72d445be-41e5-4936-bdba-87ef8e70fa09"> + src="https://github.com/documenso/documenso/assets/1309312/d7b86c0f-a755-4476-a022-a608db2c4633"> - - - + src=https://github.com/documenso/documenso/assets/1309312/c0f55116-ab82-433f-a266-f3fc8571d69f">
-> **Note** -> This project is currently under community review and will publish it's first production release soon™. - ## About this project -Signing documents digitally is fast, easy and should be best practice for every document signed worldwide. This is technically quite easy today, but it also introduces a new party to every signature: The signing tool providers. While this is not a problem in itself, it should make us think about how we want these providers of trust to work. Documenso aims to be the world's most trusted document signing tool. This trust is built by empowering you to self-host Documenso and review how it works under the hood. Join us in creating the next generation of open trust infrastructure. +Signing documents digitally is fast and easy and should be the best practice for every document signed worldwide. This is technically quite easy today, but it also introduces a new party to every signature: The signing tool providers. While this is not a problem in itself, it should make us think about how we want these providers of trust to work. Documenso aims to be the world's most trusted document-signing tool. This trust is built by empowering you to self-host Documenso and review how it works under the hood. Join us in creating the next generation of open trust infrastructure. ## Recognition @@ -65,13 +58,13 @@ Signing documents digitally is fast, easy and should be best practice for every ## Community and Next Steps 🎯 -We're currently working on a redesign of the application including a revamp of the codebase so Documenso can be more intuitive to use and robust to develop upon. +We're currently working on a redesign of the application, including a revamp of the codebase so Documenso can be more intuitive to use and robust to develop upon. - Check out the first source code release in this repository and test it - Tell us what you think in the current [Discussions](https://github.com/documenso/documenso/discussions) - Join the [Discord server](https://documen.so/discord) for any questions and getting to know to other community members - ⭐ the repository to help us raise awareness -- Spread the word on Twitter, that Documenso is working towards a more open signing tool +- Spread the word on Twitter that Documenso is working towards a more open signing tool - Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release ## Contributing diff --git a/apps/marketing/content/blog/malfunction-mania.mdx b/apps/marketing/content/blog/malfunction-mania.mdx new file mode 100644 index 000000000..cb21e951a --- /dev/null +++ b/apps/marketing/content/blog/malfunction-mania.mdx @@ -0,0 +1,54 @@ +--- +title: Announcing Malfunction Mania +description: Launch Week Day 2 Y'all! We're getting ready to release Documenso 1.0! Join in on the fun of making sure the open-source alternative to DocuSign is on point. We're calling a 'MALFUNCTION MANIA.' +authorName: 'Timur Ercan' +authorImage: '/blog/blog-author-timur.jpeg' +authorRole: 'Co-Founder' +date: 2023-09-26 +tags: + - Testing + - Rewrite + - Bounties +--- + +
+ + +
+ We're calling a MALFUNCTION MANIA! 🚨 +
+
+ +> TLDR; Documenso 1.0 is in the [staging environment](https://documen.so/staging). Go check it out. + +It's been a minute since Lucas proclaimed, ["We're doing a rewrite"](https://documenso.com/blog/why-were-doing-a-rewrite), and many of you have been asking when the new version will be available. I'm happy to say that the wait has come to an end. The work on Documenso 1.0 has reached a level we feel comfortable going into the next phase with. We had a lot of community feedback, contributions, and moral support to get us this far, which is why we're excited to announce the most extensive community project yet: + +As Documenso 1.0 just hit the staging environment, we're calling a MALFUNCTION MANIA. An enormous, public testing phase, where we invite everyone to try out the new version, hunt down, report, and fix any malfunctions (aka bugs), and give feedback before release. Malfunction Mania will happen alongside our internal testing, and by combining the two, we want to ensure the best possible release we can have for Version 1.0. We know many of you have been eager to contribute; this is your chance (the first of many to come). + +## As part of Malfunction Mania, we're offering special bug bounties + +- We award $25 - $100 per report/ issue/ fix, depending on the severity and if the problem is already known +- Bounties will be awarded for fixing reported or other critical issues via accepted Pull Requests (PR) +- Just reporting issues in a reproducible way can also be awarded +- Smaller but notable contributions like minor issues and documentation will be awarded with exclusive merch as we see fit. + +## What you can do + +- Head over to the [staging environment](https://documen.so/staging), check out the new version and give it a spin; +- Check out the [source code](https://github.com/documenso/documenso) on GitHub and look it over; +- Spin up the new version locally and try it out. + +## How to get the bounties + +- Report bugs by creating an issue here: [documen.so/issues](https://documen.so/issues); +- Fix bugs by creating a Pull Request (PR); +- Look over and add missing documentation/ Quickstarts and other useful resources. + +We don't have a specific end date for Malfunction Mania. We plan to move the staging version into the production environment by the end of the month once we're happy with the results. Bug reports and fixes are, of course, always welcome going forward. + +**[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest about Malfunction Mania.** diff --git a/apps/marketing/content/blog/next.mdx b/apps/marketing/content/blog/next.mdx index c241cf3eb..2f83fe3ef 100644 --- a/apps/marketing/content/blog/next.mdx +++ b/apps/marketing/content/blog/next.mdx @@ -20,11 +20,11 @@ Today, I'm pleased to share with you a preview of the next Documenso. We redesigned the whole signing flow to make it more appealing and more convenient. -We improved the overall look and feel by making it more elegant and appropriately playful. Focused on the task at hand, but explicitly enjoying doing it. +We improved the overall look and feel by making it more elegant and appropriately playful. Focused on the task at hand, but explicitly enjoying doing it. **We call it happy minimalism.** -We paid particular attention to the moment of signing, which should be celebrated. +We paid particular attention to the moment of signing, which should be celebrated. The image below is the final bloom of the completion celebration we added: diff --git a/apps/marketing/content/blog/shop.mdx b/apps/marketing/content/blog/shop.mdx new file mode 100644 index 000000000..88f092ac3 --- /dev/null +++ b/apps/marketing/content/blog/shop.mdx @@ -0,0 +1,62 @@ +--- +title: Shop and Limited Edition "Mania" Shirt +description: Happy Launch Week Day 3. The limited edition "Malfunction Mania" shirt is here. Grab it, while you can. +authorName: 'Timur Ercan' +authorImage: '/blog/blog-author-timur.jpeg' +authorRole: 'Co-Founder' +date: 2023-09-27 +tags: + - Testing + - Rewrite + - Bounties +--- + +
+ + +
+ The Limited Edition "Malfunction Mania" Shirt - Only during Malfunction Mania +
+
+ +> TLDR; We have a fancy limited edition shirt. Contribute to Malfunction Mania to get one. + +We kicked off [Malfunction Mania](https://documenso.com/blog/malfunction-mania) yesterday, and the first [Issues](https://github.com/documenso/documenso/issues) are coming in. As mentioned, there will be dollar bounties, but we also wanted to celebrate entering the final stage of version 1.0 with something special. This is why we created the limited edition shirt for Malfunction Mania. It will only be available during the runtime of Malfunction Mania. We have yet to set an exact end date, the next event in October, however, is looming, ready to end MM. + +## Documenso Merch Shop + +The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors. + +
+ + +
+ Merch at Documenso is always given to those who deserve it. +
+
+ +## How to get the shirt + +If you have been following us, you know we are not big on formalities but highly value rewarding merit. That being said, any worthwhile contribution has a chance to get one. To inspire, here are a few ideas on how to contribute to securing one: + +- Report a bug with detailed reproduction details +- Fix a bug (you or somebody else reported) +- Analyze and describe a usability or user experience shortcoming +- Test the product in a systematic and least somewhat documented way +- Engage in discussion about the current version and its choices +- Raise awareness for Malfunction Mania and try out the [version currently in staging](https://documen.so/staging) +- Review the version with a video, stream, or screenshots and post about it +- Review existing or create missing documenso +- ... + +**[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest about Malfunction Mania.** diff --git a/apps/marketing/public/blog/mania-shirt.png b/apps/marketing/public/blog/mania-shirt.png new file mode 100644 index 000000000..eab20119e Binary files /dev/null and b/apps/marketing/public/blog/mania-shirt.png differ diff --git a/apps/marketing/public/blog/mm.png b/apps/marketing/public/blog/mm.png new file mode 100644 index 000000000..19477f0f7 Binary files /dev/null and b/apps/marketing/public/blog/mm.png differ diff --git a/apps/marketing/public/blog/shop.png b/apps/marketing/public/blog/shop.png new file mode 100644 index 000000000..432a5abe8 Binary files /dev/null and b/apps/marketing/public/blog/shop.png differ 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 70bf58926..0c85fc65c 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 @@ -73,7 +73,7 @@ export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerMod )} -

+

You have signed {document.title}

@@ -84,17 +84,17 @@ export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerMod signingCelebrationImage={signingCelebration} /> -
+
{/* TODO: Hook this up */} - null); if (!recipientOrSender) { - return null; + return NextResponse.json({ error: 'Not found' }, { status: 404 }); } const isRecipient = 'Signature' in recipientOrSender; diff --git a/apps/web/src/app/(share)/share/[slug]/page.tsx b/apps/web/src/app/(share)/share/[slug]/page.tsx index 63449f29f..51684d384 100644 --- a/apps/web/src/app/(share)/share/[slug]/page.tsx +++ b/apps/web/src/app/(share)/share/[slug]/page.tsx @@ -1,11 +1,39 @@ import { Metadata } from 'next'; +import { headers } from 'next/headers'; +import { redirect } from 'next/navigation'; -import { Redirect } from './redirect'; +import { APP_BASE_URL } from '@documenso/lib/constants/app'; -export const metadata: Metadata = { - title: 'Documenso - Share', +type SharePageProps = { + params: { slug: string }; }; -export default function SharePage() { - return ; +export function generateMetadata({ params: { slug } }: SharePageProps) { + return { + title: 'Documenso - Share', + description: 'I just signed a document with Documenso!', + openGraph: { + title: 'Documenso - Join the open source signing revolution', + description: 'I just signed with Documenso!', + type: 'website', + images: [`${APP_BASE_URL}/share/${slug}/opengraph`], + }, + twitter: { + site: '@documenso', + card: 'summary_large_image', + images: [`${APP_BASE_URL}/share/${slug}/opengraph`], + description: 'I just signed with Documenso!', + }, + } satisfies Metadata; +} + +export default function SharePage() { + const userAgent = headers().get('User-Agent') ?? ''; + + // https://stackoverflow.com/questions/47026171/how-to-detect-bots-for-open-graph-with-user-agent + if (/bot|facebookexternalhit|WhatsApp|google|bing|duckduckbot|MetaInspector/i.test(userAgent)) { + return null; + } + + redirect(process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001'); } diff --git a/apps/web/src/app/(share)/share/[slug]/redirect.tsx b/apps/web/src/app/(share)/share/[slug]/redirect.tsx deleted file mode 100644 index 5b3af0771..000000000 --- a/apps/web/src/app/(share)/share/[slug]/redirect.tsx +++ /dev/null @@ -1,11 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; - -export const Redirect = () => { - useEffect(() => { - window.location.href = process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001'; - }, []); - - return null; -}; diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx index a89f1bb3f..a8081069f 100644 --- a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx @@ -9,7 +9,7 @@ import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-f import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; import { DocumentStatus, FieldType } from '@documenso/prisma/client'; import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button'; -import { SigningCard } from '@documenso/ui/components/signing-card'; +import { SigningCard3D } from '@documenso/ui/components/signing-card'; import signingCelebration from '~/assets/signing-celebration.png'; @@ -53,11 +53,11 @@ export default async function CompletedSigningPage({ recipient.email; return ( -
+
{/* Card with recipient */} - + -
+
{match(document.status) .with(DocumentStatus.COMPLETED, () => (
@@ -71,41 +71,44 @@ export default async function CompletedSigningPage({ Waiting for others to sign
))} + +

+ You have signed "{document.title}" +

+ + {match(document.status) + .with(DocumentStatus.COMPLETED, () => ( +

+ Everyone has signed! You will receive an Email copy of the signed document. +

+ )) + .otherwise(() => ( +

+ You will receive an Email copy of the signed document once everyone has signed. +

+ ))} + +
+ + + +
+ +

+ Want to send slick signing links like this one?{' '} + + Check out Documenso. + +

- -

- You have signed "{document.title}" -

- - {match(document.status) - .with(DocumentStatus.COMPLETED, () => ( -

- Everyone has signed! You will receive an Email copy of the signed document. -

- )) - .otherwise(() => ( -

- You will receive an Email copy of the signed document once everyone has signed. -

- ))} - -
- - - -
- -

- Want to send slick signing links like this one?{' '} - - Check out Documenso. - -

); } diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/share-button.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/share-button.tsx index c13b8a7ab..f4476ade8 100644 --- a/apps/web/src/app/(signing)/sign/[token]/complete/share-button.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/complete/share-button.tsx @@ -1,11 +1,20 @@ 'use client'; -import { HTMLAttributes } from 'react'; +import { HTMLAttributes, useState } from 'react'; -import { Share } from 'lucide-react'; +import { Copy, Share, Twitter } from 'lucide-react'; +import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard'; @@ -19,14 +28,36 @@ export const ShareButton = ({ token, documentId }: ShareButtonProps) => { const { toast } = useToast(); const [, copyToClipboard] = useCopyToClipboard(); - const { mutateAsync: createOrGetShareLink, isLoading } = - trpc.shareLink.createOrGetShareLink.useMutation(); + const [isOpen, setIsOpen] = useState(false); - const onShareClick = async () => { - const { slug } = await createOrGetShareLink({ - token: token, - documentId, - }); + const { + mutateAsync: createOrGetShareLink, + data: shareLink, + isLoading, + } = trpc.shareLink.createOrGetShareLink.useMutation(); + + const onOpenChange = (nextOpen: boolean) => { + if (nextOpen) { + void createOrGetShareLink({ + token, + documentId, + }); + } + + setIsOpen(nextOpen); + }; + + const onCopyClick = async () => { + let { slug = '' } = shareLink || {}; + + if (!slug) { + const result = await createOrGetShareLink({ + token, + documentId, + }); + + slug = result.slug; + } await copyToClipboard(`${window.location.origin}/share/${slug}`).catch(() => null); @@ -34,18 +65,82 @@ export const ShareButton = ({ token, documentId }: ShareButtonProps) => { title: 'Copied to clipboard', description: 'The sharing link has been copied to your clipboard.', }); + + setIsOpen(false); + }; + + const onTweetClick = async () => { + let { slug = '' } = shareLink || {}; + + if (!slug) { + const result = await createOrGetShareLink({ + token, + documentId, + }); + + slug = result.slug; + } + + window.open( + generateTwitterIntent( + `I just ${token ? 'signed' : 'sent'} a document with @documenso. Check it out!`, + `${window.location.origin}/share/${slug}`, + ), + '_blank', + ); + + setIsOpen(false); }; return ( - + + + + + + + + Share + + Share your signing experience! + + +
+
+ I just {token ? 'signed' : 'sent'} a document with{' '} + @documenso + . Check it out! + + + {window.location.origin}/share/{shareLink?.slug || '...'} + +
+ + + +
+
+ Or +
+
+ + +
+ +
); }; diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index 2d61f096e..f5f1f3c3d 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -1,4 +1,4 @@ -import { notFound } from 'next/navigation'; +import { notFound, redirect } from 'next/navigation'; import { match } from 'ts-pattern'; @@ -9,7 +9,7 @@ import { viewedDocument } from '@documenso/lib/server-only/document/viewed-docum import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token'; import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; import { getFile } from '@documenso/lib/universal/upload/get-file'; -import { FieldType } from '@documenso/prisma/client'; +import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client'; import { Card, CardContent } from '@documenso/ui/primitives/card'; import { ElementVisible } from '@documenso/ui/primitives/element-visible'; import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; @@ -53,6 +53,13 @@ export default async function SigningPage({ params: { token } }: SigningPageProp const user = await getServerComponentSession(); + if ( + document.status === DocumentStatus.COMPLETED || + recipient.signingStatus === SigningStatus.SIGNED + ) { + redirect(`/sign/${token}/complete`); + } + return (
diff --git a/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx b/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx index 9ef2cd105..8d611c2d1 100644 --- a/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx +++ b/apps/web/src/components/(dashboard)/avatar/stack-avatars-with-tooltip.tsx @@ -40,7 +40,7 @@ export const StackAvatarsWithTooltip = ({ return ( - + {children || } diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx index 2a209ddbe..aed0a4d3d 100644 --- a/apps/web/src/components/forms/signin.tsx +++ b/apps/web/src/components/forms/signin.tsx @@ -1,8 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; - -import { useSearchParams } from 'next/navigation'; +import { useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { Eye, EyeOff, Loader } from 'lucide-react'; @@ -40,8 +38,6 @@ export type SignInFormProps = { }; export const SignInForm = ({ className }: SignInFormProps) => { - const searchParams = useSearchParams(); - const { toast } = useToast(); const [showPassword, setShowPassword] = useState(false); @@ -57,36 +53,29 @@ export const SignInForm = ({ className }: SignInFormProps) => { resolver: zodResolver(ZSignInFormSchema), }); - const errorCode = searchParams?.get('error'); - - useEffect(() => { - let timeout: NodeJS.Timeout | null = null; - - if (isErrorCode(errorCode)) { - timeout = setTimeout(() => { - toast({ - variant: 'destructive', - description: ERROR_MESSAGES[errorCode] ?? 'An unknown error occurred', - }); - }, 0); - } - - return () => { - if (timeout) { - clearTimeout(timeout); - } - }; - }, [errorCode, toast]); - const onFormSubmit = async ({ email, password }: TSignInFormSchema) => { try { - await signIn('credentials', { + const result = await signIn('credentials', { email, password, callbackUrl: LOGIN_REDIRECT_PATH, - }).catch((err) => { - console.error(err); + redirect: false, }); + + if (result?.error && isErrorCode(result.error)) { + toast({ + variant: 'destructive', + description: ERROR_MESSAGES[result.error], + }); + + return; + } + + if (!result?.url) { + throw new Error('An unknown error occurred'); + } + + window.location.href = result.url; } catch (err) { toast({ title: 'An unknown error occurred', diff --git a/packages/lib/universal/generate-twitter-intent.ts b/packages/lib/universal/generate-twitter-intent.ts new file mode 100644 index 000000000..29774fb32 --- /dev/null +++ b/packages/lib/universal/generate-twitter-intent.ts @@ -0,0 +1,5 @@ +export const generateTwitterIntent = (text: string, shareUrl: string) => { + return `https://twitter.com/intent/tweet?text=${encodeURIComponent( + text, + )}%0A%0A${encodeURIComponent(shareUrl)}`; +}; diff --git a/packages/ui/components/signing-card.tsx b/packages/ui/components/signing-card.tsx index 496e451d0..a2dd66bae 100644 --- a/packages/ui/components/signing-card.tsx +++ b/packages/ui/components/signing-card.tsx @@ -148,7 +148,7 @@ const SigningCardContent = ({ className, name }: SigningCardContentProps) => { return ( { return (