diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index bba23cf7d8..85761bffc0 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -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 diff --git a/frontend-modern/src/pages/PricingV6.tsx b/frontend-modern/src/pages/PricingV6.tsx index d09a0767fb..fc4f6db5b3 100644 --- a/frontend-modern/src/pages/PricingV6.tsx +++ b/frontend-modern/src/pages/PricingV6.tsx @@ -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 Current Plan; + return {SELF_HOSTED_COMMERCIAL_PRESENTATION.currentPlanLabel}; } - return Free; + return {SELF_HOSTED_COMMERCIAL_PRESENTATION.freeLabel}; }); // Tier rank for comparison — higher rank = higher tier. @@ -236,11 +237,11 @@ export default function PricingV6() { const relayCta = createMemo(() => { if (isCurrentTier('relay')) { - return Current Plan; + return {SELF_HOSTED_COMMERCIAL_PRESENTATION.currentPlanLabel}; } // Users already on a higher tier don't need a Relay CTA. if ((TIER_RANK[normalizedTier()] ?? 0) > (TIER_RANK['relay'] ?? 0)) { - return Included; + return {SELF_HOSTED_COMMERCIAL_PRESENTATION.includedLabel}; } return ( - Buy Relay + {SELF_HOSTED_COMMERCIAL_PRESENTATION.buyRelayLabel} ); }); const proTrialOrUpgrade = createMemo(() => { if (isCurrentTier('pro')) { - return Current Plan; + return {SELF_HOSTED_COMMERCIAL_PRESENTATION.currentPlanLabel}; } // Users already on a higher tier don't need a Pro CTA. if ((TIER_RANK[normalizedTier()] ?? 0) > (TIER_RANK['pro'] ?? 0)) { - return Included; + return {SELF_HOSTED_COMMERCIAL_PRESENTATION.includedLabel}; } if (canStartTrial() && trialCtaMode() !== 'upgrade') { return ( - Start Free 14-day Trial + {SELF_HOSTED_COMMERCIAL_PRESENTATION.startTrialLabel} ); } @@ -278,24 +279,24 @@ export default function PricingV6() { target="_blank" rel="noopener noreferrer" > - Upgrade to Pro + {SELF_HOSTED_COMMERCIAL_PRESENTATION.upgradeToProLabel} ); }); const proPlusCta = createMemo(() => { if (isCurrentTier('pro_plus')) { - return Current Plan; + return {SELF_HOSTED_COMMERCIAL_PRESENTATION.currentPlanLabel}; } // Users already on a higher tier don't need a Pro+ CTA. if ((TIER_RANK[normalizedTier()] ?? 0) > (TIER_RANK['pro_plus'] ?? 0)) { - return Included; + return {SELF_HOSTED_COMMERCIAL_PRESENTATION.includedLabel}; } if (canStartTrial() && trialCtaMode() !== 'upgrade') { return ( - Start Free 14-day Trial + {SELF_HOSTED_COMMERCIAL_PRESENTATION.startTrialLabel} ); } @@ -307,7 +308,7 @@ export default function PricingV6() { target="_blank" rel="noopener noreferrer" > - Upgrade to Pro+ + {SELF_HOSTED_COMMERCIAL_PRESENTATION.upgradeToProPlusLabel} ); }); @@ -316,7 +317,10 @@ export default function PricingV6() { return (
- + {/* Tier cards — 4 columns on desktop, 2 on tablet, 1 on mobile */}
@@ -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()} /> @@ -334,35 +338,23 @@ export default function PricingV6() { {/* Upsell links */}
- - Need managed hosting?{' '} - - See Cloud plans - - - - Managing clients?{' '} - - See MSP plans - - - - Need more than 50 monitored systems?{' '} - - Contact us - - + + {(link) => ( + + {link.preface}{' '} + + {link.label} + + + )} +
{/* Feature comparison table */} -

Feature Comparison

+

+ {SELF_HOSTED_COMMERCIAL_PRESENTATION.featureComparisonHeading} +

diff --git a/frontend-modern/src/pages/__tests__/PricingV6.test.tsx b/frontend-modern/src/pages/__tests__/PricingV6.test.tsx index 48642730ce..7de52ba90e 100644 --- a/frontend-modern/src/pages/__tests__/PricingV6.test.tsx +++ b/frontend-modern/src/pages/__tests__/PricingV6.test.tsx @@ -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(); diff --git a/frontend-modern/src/utils/__tests__/selfHostedPlans.test.ts b/frontend-modern/src/utils/__tests__/selfHostedPlans.test.ts index c27d16ae99..d3af43f03c 100644 --- a/frontend-modern/src/utils/__tests__/selfHostedPlans.test.ts +++ b/frontend-modern/src/utils/__tests__/selfHostedPlans.test.ts @@ -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', + }, + ], + }); + }); }); diff --git a/frontend-modern/src/utils/selfHostedPlans.ts b/frontend-modern/src/utils/selfHostedPlans.ts index 8849df6afb..8cb357fe48 100644 --- a/frontend-modern/src/utils/selfHostedPlans.ts +++ b/frontend-modern/src/utils/selfHostedPlans.ts @@ -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