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 (
+
+
+
+
+
+ )
+}