import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session'; import { useOptionalSession } from '@documenso/lib/client-only/providers/session'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { prisma } from '@documenso/prisma'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { Trans, useLingui } from '@lingui/react/macro'; import { OrganisationMemberInviteStatus } from '@prisma/client'; import { useState } from 'react'; import { Link, useSearchParams } from 'react-router'; import { match } from 'ts-pattern'; import type { Route } from './+types/organisation.invite.$token'; export async function loader({ params, request }: Route.LoaderArgs) { const session = await getOptionalSession(request); const { token } = params; if (!token) { return { state: 'InvalidLink', } as const; } const organisationMemberInvite = await prisma.organisationMemberInvite.findUnique({ where: { token, }, include: { organisation: { select: { name: true, }, }, }, }); if (!organisationMemberInvite) { return { state: 'InvalidLink', } as const; } const organisationName = organisationMemberInvite.organisation.name; if (organisationMemberInvite.status === OrganisationMemberInviteStatus.ACCEPTED) { return { state: 'AlreadyAccepted', organisationName, } as const; } if (organisationMemberInvite.status === OrganisationMemberInviteStatus.DECLINED) { return { state: 'AlreadyDeclined', organisationName, } as const; } const user = await prisma.user.findFirst({ where: { email: { equals: organisationMemberInvite.email, mode: 'insensitive', }, }, select: { id: true, }, }); return { state: 'Pending', token: organisationMemberInvite.token, email: organisationMemberInvite.email, organisationName, userExists: user !== null, isSessionUserTheInvitedUser: user !== null && user.id === session.user?.id, } as const; } export default function AcceptInvitationPage({ loaderData }: Route.ComponentProps) { const data = loaderData; if (data.state === 'InvalidLink') { return (

Invalid token

This token is invalid or has expired. Please contact your team for a new invitation.

); } if (data.state === 'AlreadyAccepted') { return (

Invitation already accepted

You are already a member of {data.organisationName}.

); } if (data.state === 'AlreadyDeclined') { return ; } return ( ); } type PendingInvitationProps = { token: string; email: string; organisationName: string; userExists: boolean; isSessionUserTheInvitedUser: boolean; }; type InvitationResult = 'idle' | 'accepted' | 'declined'; type AcceptFailureReason = 'CapExceeded' | 'SubscriptionInactive' | 'Unknown'; const PendingInvitation = ({ token, email, organisationName, userExists, isSessionUserTheInvitedUser, }: PendingInvitationProps) => { const { t } = useLingui(); const { toast } = useToast(); const { refreshSession } = useOptionalSession(); const [searchParams] = useSearchParams(); const actionIsDecline = searchParams.get('action') === 'decline'; const [result, setResult] = useState('idle'); const [acceptFailureReason, setAcceptFailureReason] = useState(null); const acceptInvitation = trpc.organisation.member.invite.accept.useMutation({ onSuccess: async () => { await refreshSession(); setResult('accepted'); }, onError: (err) => { const error = AppError.parseError(err); const failureReason = match(error.code) .with(AppErrorCode.LIMIT_EXCEEDED, () => 'CapExceeded' as const) .with('SUBSCRIPTION_INACTIVE', () => 'SubscriptionInactive' as const) .otherwise(() => 'Unknown' as const); setAcceptFailureReason(failureReason); }, }); const declineInvitation = trpc.organisation.member.invite.decline.useMutation({ onSuccess: async () => { await refreshSession(); setResult('declined'); }, onError: () => { toast({ title: t`Something went wrong`, description: t`Unable to decline this invitation at this time.`, variant: 'destructive', duration: 10000, }); }, }); if (result === 'accepted') { return (

Invitation accepted!

You have accepted an invitation from {organisationName} to join their organisation.

{isSessionUserTheInvitedUser ? ( ) : ( )}
); } if (result === 'declined') { return ; } // Accepting requires an account (acceptance keys off the invited email). // Declining does not, so we only gate account creation on the accept flow. if (!actionIsDecline && !userExists) { return (

Organisation invitation

You have been invited by {organisationName} to join their organisation.

To accept this invitation you must create an account.

); } const isPending = acceptInvitation.isPending || declineInvitation.isPending; return (

Organisation invitation

You have been invited to join {organisationName} on Documenso.

{acceptFailureReason && (

{match(acceptFailureReason) .with('CapExceeded', () => ( {organisationName} has reached its member limit. Please contact the organisation administrator to upgrade their plan before accepting this invitation. )) .with('SubscriptionInactive', () => ( {organisationName} does not have an active subscription. Please contact the organisation administrator to renew their plan before accepting this invitation. )) .with('Unknown', () => ( We were unable to add you to {organisationName} at this time. Please try again later, or contact the organisation administrator. )) .exhaustive()}

)}
{!actionIsDecline && ( )}
); }; const InvitationDeclined = ({ organisationName }: { organisationName: string }) => { return (

Invitation declined

You have declined the invitation from {organisationName} to join their organisation.

); };