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 { useEffect, useState } from 'react' import { toast } from 'sonner' 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 { parseAsBoolean, parseAsString, parseAsStringLiteral, useQueryStates } from 'nuqs' import { Button, Input, Listbox, Toggle } from 'ui' 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 } /** * No org selected yet, create a new one */ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { const router = useRouter() const user = useProfile() const { data: organizations, isSuccess } = useOrganizationsQuery() const stripe = useStripe() const elements = useElements() const queryClient = useQueryClient() const [state, setState] = useQueryStates({ plan: parseAsStringLiteral(Object.keys(PRICING_TIER_LABELS_ORG)).withDefault('FREE'), name: parseAsString.withDefault(''), kind: parseAsStringLiteral(Object.keys(ORG_KIND_TYPES)).withDefault(ORG_KIND_DEFAULT), size: parseAsString.withDefault(ORG_SIZE_DEFAULT), spend_cap: parseAsBoolean.withDefault(true), returnTo: parseAsString.withDefault(''), auth_id: parseAsString.withDefault(''), }) const { name: orgName, kind: orgKind, spend_cap: isSpendCapEnabled, plan: dbPricingTierKey, size: orgSize, returnTo, auth_id, } = state // [Joshen] Separate loading state here as there's 2 async processes const [newOrgLoading, setNewOrgLoading] = useState(false) const [paymentMethod, setPaymentMethod] = useState() const [showSpendCapHelperModal, setShowSpendCapHelperModal] = useState(false) useEffect(() => { if (!orgName && organizations?.length === 0 && !user.isLoading) { const prefilledOrgName = user.profile?.username ? user.profile.username + `'s Org` : 'My Org' setState({ ...state, name: prefilledOrgName }) } }, [isSuccess]) const { mutate: createOrganization } = useOrganizationCreateMutation({ onSuccess: async (org) => { await invalidateOrganizationsQuery(queryClient) const prefilledProjectName = user.profile?.username ? user.profile.username + `'s Project` : 'My Project' if (returnTo && auth_id) { router.push(`${returnTo}?auth_id=${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 } function onOrgNameChange(e: any) { setState({ ...state, name: e.target.value }) } function onOrgKindChange(value: any) { setState({ ...state, kind: value }) } function onOrgSizeChange(value: any) { setState({ ...state, size: value }) } function onDbPricingPlanChange(value: string) { setState({ ...state, plan: value as any }) } async function createOrg(paymentMethodId?: string) { const dbTier = dbPricingTierKey === 'PRO' && !isSpendCapEnabled ? 'PAYG' : dbPricingTierKey createOrganization({ name: orgName, kind: orgKind, tier: ('tier_' + dbTier.toLowerCase()) as | 'tier_payg' | 'tier_pro' | 'tier_free' | 'tier_team' | 'tier_enterprise', ...(orgKind == 'COMPANY' ? { size: orgSize } : {}), payment_method: paymentMethodId, }) } const handleSubmit = async (event: any) => { event.preventDefault() const isOrgNameValid = validateOrgName(orgName) if (!isOrgNameValid) { return toast.error('Organization name is empty') } if (!stripe || !elements) { return console.error('Stripe.js has not loaded') } setNewOrgLoading(true) if (dbPricingTierKey === '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() } 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.

{Object.entries(ORG_KIND_TYPES).map(([k, v]) => { return ( {v} ) })} {orgKind == 'COMPANY' ? ( {Object.entries(ORG_SIZE_TYPES).map(([k, v]) => { return ( {v} ) })} ) : ( <> )} Plan Pricing } layout="horizontal" value={dbPricingTierKey} // @ts-ignore onChange={onDbPricingPlanChange} // @ts-ignore descriptionText={ dbPricingTierKey !== 'FREE' ? (

The plan applies only to this new organization. To upgrade an existing organization,{' '} click here .

) : undefined } > {Object.entries(PRICING_TIER_LABELS_ORG).map(([k, v]) => { return ( {v} ) })}
{dbPricingTierKey === 'PRO' && ( <> Spend Cap setShowSpendCapHelperModal(true)} /> } checked={isSpendCapEnabled} onChange={() => setState({ ...state, spend_cap: !isSpendCapEnabled })} descriptionText={

With Spend Cap enabled, usage is limited to the plan's quota, with restrictions when limits are exceeded. To scale beyond Pro Plan limits, disable the Spend Cap to pay over-usage.

} />
setShowSpendCapHelperModal(false)} /> )} {dbPricingTierKey !== 'FREE' && ( {paymentMethod?.card !== undefined ? (
Card

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

) : ( )}
)}
) } export default NewOrgForm