import { PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js' import type { PaymentMethod } from '@stripe/stripe-js' import { useQueryClient } from '@tanstack/react-query' import { Edit2, ExternalLink, HelpCircle } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { parseAsString, useQueryStates } from 'nuqs' import { useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' import { z } from 'zod' import SpendCapModal from 'components/interfaces/Billing/SpendCapModal' import Panel from 'components/ui/Panel' import { useOrganizationCreateMutation } from 'data/organizations/organization-create-mutation' import { invalidateOrganizationsQuery, useOrganizationsQuery, } from 'data/organizations/organizations-query' import { BASE_PATH, PRICING_TIER_LABELS_ORG } from 'lib/constants' import { getURL } from 'lib/helpers' import { useProfile } from 'lib/profile' import { Button, Input, Input_Shadcn_, Label_Shadcn_, Select_Shadcn_, SelectContent_Shadcn_, SelectItem_Shadcn_, SelectTrigger_Shadcn_, SelectValue_Shadcn_, Switch, Tooltip, TooltipContent, TooltipTrigger, } from 'ui' import { useProjectsQuery } from 'data/projects/projects-query' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import _ from 'lodash' const ORG_KIND_TYPES = { PERSONAL: 'Personal', EDUCATIONAL: 'Educational', STARTUP: 'Startup', AGENCY: 'Agency', COMPANY: 'Company', UNDISCLOSED: 'N/A', } const ORG_KIND_DEFAULT = 'PERSONAL' const ORG_SIZE_TYPES = { '1': '1 - 10', '10': '10 - 49', '50': '50 - 99', '100': '100 - 299', '300': 'More than 300', } const ORG_SIZE_DEFAULT = '1' interface NewOrgFormProps { onPaymentMethodReset: () => void } const formSchema = z.object({ plan: z .string() .transform((val) => val.toUpperCase()) .pipe(z.enum(['FREE', 'PRO', 'TEAM', 'ENTERPRISE'] as const)), name: z.string().min(1), kind: z .string() .transform((val) => val.toUpperCase()) .pipe( z.enum(['PERSONAL', 'EDUCATIONAL', 'STARTUP', 'AGENCY', 'COMPANY', 'UNDISCLOSED'] as const) ), size: z.enum(['1', '10', '50', '100', '300'] as const), spend_cap: z.boolean(), }) type FormState = z.infer /** * No org selected yet, create a new one * [Joshen] Need to refactor to use Form_Shadcn here */ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { const router = useRouter() const user = useProfile() const { data: organizations, isSuccess } = useOrganizationsQuery() const { data: projects } = useProjectsQuery() const stripe = useStripe() const elements = useElements() const queryClient = useQueryClient() const freeOrgs = (organizations || []).filter((it) => it.plan.id === 'free') const projectsByOrg = useMemo(() => { return _.groupBy(projects || [], 'organization_slug') }, [projects]) const [isOrgCreationConfirmationModalVisible, setIsOrgCreationConfirmationModalVisible] = useState(false) const [formState, setFormState] = useState({ plan: 'FREE', name: '', kind: ORG_KIND_DEFAULT, size: ORG_SIZE_DEFAULT, spend_cap: true, }) const [searchParams] = useQueryStates({ returnTo: parseAsString.withDefault(''), auth_id: parseAsString.withDefault(''), }) const updateForm = (key: keyof FormState, value: unknown) => { setFormState((prev) => ({ ...prev, [key]: value })) } useEffect(() => { if (!router.isReady) return const { name, kind, plan, size, spend_cap } = router.query if (typeof name === 'string') updateForm('name', name) if (typeof kind === 'string') updateForm('kind', kind) if (typeof plan === 'string') updateForm('plan', plan) if (typeof size === 'string') updateForm('size', size) if (typeof spend_cap === 'string') updateForm('spend_cap', spend_cap === 'true') }, [router.isReady]) useEffect(() => { if (!formState.name && organizations?.length === 0 && !user.isLoading) { const prefilledOrgName = user.profile?.username ? user.profile.username + `'s Org` : 'My Org' updateForm('name', prefilledOrgName) } }, [isSuccess]) const [newOrgLoading, setNewOrgLoading] = useState(false) const [paymentMethod, setPaymentMethod] = useState() const [showSpendCapHelperModal, setShowSpendCapHelperModal] = useState(false) const { mutate: createOrganization } = useOrganizationCreateMutation({ onSuccess: async (org) => { await invalidateOrganizationsQuery(queryClient) const prefilledProjectName = user.profile?.username ? user.profile.username + `'s Project` : 'My Project' if (searchParams.returnTo && searchParams.auth_id) { router.push(`${searchParams.returnTo}?auth_id=${searchParams.auth_id}`, undefined, { shallow: false, }) } else { router.push(`/new/${org.slug}?projectName=${prefilledProjectName}`) } }, onError: () => { resetPaymentMethod() setNewOrgLoading(false) }, }) function validateOrgName(name: any) { const value = name ? name.trim() : '' return value.length >= 1 } async function createOrg(paymentMethodId?: string) { const dbTier = formState.plan === 'PRO' && !formState.spend_cap ? 'PAYG' : formState.plan createOrganization({ name: formState.name, kind: formState.kind, tier: ('tier_' + dbTier.toLowerCase()) as | 'tier_payg' | 'tier_pro' | 'tier_free' | 'tier_team' | 'tier_enterprise', ...(formState.kind == 'COMPANY' ? { size: formState.size } : {}), payment_method: paymentMethodId, }) } const handleSubmit = async () => { if (!stripe || !elements) { return console.error('Stripe.js has not loaded') } setNewOrgLoading(true) if (formState.plan === 'FREE') { await createOrg() } else if (!paymentMethod) { const { error, setupIntent } = await stripe.confirmSetup({ elements, redirect: 'if_required', confirmParams: { return_url: `${getURL()}/new`, expand: ['payment_method'], }, }) if (error || !setupIntent.payment_method) { toast.error(error?.message ?? ' Failed to save card details') setNewOrgLoading(false) return } const paymentMethodFromSetup = setupIntent.payment_method as PaymentMethod setPaymentMethod(paymentMethodFromSetup) createOrg(paymentMethodFromSetup.id) } else { createOrg(paymentMethod.id) } } const resetPaymentMethod = () => { setPaymentMethod(undefined) return onPaymentMethodReset() } const onSubmitWithOrgCreation = async (event: any) => { event.preventDefault() const isOrgNameValid = validateOrgName(formState.name) if (!isOrgNameValid) { return toast.error('Organization name is empty') } const hasFreeOrgWithProjects = freeOrgs.some((it) => projectsByOrg[it.slug]?.length > 0) if (hasFreeOrgWithProjects && formState.plan !== 'FREE') { setIsOrgCreationConfirmationModalVisible(true) } else { await handleSubmit() } } return (

Create a new organization

} footer={

You can rename your organization later

} >

This is your organization within Supabase.

For example, you can use the name of your company or department.

Name
updateForm('name', e.target.value)} />
What's the name of your company or team?
Type
updateForm('kind', value)} > {Object.entries(ORG_KIND_TYPES).map(([k, v]) => ( {v} ))}
What would best describe your organization?
{formState.kind == 'COMPANY' && (
Company size
updateForm('size', value)} > {Object.entries(ORG_SIZE_TYPES).map(([k, v]) => ( {v} ))}
How many people are in your company?
)}
updateForm('plan', value)} > {Object.entries(PRICING_TIER_LABELS_ORG).map(([k, v]) => ( {v} ))}
The Plan applies to your new organization.
{formState.plan === 'PRO' && ( <>
Spend Cap setShowSpendCapHelperModal(true)} />
updateForm('spend_cap', !formState.spend_cap)} /> {formState.spend_cap ? `Usage is limited to the plan's quota.` : `You pay for overages beyond the plan's quota.`}
setShowSpendCapHelperModal(false)} /> )} {formState.plan !== 'FREE' && ( {paymentMethod?.card !== undefined ? (
Card

Expires: {paymentMethod.card.exp_month}/{paymentMethod.card.exp_year}

) : ( )}
)}
Confirm organization creation} confirmLabel="Create new organization" onCancel={() => setIsOrgCreationConfirmationModalVisible(false)} onConfirm={async () => { await handleSubmit() setIsOrgCreationConfirmationModalVisible(false) }} variant={'warning'} >

Supabase{' '} bills per organization . If you want to upgrade your existing projects, upgrade your existing organization instead.

    {freeOrgs .filter((it) => projectsByOrg[it.slug]?.length > 0) .map((org) => { const orgProjects = projectsByOrg[org.slug].map((it) => it.name) return (
  • {org.name}
    {orgProjects.length <= 2 ? ( {orgProjects.join('and ')} ) : (
    {orgProjects.slice(0, 2).join(', ')} and{' '} {orgProjects.length - 2} other{' '} {orgProjects.length === 3 ? 'project' : 'project'}
      {orgProjects.slice(2).map((project) => (
    • {project}
    • ))}
    )}
  • ) })}
) } export default NewOrgForm