Move Pro billing shell copy into shared presentation

This commit is contained in:
rcourtman
2026-03-26 23:26:36 +00:00
parent fa98e1c6d7
commit b45177f8a5
6 changed files with 54 additions and 7 deletions
@@ -481,6 +481,10 @@ top-level Pulse Pro surface must keep its page-shell title and leading
SettingsPanel title aligned so commercial activation, trial, and pricing state
do not present as one surface in navigation and a differently named surface in
the actual paid settings UI.
That same boundary also owns the shell and section framing copy for the
self-hosted Pro billing surface. `ProLicensePanel.tsx` must consume shared
presentation for its shell title/description, refresh CTA label, and section
headings rather than carrying those commercial labels inline.
Paid Pulse Pro v5 grandfathering is now part of that same canonical boundary:
when a recurring v5 customer migrates into v6, billing persistence,
entitlement evaluation, renewal handling, and Pro-license presentation must
@@ -6,6 +6,7 @@ import { CommercialBillingShell, CommercialSection } from './CommercialBillingSe
import { ProLicensePlanSection } from './ProLicensePlanSection';
import { SelfHostedCommercialActivationSection } from './SelfHostedCommercialActivationSection';
import { useProLicensePanelState } from './useProLicensePanelState';
import { SELF_HOSTED_PRO_BILLING_PRESENTATION } from '@/utils/licensePresentation';
import { getMonitoredSystemBriefSummary } from '@/utils/monitoredSystemPresentation';
export const ProLicensePanel: Component = () => {
@@ -14,8 +15,8 @@ export const ProLicensePanel: Component = () => {
return (
<div class="space-y-6">
<CommercialBillingShell
title="Pulse Pro"
description="Manage self-hosted billing, monitored-system limits, and Pulse Pro license status."
title={SELF_HOSTED_PRO_BILLING_PRESENTATION.shellTitle}
description={SELF_HOSTED_PRO_BILLING_PRESENTATION.shellDescription}
icon={<ShieldCheck class="w-5 h-5" />}
action={
<button
@@ -24,15 +25,15 @@ export const ProLicensePanel: Component = () => {
onClick={state.loadPanelData}
>
<RefreshCw class={`w-3.5 h-3.5 ${state.loading() ? 'animate-spin' : ''}`} />
Refresh
{SELF_HOSTED_PRO_BILLING_PRESENTATION.refreshLabel}
</button>
}
loading={false}
>
<div class="space-y-6">
<CommercialSection
title="Plan"
description="Review your active plan, expiry, included limits, and paid capabilities."
title={SELF_HOSTED_PRO_BILLING_PRESENTATION.planSectionTitle}
description={SELF_HOSTED_PRO_BILLING_PRESENTATION.planSectionDescription}
>
<ProLicensePlanSection
commercialMigrationNotice={state.commercialMigrationNotice()}
@@ -51,7 +52,7 @@ export const ProLicensePanel: Component = () => {
</CommercialSection>
<CommercialSection
title="Usage"
title={SELF_HOSTED_PRO_BILLING_PRESENTATION.usageSectionTitle}
description={getMonitoredSystemBriefSummary()}
>
<MonitoredSystemLedgerPanel embedded />
@@ -458,6 +458,7 @@ describe('ProLicensePanel', () => {
expect(proLicensePanelSource).toContain('./useProLicensePanelState');
expect(proLicensePanelSource).toContain('./ProLicensePlanSection');
expect(proLicensePanelSource).toContain('SelfHostedCommercialActivationSection');
expect(proLicensePanelSource).toContain('SELF_HOSTED_PRO_BILLING_PRESENTATION');
expect(proLicensePanelSource).not.toContain('createSignal(');
expect(proLicensePanelSource).not.toContain('useLocation()');
expect(proLicensePanelStateSource).toContain('useLocation');
@@ -533,10 +533,16 @@ describe('Settings architecture guardrails', () => {
);
expect(monitoredSystemDefinitionDisclosureSource).not.toContain('summary?: string');
expect(monitoredSystemDefinitionDisclosureSource).not.toContain('{props.summary}');
expect(proLicensePanelSource).toContain('@/utils/licensePresentation');
expect(proLicensePanelSource).toContain('SELF_HOSTED_PRO_BILLING_PRESENTATION');
expect(proLicensePanelStateSource).toContain('buildSelfHostedCommercialPlanModel');
expect(proLicensePanelStateSource).toContain('loadLicenseStatus(true)');
expect(proLicensePanelStateSource).toContain('runStartProTrialAction({');
expect(proLicensePanelStateSource).not.toContain('startProTrial()');
expect(proLicensePanelSource).not.toContain('title="Pulse Pro"');
expect(proLicensePanelSource).not.toContain('description="Manage self-hosted billing');
expect(proLicensePanelSource).not.toContain('title="Plan"');
expect(proLicensePanelSource).not.toContain('title="Usage"');
expect(proLicensePlanSectionSource).toContain('CommercialStatGrid');
expect(proLicensePlanSectionSource).toContain('getLicenseStatusLoadingState');
expect(monitoredSystemPresentationSource).toContain(
@@ -1279,7 +1285,11 @@ describe('Settings architecture guardrails', () => {
title,
);
const allowedTitleExpression =
tab === 'system-updates' ? `title={UPDATES_PANEL_COPY.title}` : `title="${title}"`;
tab === 'system-updates'
? `title={UPDATES_PANEL_COPY.title}`
: tab === 'system-billing'
? 'title={SELF_HOSTED_PRO_BILLING_PRESENTATION.shellTitle}'
: `title="${title}"`;
expect(
source,
`${tab} should keep its leading SettingsPanel title aligned with the shell title`,
@@ -19,6 +19,7 @@ import {
getTrialActivationNotice,
isDisplayableLicenseFeature,
SELF_HOSTED_ACTIVATION_PRESENTATION,
SELF_HOSTED_PRO_BILLING_PRESENTATION,
} from '@/utils/licensePresentation';
describe('licensePresentation', () => {
@@ -61,6 +62,16 @@ describe('licensePresentation', () => {
idleActionLabel: 'Start 14-day Pro Trial',
},
});
expect(SELF_HOSTED_PRO_BILLING_PRESENTATION).toEqual({
shellTitle: 'Pulse Pro',
shellDescription:
'Manage self-hosted billing, monitored-system limits, and Pulse Pro license status.',
refreshLabel: 'Refresh',
planSectionTitle: 'Plan',
planSectionDescription:
'Review your active plan, expiry, included limits, and paid capabilities.',
usageSectionTitle: 'Usage',
});
});
it('returns canonical feature labels', () => {
@@ -102,6 +102,15 @@ export interface SelfHostedActivationPresentation {
trial: SelfHostedActivationTrialCopy;
}
export interface SelfHostedProBillingPresentation {
shellTitle: string;
shellDescription: string;
refreshLabel: string;
planSectionTitle: string;
planSectionDescription: string;
usageSectionTitle: string;
}
const GRANDFATHERED_V5_PLAN_LABELS: Record<string, string> = {
v5_lifetime_grandfathered: 'V5 Lifetime Grandfathered',
v5_pro_monthly_grandfathered: 'V5 Pro Monthly (Grandfathered)',
@@ -362,6 +371,17 @@ export const SELF_HOSTED_ACTIVATION_PRESENTATION: SelfHostedActivationPresentati
},
};
export const SELF_HOSTED_PRO_BILLING_PRESENTATION: SelfHostedProBillingPresentation = {
shellTitle: 'Pulse Pro',
shellDescription:
'Manage self-hosted billing, monitored-system limits, and Pulse Pro license status.',
refreshLabel: 'Refresh',
planSectionTitle: 'Plan',
planSectionDescription:
'Review your active plan, expiry, included limits, and paid capabilities.',
usageSectionTitle: 'Usage',
};
export const getOrganizationBillingLicenseStatusLabel = (
status?: Pick<LicenseStatus, 'valid' | 'in_grace_period'> | null,
): string => {