Files
accounted/components/extensions/ExtensionUpsellState.tsx
T
Jakob Wennberg 5b505bb4a7 fix(entitlements): stop passing a component across the RSC boundary on the extension upsell page (#962)
The paywall branch in app/(dashboard)/e/[sector]/[slug]/page.tsx resolved
the extension icon server-side and passed the resulting forwardRef component
into the 'use client' EmptyState. React cannot serialize a component across
the server-to-client boundary, so non-payers opening a gated extension
(e.g. /e/general/invoice-inbox) got a 500 error page instead of the
upgrade CTA (digest 1621801304, 2026-07-08).

Fix: new client wrapper components/extensions/ExtensionUpsellState.tsx
accepts only plain string props (iconName, title, description, ctaLabel,
ctaHref) and resolves the icon client-side via resolveIcon, the same
pattern DashboardNav and the command palette already use. The server page
now passes definition.icon as a string. The two other resolveIcon call
sites in server pages render the icon inside the server component, which
is valid RSC, and are left untouched.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:03:46 +02:00

43 lines
1.2 KiB
TypeScript

'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 (
<EmptyState icon={Icon} title={title} description={description}>
<Link href={ctaHref}>
<Button>{ctaLabel}</Button>
</Link>
</EmptyState>
)
}