diff --git a/app/(dashboard)/e/[sector]/[slug]/page.tsx b/app/(dashboard)/e/[sector]/[slug]/page.tsx index 1e260015..bc347264 100644 --- a/app/(dashboard)/e/[sector]/[slug]/page.tsx +++ b/app/(dashboard)/e/[sector]/[slug]/page.tsx @@ -1,15 +1,12 @@ import { createClient } from '@/lib/supabase/server' import { redirect, notFound } from 'next/navigation' import { getTranslations } from 'next-intl/server' -import Link from 'next/link' import { getExtensionDefinition } from '@/lib/extensions/sectors' import ExtensionWorkspaceLoader from '@/components/extensions/ExtensionWorkspaceLoader' import { getActiveCompanyId } from '@/lib/company/context' import { hasCapability } from '@/lib/entitlements/has-capability' import { requiredCapabilityForExtension } from '@/lib/entitlements/keys' -import { resolveIcon } from '@/lib/extensions/icon-resolver' -import { EmptyState } from '@/components/ui/empty-state' -import { Button } from '@/components/ui/button' +import { ExtensionUpsellState } from '@/components/extensions/ExtensionUpsellState' export default async function ExtensionWorkspacePage({ params, @@ -38,17 +35,16 @@ export default async function ExtensionWorkspacePage({ : false if (!allowed) { const t = await getTranslations('extensions') - const Icon = resolveIcon(definition.icon) + // Only plain strings cross into the client component here: passing a + // resolved icon component would crash RSC serialization (500 page). return ( - - - - - + ctaLabel={t('upsell_cta')} + ctaHref="/settings/billing" + /> ) } } diff --git a/components/extensions/ExtensionUpsellState.tsx b/components/extensions/ExtensionUpsellState.tsx new file mode 100644 index 00000000..b89119fd --- /dev/null +++ b/components/extensions/ExtensionUpsellState.tsx @@ -0,0 +1,42 @@ +'use client' + +import Link from 'next/link' +import { Button } from '@/components/ui/button' +import { EmptyState } from '@/components/ui/empty-state' +import { resolveIcon } from '@/lib/extensions/icon-resolver' + +interface ExtensionUpsellStateProps { + iconName?: string + title: string + description: string + ctaLabel: string + ctaHref: string +} + +/** + * Paywall state for a gated extension workspace, rendered when the active + * company lacks the required capability. + * + * This is a client component on purpose: the server page cannot pass a + * resolved icon component into the 'use client' EmptyState (React cannot + * serialize a component across the RSC boundary; doing so 500s the page). + * The page passes the icon NAME as a plain string and this wrapper resolves + * it client-side, same as DashboardNav and the command palette do. + * Every prop here must stay plain-serializable (strings only). + */ +export function ExtensionUpsellState({ + iconName, + title, + description, + ctaLabel, + ctaHref, +}: ExtensionUpsellStateProps) { + const Icon = iconName ? resolveIcon(iconName) : undefined + return ( + + + + + + ) +}