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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a8801430f4
commit
5b505bb4a7
@@ -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 (
|
||||
<EmptyState
|
||||
icon={Icon}
|
||||
<ExtensionUpsellState
|
||||
iconName={definition.icon}
|
||||
title={t('upsell_title', { name: definition.name })}
|
||||
description={t('upsell_description')}
|
||||
>
|
||||
<Link href="/settings/billing">
|
||||
<Button>{t('upsell_cta')}</Button>
|
||||
</Link>
|
||||
</EmptyState>
|
||||
ctaLabel={t('upsell_cta')}
|
||||
ctaHref="/settings/billing"
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user