diff --git a/apps/web/src/app/(dashboard)/documents/data-table-action-button.tsx b/apps/web/src/app/(dashboard)/documents/data-table-action-button.tsx index 7c1d42d2b..4443981f8 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table-action-button.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table-action-button.tsx @@ -7,7 +7,11 @@ import { useSession } from 'next-auth/react'; import { match } from 'ts-pattern'; import { Document, DocumentStatus, Recipient, SigningStatus, User } from '@documenso/prisma/client'; +import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard'; export type DataTableActionButtonProps = { row: Document & { @@ -18,11 +22,16 @@ export type DataTableActionButtonProps = { export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => { const { data: session } = useSession(); + const { toast } = useToast(); + const [, copyToClipboard] = useCopyToClipboard(); if (!session) { return null; } + const { mutateAsync: createOrGetShareLink, isLoading: isCreatingShareLink } = + trpc.shareLink.createOrGetShareLink.useMutation(); + const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email); const isOwner = row.User.id === session.user.id; @@ -32,6 +41,20 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => { const isComplete = row.status === DocumentStatus.COMPLETED; const isSigned = recipient?.signingStatus === SigningStatus.SIGNED; + const onShareClick = async () => { + const { slug } = await createOrGetShareLink({ + token: recipient?.token, + documentId: row.id, + }); + + await copyToClipboard(`${window.location.origin}/share/${slug}`).catch(() => null); + + toast({ + title: 'Copied to clipboard', + description: 'The sharing link has been copied to your clipboard.', + }); + }; + return match({ isOwner, isRecipient, @@ -57,8 +80,8 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => { )) .otherwise(() => ( - )); diff --git a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx index dd028e0b2..e0eac175e 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx @@ -7,6 +7,7 @@ import { Download, Edit, History, + Loader, MoreHorizontal, Pencil, Share, @@ -18,7 +19,8 @@ import { useSession } from 'next-auth/react'; import { getFile } from '@documenso/lib/universal/upload/get-file'; import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client'; import { DocumentWithData } from '@documenso/prisma/types/document-with-data'; -import { trpc } from '@documenso/trpc/client'; +import { trpc as trpcClient } from '@documenso/trpc/client'; +import { trpc as trpcReact } from '@documenso/trpc/react'; import { DropdownMenu, DropdownMenuContent, @@ -26,6 +28,9 @@ import { DropdownMenuLabel, DropdownMenuTrigger, } from '@documenso/ui/primitives/dropdown-menu'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard'; export type DataTableActionDropdownProps = { row: Document & { @@ -36,11 +41,16 @@ export type DataTableActionDropdownProps = { export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => { const { data: session } = useSession(); + const { toast } = useToast(); + const [, copyToClipboard] = useCopyToClipboard(); if (!session) { return null; } + const { mutateAsync: createOrGetShareLink, isLoading: isCreatingShareLink } = + trpcReact.shareLink.createOrGetShareLink.useMutation(); + const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email); const isOwner = row.User.id === session.user.id; @@ -50,15 +60,29 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = const isComplete = row.status === DocumentStatus.COMPLETED; // const isSigned = recipient?.signingStatus === SigningStatus.SIGNED; + const onShareClick = async () => { + const { slug } = await createOrGetShareLink({ + token: recipient?.token, + documentId: row.id, + }); + + await copyToClipboard(`${window.location.origin}/share/${slug}`).catch(() => null); + + toast({ + title: 'Copied to clipboard', + description: 'The sharing link has been copied to your clipboard.', + }); + }; + const onDownloadClick = async () => { let document: DocumentWithData | null = null; if (!recipient) { - document = await trpc.document.getDocumentById.query({ + document = await trpcClient.document.getDocumentById.query({ id: row.id, }); } else { - document = await trpc.document.getDocumentByToken.query({ + document = await trpcClient.document.getDocumentByToken.query({ token: recipient.token, }); } @@ -135,8 +159,12 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = Resend - - + + {isCreatingShareLink ? ( + + ) : ( + + )} Share diff --git a/apps/web/src/app/(share)/share/[shareId]/opengraph-image.tsx b/apps/web/src/app/(share)/share/[slug]/opengraph-image.tsx similarity index 50% rename from apps/web/src/app/(share)/share/[shareId]/opengraph-image.tsx rename to apps/web/src/app/(share)/share/[slug]/opengraph-image.tsx index 35262a5ae..bcba22e3b 100644 --- a/apps/web/src/app/(share)/share/[shareId]/opengraph-image.tsx +++ b/apps/web/src/app/(share)/share/[slug]/opengraph-image.tsx @@ -1,5 +1,9 @@ import { ImageResponse } from 'next/server'; +import { P, match } from 'ts-pattern'; + +import { getRecipientOrSenderByShareLinkSlug } from '@documenso/lib/server-only/share/get-recipient-or-sender-by-share-link-slug'; + export const runtime = 'edge'; const CARD_OFFSET_TOP = 152; @@ -13,14 +17,31 @@ const size = { }; type SharePageOpenGraphImageProps = { - params: { shareId: string }; + params: { slug: string }; }; -export default async function Image({ params: { shareId } }: SharePageOpenGraphImageProps) { - // Cannot use trpc here and prisma does not work in the browser so I cannot fetch the client - // const { data } = trpc.share.get.useQuery({ shareId }); +export default async function Image({ params: { slug } }: SharePageOpenGraphImageProps) { + const recipientOrSender = await getRecipientOrSenderByShareLinkSlug({ slug }).catch(() => null); - const signature = shareId; + if (!recipientOrSender) { + return null; + } + + const signatureImage = match(recipientOrSender) + .with({ Signature: P.array(P._) }, (recipient) => { + return recipient.Signature?.[0]?.signatureImageAsBase64 || null; + }) + .otherwise((sender) => { + return sender.signature || null; + }); + + const signatureName = match(recipientOrSender) + .with({ Signature: P.array(P._) }, (recipient) => { + return recipient.name || recipient.email; + }) + .otherwise((sender) => { + return sender.name || sender.email; + }); const [interSemiBold, interRegular, caveatRegular, shareFrameImage] = await Promise.all([ fetch(new URL('./../../../../assets/inter-semibold.ttf', import.meta.url)).then(async (res) => @@ -43,19 +64,36 @@ export default async function Image({ params: { shareId } }: SharePageOpenGraphI {/* @ts-expect-error Lack of typing from ImageResponse */} og-share-frame -

- {signature} -

+ {signatureImage ? ( +
+ signature +
+ ) : ( +

+ {signatureName} +

+ )}
null); if (!share) { return notFound(); diff --git a/apps/web/src/app/(share)/share/[shareId]/redirect.tsx b/apps/web/src/app/(share)/share/[slug]/redirect.tsx similarity index 80% rename from apps/web/src/app/(share)/share/[shareId]/redirect.tsx rename to apps/web/src/app/(share)/share/[slug]/redirect.tsx index f64095608..f8a29c561 100644 --- a/apps/web/src/app/(share)/share/[shareId]/redirect.tsx +++ b/apps/web/src/app/(share)/share/[slug]/redirect.tsx @@ -5,15 +5,15 @@ import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; export default function Redirect() { - const router = useRouter(); + const { push } = useRouter(); useEffect(() => { const timer = setTimeout(() => { - router.push('/'); + push('/'); }, 3000); return () => clearTimeout(timer); - }, []); + }, [push]); return
; } 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 9b3b80851..af9b2ab06 100644 --- a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx @@ -88,7 +88,7 @@ export default async function CompletedSigningPage({ ))}
- + & { - recipientId: number; + token: string; documentId: number; }; -export const ShareButton = ({ recipientId, documentId }: ShareButtonProps) => { - const { mutateAsync: createShareId, isLoading } = trpc.share.create.useMutation(); +export const ShareButton = ({ token, documentId }: ShareButtonProps) => { + const { mutateAsync: createOrGetShareLink, isLoading } = + trpc.shareLink.createOrGetShareLink.useMutation(); const router = useRouter(); @@ -23,19 +24,17 @@ export const ShareButton = ({ recipientId, documentId }: ShareButtonProps) => {