import { useMemo, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { useLingui } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro'; import { AnimatePresence, motion } from 'framer-motion'; import { Building2Icon, PlusIcon } from 'lucide-react'; import { useForm } from 'react-hook-form'; import type { InternalClaimPlans } from '@documenso/ee/server-only/stripe/get-internal-claim-plans'; import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { useSession } from '@documenso/lib/client-only/providers/session'; import { INTERNAL_CLAIM_ID } from '@documenso/lib/types/subscription'; import { isPersonalLayout } from '@documenso/lib/utils/organisations'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card'; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@documenso/ui/primitives/dialog'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group'; import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { ZCreateOrganisationFormSchema } from '../dialogs/organisation-create-dialog'; const MotionCard = motion(Card); export type BillingPlansProps = { plans: InternalClaimPlans; }; export const BillingPlans = ({ plans }: BillingPlansProps) => { const isMounted = useIsMounted(); const { organisations } = useSession(); const [interval, setInterval] = useState<'monthlyPrice' | 'yearlyPrice'>('yearlyPrice'); const isPersonalLayoutMode = isPersonalLayout(organisations); const pricesToDisplay = useMemo(() => { const prices = []; for (const plan of Object.values(plans)) { if (plan[interval] && plan[interval].isVisibleInApp) { prices.push({ ...plan[interval], memberCount: plan.memberCount, claim: plan.id, }); } } return prices; }, [plans, interval]); return (
setInterval(value as 'monthlyPrice' | 'yearlyPrice')} > Monthly Yearly
{pricesToDisplay.map((price) => ( {price.product.name}
{price.friendlyPrice + ' '} {interval === 'monthlyPrice' ? ( per month ) : ( per year )}
{price.product.description}
{price.product.features && price.product.features.length > 0 && (
Includes:
    {price.product.features.map((feature, index) => (
  • {feature.name}
  • ))}
)}
{isPersonalLayoutMode && price.claim === INTERNAL_CLAIM_ID.INDIVIDUAL ? ( Subscribe ) : ( )} ))}
); }; const BillingDialog = ({ priceId, planName, claim, }: { priceId: string; planName: string; memberCount: number; claim: string; }) => { const [isOpen, setIsOpen] = useState(false); const { t } = useLingui(); const { toast } = useToast(); const organisation = useCurrentOrganisation(); const [subscriptionOption, setSubscriptionOption] = useState<'update' | 'create'>( organisation.type === 'PERSONAL' && claim === INTERNAL_CLAIM_ID.INDIVIDUAL ? 'update' : 'create', ); const [step, setStep] = useState(0); const form = useForm({ resolver: zodResolver(ZCreateOrganisationFormSchema), defaultValues: { name: '', }, }); const { mutateAsync: createSubscription, isPending: isCreatingSubscription } = trpc.enterprise.billing.subscription.create.useMutation(); const { mutateAsync: createOrganisation, isPending: isCreatingOrganisation } = trpc.organisation.create.useMutation(); const isPending = isCreatingSubscription || isCreatingOrganisation; const onSubscribeClick = async () => { try { let redirectUrl = ''; if (subscriptionOption === 'update') { const createSubscriptionResponse = await createSubscription({ organisationId: organisation.id, priceId, }); redirectUrl = createSubscriptionResponse.redirectUrl; } else { const createOrganisationResponse = await createOrganisation({ name: form.getValues('name'), priceId, }); if (!createOrganisationResponse.paymentRequired) { setIsOpen(false); return; } redirectUrl = createOrganisationResponse.checkoutUrl; } window.location.href = redirectUrl; } catch (_err) { toast({ title: t`Something went wrong`, description: t`An error occurred while trying to create a checkout session.`, variant: 'destructive', }); } }; return ( !isPending && setIsOpen(value)}> Subscribe You are about to subscribe to the {planName} {step === 0 ? (
setSubscriptionOption(value as 'update' | 'create')} >

Upgrade {organisation.name} to {planName}

Create a new organisation with {planName} plan. Keep your current organisation on it's current plan

) : (
( Organisation Name )} /> )} {subscriptionOption === 'create' && step === 0 ? ( ) : ( )}
); }; /** * Custom checkout button for individual organisations in personal layout mode. * * This is so they don't create an additional organisation which is not needed since * it will clutter up the UI for them with unnecessary organisations. */ export const IndividualPersonalLayoutCheckoutButton = ({ priceId, children, }: { priceId: string; children: React.ReactNode; }) => { const { t } = useLingui(); const { toast } = useToast(); const { organisations } = useSession(); const { mutateAsync: createSubscription, isPending } = trpc.enterprise.billing.subscription.create.useMutation(); const onSubscribeClick = async () => { try { const createSubscriptionResponse = await createSubscription({ organisationId: organisations[0].id, priceId, isPersonalLayoutMode: true, }); window.location.href = createSubscriptionResponse.redirectUrl; } catch (_err) { toast({ title: t`Something went wrong`, description: t`An error occurred while trying to create a checkout session.`, variant: 'destructive', }); } }; return ( ); };