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/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]/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/packages/prisma/migrations/20230605122017_password_reset/migration.sql b/packages/prisma/migrations/20230605122017_password_reset/migration.sql new file mode 100644 index 000000000..782a60880 --- /dev/null +++ b/packages/prisma/migrations/20230605122017_password_reset/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "PasswordResetToken" ( + "id" SERIAL NOT NULL, + "token" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" INTEGER NOT NULL, + + CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "PasswordResetToken_token_key" ON "PasswordResetToken"("token"); + +-- AddForeignKey +ALTER TABLE "PasswordResetToken" ADD CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20230605164015_expire_password_reset_token/migration.sql b/packages/prisma/migrations/20230605164015_expire_password_reset_token/migration.sql new file mode 100644 index 000000000..a3a70e575 --- /dev/null +++ b/packages/prisma/migrations/20230605164015_expire_password_reset_token/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `expiry` to the `PasswordResetToken` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "PasswordResetToken" ADD COLUMN "expiry" TIMESTAMP(3) NOT NULL; diff --git a/packages/prisma/migrations/20230917190854_password_reset_token/migration.sql b/packages/prisma/migrations/20230917190854_password_reset_token/migration.sql index d22107691..2d127d917 100644 --- a/packages/prisma/migrations/20230917190854_password_reset_token/migration.sql +++ b/packages/prisma/migrations/20230917190854_password_reset_token/migration.sql @@ -1,3 +1,5 @@ +DROP TABLE IF EXISTS "PasswordResetToken" CASCADE; + -- CreateTable CREATE TABLE "PasswordResetToken" ( "id" SERIAL NOT NULL, 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 (