import { useQuery } from '@apollo/client/react'; import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; import { isDefined } from 'twenty-shared/utils'; import { Tag } from 'twenty-ui/data-display'; import { IconBox, IconCalendarEvent, IconCalendarRepeat, IconChartBar, IconCircleX, IconCoins, IconCreditCard, IconExternalLink, IconId, IconStatusChange, IconTag, IconUsers, } from 'twenty-ui/icon'; import { H2Title } from 'twenty-ui/typography'; import { Section } from 'twenty-ui/layout'; import { type ThemeColor } from 'twenty-ui/theme'; import { themeCssVariables } from 'twenty-ui/theme-constants'; import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient'; import { GET_WORKSPACE_BILLING_ADMIN_PANEL } from '@/settings/admin-panel/graphql/queries/getWorkspaceBillingAdminPanel'; import { SettingsTableCard } from '@/settings/components/SettingsTableCard'; import { PlansTags } from '@/settings/billing/components/internal/PlansTags'; import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader'; import { useNumberFormat } from '@/localization/hooks/useNumberFormat'; import { beautifyExactDate } from '~/utils/date-utils'; import { BillingPlanKey } from '~/generated-metadata/graphql'; import { SubscriptionInterval, SubscriptionStatus, type WorkspaceBillingAdminPanelQuery, } from '~/generated-admin/graphql'; const STRIPE_DASHBOARD_BASE_URL = 'https://dashboard.stripe.com'; const BASE_PRODUCT_KEY = 'BASE_PRODUCT'; const RESOURCE_CREDIT_KEY = 'RESOURCE_CREDIT'; const EM_DASH = '\u2014'; type SettingsAdminWorkspaceBillingContentProps = { workspaceId: string; }; const StyledContainer = styled.div` display: flex; flex-direction: column; gap: ${themeCssVariables.spacing[3]}; margin-top: ${themeCssVariables.spacing[6]}; `; const StyledExternalLink = styled.a` align-items: center; color: inherit; display: inline-flex; gap: ${themeCssVariables.spacing[1]}; text-decoration: none; &:hover { color: ${themeCssVariables.font.color.primary}; } `; const StyledMono = styled.span` font-family: ${themeCssVariables.code.font.family}; `; const StyledItemValue = styled.div` align-items: center; display: flex; gap: ${themeCssVariables.spacing[2]}; `; const STATUS_COLORS: Record = { [SubscriptionStatus.Active]: 'green', [SubscriptionStatus.Trialing]: 'blue', [SubscriptionStatus.PastDue]: 'orange', [SubscriptionStatus.Canceled]: 'red', [SubscriptionStatus.Unpaid]: 'red', [SubscriptionStatus.Paused]: 'gray', [SubscriptionStatus.Incomplete]: 'gray', [SubscriptionStatus.IncompleteExpired]: 'gray', }; const STATUS_LABELS: Record = { [SubscriptionStatus.Active]: 'Active', [SubscriptionStatus.Trialing]: 'Trialing', [SubscriptionStatus.PastDue]: 'Past Due', [SubscriptionStatus.Canceled]: 'Canceled', [SubscriptionStatus.Unpaid]: 'Unpaid', [SubscriptionStatus.Paused]: 'Paused', [SubscriptionStatus.Incomplete]: 'Incomplete', [SubscriptionStatus.IncompleteExpired]: 'Incomplete Expired', }; const formatCurrency = (amountMinor: number, currency: string): string => { const normalizedCurrency = currency.toUpperCase(); try { return new Intl.NumberFormat('en-US', { style: 'currency', currency: normalizedCurrency, }).format(amountMinor / 100); } catch { return `${(amountMinor / 100).toFixed(2)} ${normalizedCurrency}`; } }; const toBillingPlanKey = (planKey: string): BillingPlanKey | null => planKey === BillingPlanKey.PRO ? BillingPlanKey.PRO : planKey === BillingPlanKey.ENTERPRISE ? BillingPlanKey.ENTERPRISE : null; const StripeLink = ({ path, id, }: { path: 'customers' | 'subscriptions'; id: string; }) => ( {id} ); export const SettingsAdminWorkspaceBillingContent = ({ workspaceId, }: SettingsAdminWorkspaceBillingContentProps) => { const { t } = useLingui(); const { formatNumber } = useNumberFormat(); const apolloAdminClient = useApolloAdminClient(); const { data, loading } = useQuery( GET_WORKSPACE_BILLING_ADMIN_PANEL, { client: apolloAdminClient, variables: { workspaceId }, skip: !workspaceId, }, ); if (loading) { return ( ); } const billing = data?.workspaceBillingAdminPanel ?? null; if (!billing) { return (
); } const { stripeCustomerId, creditBalance, subscription, usage } = billing; const formatCredits = (credits: number): string => formatNumber(credits, { abbreviate: true, decimals: 2 }); const customerItems = [ { Icon: IconId, label: t`Stripe customer`, value: isDefined(stripeCustomerId) ? ( ) : ( EM_DASH ), }, { Icon: IconCoins, label: t`Credit balance`, value: isDefined(creditBalance) ? `${formatNumber(creditBalance, { abbreviate: true, decimals: 2 })} ${t`credits`}` : EM_DASH, }, ]; const intervalLabel = subscription?.interval === SubscriptionInterval.Month ? t`Monthly` : subscription?.interval === SubscriptionInterval.Year ? t`Yearly` : null; const formatPeriod = (start: string, end: string): string => `${beautifyExactDate(start)} → ${beautifyExactDate(end)}`; const planKey = isDefined(subscription?.planKey) ? toBillingPlanKey(subscription.planKey) : null; const isTrialing = subscription?.status === SubscriptionStatus.Trialing; const usageItems = isDefined(usage) ? [ { Icon: IconChartBar, label: t`Credits used`, value: `${formatCredits(usage.usedCredits)} / ${formatCredits(usage.totalGrantedCredits)}`, }, ...(!isTrialing ? [ { Icon: IconCoins, label: t`Base credits`, value: formatCredits(usage.grantedCredits), }, ] : []), ...(usage.rolloverCredits > 0 ? [ { Icon: IconCoins, label: t`Rollover credits`, value: formatCredits(usage.rolloverCredits), }, ] : []), { Icon: IconCalendarRepeat, label: t`Usage period`, value: formatPeriod(usage.periodStart, usage.periodEnd), }, ] : []; const formatItemValue = ( item: NonNullable['items'][number], ): string => { const parts: string[] = []; if (isDefined(item.quantity)) { parts.push(`${formatNumber(item.quantity)} ${t`seats`}`); } if (isDefined(item.includedCredits)) { parts.push( `${formatNumber(item.includedCredits, { abbreviate: true, decimals: 2 })} ${t`credits/period`}`, ); } if (isDefined(item.unitAmount) && isDefined(subscription)) { parts.push(formatCurrency(item.unitAmount, subscription.currency)); } return parts.length > 0 ? parts.join(' · ') : EM_DASH; }; const subscriptionItems = subscription ? [ { Icon: IconCreditCard, label: t`Stripe subscription`, value: ( ), }, { Icon: IconStatusChange, label: t`Status`, value: ( ), }, ...(isDefined(planKey) ? [ { Icon: IconTag, label: t`Plan`, value: , }, ] : []), ...(isDefined(intervalLabel) ? [ { Icon: IconCalendarEvent, label: t`Billing interval`, value: intervalLabel, }, ] : []), { Icon: IconCalendarRepeat, label: t`Current period`, value: formatPeriod( subscription.currentPeriodStart, subscription.currentPeriodEnd, ), }, ...(isDefined(subscription.trialStart) && isDefined(subscription.trialEnd) ? [ { Icon: IconCalendarRepeat, label: t`Trial period`, value: formatPeriod( subscription.trialStart, subscription.trialEnd, ), }, ] : []), ...(subscription.cancelAtPeriodEnd ? [ { Icon: IconCircleX, label: t`Cancels at period end`, value: t`Yes`, }, ] : []), ...(isDefined(subscription.cancelAt) ? [ { Icon: IconCircleX, label: t`Cancels at`, value: beautifyExactDate(subscription.cancelAt), }, ] : []), ...(isDefined(subscription.canceledAt) ? [ { Icon: IconCircleX, label: t`Canceled at`, value: beautifyExactDate(subscription.canceledAt), }, ] : []), ...subscription.items.map((item) => ({ Icon: item.productKey === BASE_PRODUCT_KEY ? IconUsers : item.productKey === RESOURCE_CREDIT_KEY ? IconCoins : IconBox, label: item.productName || t`Unnamed product`, value: ( {formatItemValue(item)} {isDefined(item.productKey) && ( )} ), })), ] : []; return (
{isDefined(usage) && ( )}
{subscription && ( )}
); };