import { useEffect, useMemo, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import type { MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro'; import type * as DialogPrimitive from '@radix-ui/react-dialog'; import { ExternalLinkIcon } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { Link, useSearchParams } from 'react-router'; import { match } from 'ts-pattern'; import type { z } from 'zod'; import type { InternalClaimPlans } from '@documenso/ee/server-only/stripe/get-internal-claim-plans'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; import { useSession } from '@documenso/lib/client-only/providers/session'; import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; import { AppError } from '@documenso/lib/errors/app-error'; import { INTERNAL_CLAIM_ID } from '@documenso/lib/types/subscription'; import { parseMessageDescriptorMacro } from '@documenso/lib/utils/i18n'; import { trpc } from '@documenso/trpc/react'; import { ZCreateOrganisationRequestSchema } from '@documenso/trpc/server/organisation-router/create-organisation.types'; import { cn } from '@documenso/ui/lib/utils'; import { Badge } from '@documenso/ui/primitives/badge'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, 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 { SpinnerBox } from '@documenso/ui/primitives/spinner'; import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs'; import { useToast } from '@documenso/ui/primitives/use-toast'; export type OrganisationCreateDialogProps = { trigger?: React.ReactNode; } & Omit; export const ZCreateOrganisationFormSchema = ZCreateOrganisationRequestSchema.pick({ name: true, }); export type TCreateOrganisationFormSchema = z.infer; export const OrganisationCreateDialog = ({ trigger, ...props }: OrganisationCreateDialogProps) => { const { t } = useLingui(); const { toast } = useToast(); const { refreshSession } = useSession(); const [searchParams] = useSearchParams(); const updateSearchParams = useUpdateSearchParams(); const actionSearchParam = searchParams?.get('action'); const [step, setStep] = useState<'billing' | 'create'>( IS_BILLING_ENABLED() ? 'billing' : 'create', ); const [selectedPriceId, setSelectedPriceId] = useState(''); const [open, setOpen] = useState(false); const form = useForm({ resolver: zodResolver(ZCreateOrganisationFormSchema), defaultValues: { name: '', }, }); const { mutateAsync: createOrganisation } = trpc.organisation.create.useMutation(); const { data: plansData } = trpc.billing.plans.get.useQuery(); const onFormSubmit = async ({ name }: TCreateOrganisationFormSchema) => { try { const response = await createOrganisation({ name, priceId: selectedPriceId, }); if (response.paymentRequired) { window.open(response.checkoutUrl, '_blank'); setOpen(false); return; } await refreshSession(); setOpen(false); toast({ title: t`Success`, description: t`Your organisation has been created.`, duration: 5000, }); } catch (err) { const error = AppError.parseError(err); console.error(error); toast({ title: t`An unknown error occurred`, description: t`We encountered an unknown error while attempting to create a organisation. Please try again later.`, variant: 'destructive', }); } }; useEffect(() => { if (actionSearchParam === 'add-organisation') { setOpen(true); updateSearchParams({ action: null }); } }, [actionSearchParam, open]); useEffect(() => { form.reset(); }, [open, form]); return ( !form.formState.isSubmitting && setOpen(value)} > e.stopPropagation()} asChild={true}> {trigger ?? ( )} {match(step) .with('billing', () => ( <> Select a plan Select a plan to continue
{plansData ? ( ) : ( )}
)) .with('create', () => ( <> Create organisation Create an organisation to collaborate with teams
( Organisation Name )} /> {IS_BILLING_ENABLED() ? ( ) : ( )}
)) .exhaustive()}
); }; // This is separated from the internal claims constant because we need to use the msg // macro which would cause import issues. const internalClaimsDescription: { [key in INTERNAL_CLAIM_ID]: MessageDescriptor | string; } = { [INTERNAL_CLAIM_ID.FREE]: msg`5 Documents a month`, [INTERNAL_CLAIM_ID.INDIVIDUAL]: msg`Unlimited documents, API and more`, [INTERNAL_CLAIM_ID.TEAM]: msg`Embedding, 5 members included and more`, [INTERNAL_CLAIM_ID.PLATFORM]: msg`Whitelabeling, unlimited members and more`, [INTERNAL_CLAIM_ID.ENTERPRISE]: '', [INTERNAL_CLAIM_ID.EARLY_ADOPTER]: '', }; type BillingPlanFormProps = { value: string; onChange: (priceId: string) => void; plans: InternalClaimPlans; canCreateFreeOrganisation: boolean; }; const BillingPlanForm = ({ value, onChange, plans, canCreateFreeOrganisation, }: BillingPlanFormProps) => { const { t } = useLingui(); const [billingPeriod, setBillingPeriod] = useState<'monthlyPrice' | 'yearlyPrice'>('yearlyPrice'); const dynamicPlans = useMemo(() => { return [INTERNAL_CLAIM_ID.INDIVIDUAL, INTERNAL_CLAIM_ID.TEAM, INTERNAL_CLAIM_ID.PLATFORM].map( (planId) => { const plan = plans[planId]; return { id: planId, name: plan.name, description: parseMessageDescriptorMacro(t, internalClaimsDescription[planId]), monthlyPrice: plan.monthlyPrice, yearlyPrice: plan.yearlyPrice, }; }, ); }, [plans]); useEffect(() => { if (value === '' && !canCreateFreeOrganisation) { onChange(dynamicPlans[0][billingPeriod]?.id ?? ''); } }, [value]); const onBillingPeriodChange = (billingPeriod: 'monthlyPrice' | 'yearlyPrice') => { const plan = dynamicPlans.find((plan) => plan[billingPeriod]?.id === value); setBillingPeriod(billingPeriod); onChange(plan?.[billingPeriod]?.id ?? Object.keys(plans)[0]); }; return (
onBillingPeriodChange(value as 'monthlyPrice' | 'yearlyPrice')} > Monthly Yearly
{dynamicPlans.map((plan) => ( ))}

Enterprise

Contact sales here

Compare all plans and features in detail
); };