Move Pro license notices into shared presentation

This commit is contained in:
rcourtman
2026-03-26 23:16:52 +00:00
parent cc4a48b13e
commit 0f152660ae
5 changed files with 55 additions and 12 deletions
@@ -516,6 +516,10 @@ The same rule applies to self-hosted pricing framing: page title/description,
badge copy, primary CTA labels, and adjacent upsell-link copy on
`PricingV6.tsx` must come from the shared self-hosted plan-definition owner
rather than page-local strings.
The shared license presentation owner also holds self-hosted Pro settings
upsell and trial-ended notice copy for `ProLicensePlanSection.tsx`; that
surface must consume canonical helper notices instead of carrying inline
upgrade copy or local status-tone branches.
That same counted-unit boundary also owns the disclosure rule for retail copy:
default billing and pricing surfaces should use concise monitored-system copy,
while the full counted-unit definition appears only behind explicit disclosure
@@ -2,8 +2,10 @@ import { Component, For, Show } from 'solid-js';
import RefreshCw from 'lucide-solid/icons/refresh-cw';
import { getUpgradeActionUrlOrFallback, licenseLoadError } from '@/stores/license';
import {
getInactiveProUpsellNotice,
getLicenseStatusLoadingState,
getNoActiveProLicenseState,
getTrialEndedProLicenseNotice,
} from '@/utils/licensePresentation';
import { CommercialStatGrid } from './CommercialBillingSections';
@@ -44,6 +46,9 @@ const formatDate = (value?: string | null) => {
return date.toLocaleDateString();
};
const trialEndedNotice = getTrialEndedProLicenseNotice();
const inactiveProUpsellNotice = getInactiveProUpsellNotice();
export const ProLicensePlanSection: Component<ProLicensePlanSectionProps> = (props) => (
<>
<Show when={props.trialActivationNotice}>
@@ -71,16 +76,16 @@ export const ProLicensePlanSection: Component<ProLicensePlanSectionProps> = (pro
)}
</Show>
<Show when={props.trialEnded && !licenseLoadError()}>
<div class="mb-4 rounded-md border border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-900 p-3 text-sm text-red-900 dark:text-red-100">
<p class="font-medium">Your Pro trial has ended</p>
<p class="text-xs text-red-800 dark:text-red-200 mt-1">Upgrade to keep Pro features.</p>
<div class={`mb-4 rounded-md border p-3 text-sm ${trialEndedNotice.tone}`}>
<p class="font-medium">{trialEndedNotice.title}</p>
<p class="text-xs mt-1 opacity-90">{trialEndedNotice.body}</p>
<a
class="inline-flex items-center gap-1 mt-2 text-xs font-medium text-red-900 dark:text-red-100 hover:underline"
class="inline-flex items-center gap-1 mt-2 text-xs font-medium hover:underline"
href={getUpgradeActionUrlOrFallback('trial_expired')}
target="_blank"
rel="noreferrer"
>
View Pro plans
{trialEndedNotice.actionLabel}
</a>
</div>
</Show>
@@ -149,18 +154,16 @@ export const ProLicensePlanSection: Component<ProLicensePlanSectionProps> = (pro
</Show>
<Show when={!props.hasPaidFeatures && !props.trialEnded}>
<div class="rounded-md border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900 p-3 text-sm text-amber-800 dark:text-amber-200">
<p class="font-medium">Upgrade to Pro</p>
<p class="text-xs text-amber-700 dark:text-amber-300 mt-1">
Unlock Pulse Patrol, alert analysis, auto-fix, and more.
</p>
<div class={`rounded-md border p-3 text-sm ${inactiveProUpsellNotice.tone}`}>
<p class="font-medium">{inactiveProUpsellNotice.title}</p>
<p class="text-xs mt-1 opacity-90">{inactiveProUpsellNotice.body}</p>
<a
class="inline-flex items-center gap-1 mt-2 text-xs font-medium text-amber-800 dark:text-amber-200 hover:underline"
class="inline-flex items-center gap-1 mt-2 text-xs font-medium hover:underline"
href={getUpgradeActionUrlOrFallback('ai_autofix')}
target="_blank"
rel="noreferrer"
>
View Pro plans
{inactiveProUpsellNotice.actionLabel}
</a>
</div>
</Show>
@@ -466,5 +466,9 @@ describe('ProLicensePanel', () => {
expect(proLicensePanelStateSource).not.toContain('startProTrial()');
expect(proLicensePlanSectionSource).toContain('getLicenseStatusLoadingState');
expect(proLicensePlanSectionSource).toContain('getNoActiveProLicenseState');
expect(proLicensePlanSectionSource).toContain('getTrialEndedProLicenseNotice');
expect(proLicensePlanSectionSource).toContain('getInactiveProUpsellNotice');
expect(proLicensePlanSectionSource).not.toContain('Your Pro trial has ended');
expect(proLicensePlanSectionSource).not.toContain('Unlock Pulse Patrol, alert analysis, auto-fix, and more.');
});
});
@@ -14,6 +14,8 @@ import {
getLicenseTierLabel,
getNoActiveProLicenseState,
getOrganizationBillingLicenseStatusLabel,
getInactiveProUpsellNotice,
getTrialEndedProLicenseNotice,
getTrialActivationNotice,
isDisplayableLicenseFeature,
} from '@/utils/licensePresentation';
@@ -33,6 +35,18 @@ describe('licensePresentation', () => {
expect(getNoActiveProLicenseState()).toEqual({
text: 'No Pro license is active.',
});
expect(getTrialEndedProLicenseNotice()).toEqual({
tone: 'border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-900 text-red-900 dark:text-red-100',
title: 'Your Pro trial has ended',
body: 'Upgrade to keep Pro features.',
actionLabel: 'View Pro plans',
});
expect(getInactiveProUpsellNotice()).toEqual({
tone: 'border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900 text-amber-800 dark:text-amber-200',
title: 'Upgrade to Pro',
body: 'Unlock Pulse Patrol, alert analysis, auto-fix, and more.',
actionLabel: 'View Pro plans',
});
});
it('returns canonical feature labels', () => {
@@ -65,6 +65,10 @@ export interface LicenseInlineNotice {
body: string;
}
export interface LicenseActionNotice extends LicenseInlineNotice {
actionLabel: string;
}
export interface BillingAdminOrganizationBadge {
label: string;
badgeClass: string;
@@ -290,6 +294,20 @@ export const getNoActiveProLicenseState = (): LicenseLoadingStateCopy => ({
text: 'No Pro license is active.',
});
export const getTrialEndedProLicenseNotice = (): LicenseActionNotice => ({
tone: 'border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-900 text-red-900 dark:text-red-100',
title: 'Your Pro trial has ended',
body: 'Upgrade to keep Pro features.',
actionLabel: 'View Pro plans',
});
export const getInactiveProUpsellNotice = (): LicenseActionNotice => ({
tone: 'border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900 text-amber-800 dark:text-amber-200',
title: 'Upgrade to Pro',
body: 'Unlock Pulse Patrol, alert analysis, auto-fix, and more.',
actionLabel: 'View Pro plans',
});
export const getOrganizationBillingLicenseStatusLabel = (
status?: Pick<LicenseStatus, 'valid' | 'in_grace_period'> | null,
): string => {