Move self-hosted pricing copy into shared contract

This commit is contained in:
rcourtman
2026-03-26 23:10:54 +00:00
parent 428596aea3
commit 556ee2fd92
5 changed files with 128 additions and 40 deletions
@@ -512,6 +512,10 @@ The same owner also holds shared hosted commercial copy such as page title,
introductory description, common Cloud inclusions, setup-step guidance,
hosted-signup section headings, and primary hosted-signup CTA labels when
those facts describe the canonical offer rather than one page's local layout.
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.
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
+32 -40
View File
@@ -19,6 +19,7 @@ import { showToast } from '@/utils/toast';
import { logger } from '@/utils/logger';
import { getTrialStartErrorKind } from '@/utils/upgradePresentation';
import {
SELF_HOSTED_COMMERCIAL_PRESENTATION,
SELF_HOSTED_FEATURE_ROWS,
SELF_HOSTED_PLAN_BY_TIER,
type SelfHostedFeatureRow,
@@ -215,9 +216,9 @@ export default function PricingV6() {
const communityCta = createMemo(() => {
if (isCurrentTier('free')) {
return <TierCtaButton disabled>Current Plan</TierCtaButton>;
return <TierCtaButton disabled>{SELF_HOSTED_COMMERCIAL_PRESENTATION.currentPlanLabel}</TierCtaButton>;
}
return <TierCtaLabel>Free</TierCtaLabel>;
return <TierCtaLabel>{SELF_HOSTED_COMMERCIAL_PRESENTATION.freeLabel}</TierCtaLabel>;
});
// Tier rank for comparison — higher rank = higher tier.
@@ -236,11 +237,11 @@ export default function PricingV6() {
const relayCta = createMemo(() => {
if (isCurrentTier('relay')) {
return <TierCtaButton disabled>Current Plan</TierCtaButton>;
return <TierCtaButton disabled>{SELF_HOSTED_COMMERCIAL_PRESENTATION.currentPlanLabel}</TierCtaButton>;
}
// Users already on a higher tier don't need a Relay CTA.
if ((TIER_RANK[normalizedTier()] ?? 0) > (TIER_RANK['relay'] ?? 0)) {
return <TierCtaLabel>Included</TierCtaLabel>;
return <TierCtaLabel>{SELF_HOSTED_COMMERCIAL_PRESENTATION.includedLabel}</TierCtaLabel>;
}
return (
<a
@@ -249,24 +250,24 @@ export default function PricingV6() {
target="_blank"
rel="noopener noreferrer"
>
Buy Relay
{SELF_HOSTED_COMMERCIAL_PRESENTATION.buyRelayLabel}
</a>
);
});
const proTrialOrUpgrade = createMemo(() => {
if (isCurrentTier('pro')) {
return <TierCtaButton disabled>Current Plan</TierCtaButton>;
return <TierCtaButton disabled>{SELF_HOSTED_COMMERCIAL_PRESENTATION.currentPlanLabel}</TierCtaButton>;
}
// Users already on a higher tier don't need a Pro CTA.
if ((TIER_RANK[normalizedTier()] ?? 0) > (TIER_RANK['pro'] ?? 0)) {
return <TierCtaLabel>Included</TierCtaLabel>;
return <TierCtaLabel>{SELF_HOSTED_COMMERCIAL_PRESENTATION.includedLabel}</TierCtaLabel>;
}
if (canStartTrial() && trialCtaMode() !== 'upgrade') {
return (
<TierCtaButton disabled={startingTrial()} onClick={startTrial}>
Start Free 14-day Trial
{SELF_HOSTED_COMMERCIAL_PRESENTATION.startTrialLabel}
</TierCtaButton>
);
}
@@ -278,24 +279,24 @@ export default function PricingV6() {
target="_blank"
rel="noopener noreferrer"
>
Upgrade to Pro
{SELF_HOSTED_COMMERCIAL_PRESENTATION.upgradeToProLabel}
</a>
);
});
const proPlusCta = createMemo(() => {
if (isCurrentTier('pro_plus')) {
return <TierCtaButton disabled>Current Plan</TierCtaButton>;
return <TierCtaButton disabled>{SELF_HOSTED_COMMERCIAL_PRESENTATION.currentPlanLabel}</TierCtaButton>;
}
// Users already on a higher tier don't need a Pro+ CTA.
if ((TIER_RANK[normalizedTier()] ?? 0) > (TIER_RANK['pro_plus'] ?? 0)) {
return <TierCtaLabel>Included</TierCtaLabel>;
return <TierCtaLabel>{SELF_HOSTED_COMMERCIAL_PRESENTATION.includedLabel}</TierCtaLabel>;
}
if (canStartTrial() && trialCtaMode() !== 'upgrade') {
return (
<TierCtaButton disabled={startingTrial()} onClick={startTrial}>
Start Free 14-day Trial
{SELF_HOSTED_COMMERCIAL_PRESENTATION.startTrialLabel}
</TierCtaButton>
);
}
@@ -307,7 +308,7 @@ export default function PricingV6() {
target="_blank"
rel="noopener noreferrer"
>
Upgrade to Pro+
{SELF_HOSTED_COMMERCIAL_PRESENTATION.upgradeToProPlusLabel}
</a>
);
});
@@ -316,7 +317,10 @@ export default function PricingV6() {
return (
<div class="space-y-6">
<PageHeader title="Pricing" description="Compare tiers and choose what fits." />
<PageHeader
title={SELF_HOSTED_COMMERCIAL_PRESENTATION.pageTitle}
description={SELF_HOSTED_COMMERCIAL_PRESENTATION.pageDescription}
/>
{/* Tier cards — 4 columns on desktop, 2 on tablet, 1 on mobile */}
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
@@ -326,7 +330,7 @@ export default function PricingV6() {
tier={SELF_HOSTED_PLAN_BY_TIER.pro}
cta={proTrialOrUpgrade}
highlighted
badge="Most Popular"
badge={SELF_HOSTED_COMMERCIAL_PRESENTATION.mostPopularBadge}
message={trialMessage()}
/>
<TierCard tier={SELF_HOSTED_PLAN_BY_TIER.proPlus} cta={proPlusCta} />
@@ -334,35 +338,23 @@ export default function PricingV6() {
{/* Upsell links */}
<div class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-sm text-muted">
<span>
Need managed hosting?{' '}
<a href="/cloud" class="text-blue-600 hover:underline dark:text-blue-400">
See Cloud plans
</a>
</span>
<span>
Managing clients?{' '}
<a
href="mailto:hello@pulserelay.pro?subject=Pulse%20MSP%20Inquiry"
class="text-blue-600 hover:underline dark:text-blue-400"
>
See MSP plans
</a>
</span>
<span>
Need more than 50 monitored systems?{' '}
<a
href="mailto:hello@pulserelay.pro?subject=Pulse%20Enterprise%20Inquiry"
class="text-blue-600 hover:underline dark:text-blue-400"
>
Contact us
</a>
</span>
<For each={SELF_HOSTED_COMMERCIAL_PRESENTATION.footerLinks}>
{(link) => (
<span>
{link.preface}{' '}
<a href={link.href} class="text-blue-600 hover:underline dark:text-blue-400">
{link.label}
</a>
</span>
)}
</For>
</div>
{/* Feature comparison table */}
<Card padding="lg" class="overflow-hidden">
<h2 class="text-base font-semibold text-base-content">Feature Comparison</h2>
<h2 class="text-base font-semibold text-base-content">
{SELF_HOSTED_COMMERCIAL_PRESENTATION.featureComparisonHeading}
</h2>
<div class="mt-4 overflow-x-auto">
<Table class="min-w-[800px] w-full border-collapse">
<TableHeader>
@@ -80,6 +80,12 @@ describe('PricingV6', () => {
expect(screen.getByText('15 monitored systems')).toBeInTheDocument();
expect(screen.getByText('50 monitored systems')).toBeInTheDocument();
expect(screen.getByText('Feature Comparison')).toBeInTheDocument();
expect(screen.getByText('Most Popular')).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'See Cloud plans' })).toHaveAttribute('href', '/cloud');
expect(screen.getByRole('link', { name: 'See MSP plans' })).toHaveAttribute(
'href',
'mailto:hello@pulserelay.pro?subject=Pulse%20MSP%20Inquiry',
);
expect(
screen.queryByText('Billing is based on monitored systems. Child resources are included.'),
).not.toBeInTheDocument();
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import {
SELF_HOSTED_COMMERCIAL_PRESENTATION,
SELF_HOSTED_FEATURE_ROWS,
SELF_HOSTED_PLAN_BY_TIER,
SELF_HOSTED_PLAN_DEFINITIONS,
@@ -30,4 +31,37 @@ describe('selfHostedPlans', () => {
proPlus: '50',
});
});
it('keeps shared self-hosted commercial copy in the common contract', () => {
expect(SELF_HOSTED_COMMERCIAL_PRESENTATION).toEqual({
pageTitle: 'Pricing',
pageDescription: 'Compare tiers and choose what fits.',
mostPopularBadge: 'Most Popular',
currentPlanLabel: 'Current Plan',
includedLabel: 'Included',
freeLabel: 'Free',
buyRelayLabel: 'Buy Relay',
upgradeToProLabel: 'Upgrade to Pro',
upgradeToProPlusLabel: 'Upgrade to Pro+',
startTrialLabel: 'Start Free 14-day Trial',
featureComparisonHeading: 'Feature Comparison',
footerLinks: [
{
preface: 'Need managed hosting?',
label: 'See Cloud plans',
href: '/cloud',
},
{
preface: 'Managing clients?',
label: 'See MSP plans',
href: 'mailto:hello@pulserelay.pro?subject=Pulse%20MSP%20Inquiry',
},
{
preface: 'Need more than 50 monitored systems?',
label: 'Contact us',
href: 'mailto:hello@pulserelay.pro?subject=Pulse%20Enterprise%20Inquiry',
},
],
});
});
});
@@ -19,6 +19,27 @@ export interface SelfHostedFeatureRow {
proPlus: boolean | string;
}
export interface SelfHostedLinkCTA {
preface: string;
label: string;
href: string;
}
export interface SelfHostedCommercialPresentation {
pageTitle: string;
pageDescription: string;
mostPopularBadge: string;
currentPlanLabel: string;
includedLabel: string;
freeLabel: string;
buyRelayLabel: string;
upgradeToProLabel: string;
upgradeToProPlusLabel: string;
startTrialLabel: string;
featureComparisonHeading: string;
footerLinks: readonly SelfHostedLinkCTA[];
}
export const SELF_HOSTED_PLAN_DEFINITIONS: readonly SelfHostedPlanDefinition[] = [
{
tier: 'community',
@@ -88,6 +109,37 @@ export const SELF_HOSTED_PLAN_BY_TIER: Record<SelfHostedTierKey, SelfHostedPlanD
proPlus: SELF_HOSTED_PLAN_DEFINITIONS[3],
};
export const SELF_HOSTED_COMMERCIAL_PRESENTATION: SelfHostedCommercialPresentation = {
pageTitle: 'Pricing',
pageDescription: 'Compare tiers and choose what fits.',
mostPopularBadge: 'Most Popular',
currentPlanLabel: 'Current Plan',
includedLabel: 'Included',
freeLabel: 'Free',
buyRelayLabel: 'Buy Relay',
upgradeToProLabel: 'Upgrade to Pro',
upgradeToProPlusLabel: 'Upgrade to Pro+',
startTrialLabel: 'Start Free 14-day Trial',
featureComparisonHeading: 'Feature Comparison',
footerLinks: [
{
preface: 'Need managed hosting?',
label: 'See Cloud plans',
href: '/cloud',
},
{
preface: 'Managing clients?',
label: 'See MSP plans',
href: 'mailto:hello@pulserelay.pro?subject=Pulse%20MSP%20Inquiry',
},
{
preface: 'Need more than 50 monitored systems?',
label: 'Contact us',
href: 'mailto:hello@pulserelay.pro?subject=Pulse%20Enterprise%20Inquiry',
},
],
} as const;
export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
{
key: 'monitoring',