From dc61333e57fa48406f05b6bbb297d7d32ffcc0ad Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 26 Mar 2026 22:57:13 +0000 Subject: [PATCH] Canonicalize cloud pricing display contract --- .../v6/internal/subsystems/cloud-paid.md | 5 +++ frontend-modern/src/pages/CloudPricing.tsx | 25 ++++++++---- frontend-modern/src/pages/HostedSignup.tsx | 18 +++++++-- .../src/pages/__tests__/CloudPricing.test.tsx | 2 + .../src/pages/__tests__/HostedSignup.test.tsx | 15 ++++++++ .../src/utils/__tests__/cloudPlans.test.ts | 26 +++++++++++++ frontend-modern/src/utils/cloudPlans.ts | 38 ++++++++++++++----- 7 files changed, 108 insertions(+), 21 deletions(-) create mode 100644 frontend-modern/src/utils/__tests__/cloudPlans.test.ts diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 7314e9458..807b1c032 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -503,6 +503,11 @@ while `frontend-modern/src/pages/CloudPricing.tsx`, `frontend-modern/src/pages/HostedSignup.tsx`, `frontend-modern/src/pages/PricingV6.tsx`, and the self-hosted billing settings surfaces must consume those shared owners instead of redefining retail plan facts or counted-unit policy locally. +That shared ownership also includes display-ready price semantics. Monthly +headline price, founding-rate override, compare-at strike-through copy, and +annual summary text must come from the shared plan-definition owners rather +than page-local string parsing or hardcoded retail amounts inside hosted +pricing/signup screens. 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/CloudPricing.tsx b/frontend-modern/src/pages/CloudPricing.tsx index 6279574f1..1e18cc879 100644 --- a/frontend-modern/src/pages/CloudPricing.tsx +++ b/frontend-modern/src/pages/CloudPricing.tsx @@ -4,7 +4,11 @@ import { Card } from '@/components/shared/Card'; import { PageHeader } from '@/components/shared/PageHeader'; import { trackPaywallViewed } from '@/utils/upgradeMetrics'; import { onMount } from 'solid-js'; -import { CLOUD_PLAN_DEFINITIONS, type CloudPlanDefinition } from '@/utils/cloudPlans'; +import { + CLOUD_PLAN_DEFINITIONS, + getCloudPlanPricePresentation, + type CloudPlanDefinition, +} from '@/utils/cloudPlans'; const INCLUDED_IN_ALL = [ 'All Pro features', @@ -17,6 +21,7 @@ const INCLUDED_IN_ALL = [ function CloudTierCard(props: { tier: CloudPlanDefinition }) { const t = props.tier; + const price = getCloudPlanPricePresentation(t); return ( {t.name} - +
- $19/month + {price.monthlyPrice} + {price.cadence} +
+
+ {price.compareAtMonthlyPrice} + {price.cadence}
-
{t.price}
- +
- {t.price.replace('/month', '')} - /month + {price.monthlyPrice} + {price.cadence}
-
{t.subline}
+
{price.annualSummary}
diff --git a/frontend-modern/src/pages/HostedSignup.tsx b/frontend-modern/src/pages/HostedSignup.tsx index 9a4c4fe63..ce7c39364 100644 --- a/frontend-modern/src/pages/HostedSignup.tsx +++ b/frontend-modern/src/pages/HostedSignup.tsx @@ -5,7 +5,7 @@ import { PageHeader } from '@/components/shared/PageHeader'; import { HostedSignupAPI, type HostedAPIError } from '@/api/hostedSignup'; import { getUpgradeActionUrlOrFallback } from '@/stores/license'; import { logger } from '@/utils/logger'; -import { getCloudPlanForTier } from '@/utils/cloudPlans'; +import { getCloudPlanForTier, getCloudPlanPricePresentation } from '@/utils/cloudPlans'; type SignupStatus = 'idle' | 'submitting' | 'success' | 'unavailable' | 'error'; @@ -50,6 +50,7 @@ export default function HostedSignup() { const params = new URLSearchParams(location.search); return getCloudPlanForTier(params.get('tier')); }); + const selectedPlanPrice = createMemo(() => getCloudPlanPricePresentation(selectedPlan())); const cloudPortalURL = createMemo(() => getUpgradeActionUrlOrFallback('cloud')); const canSubmit = createMemo(() => { @@ -212,9 +213,20 @@ export default function HostedSignup() {

{selectedPlan().name}

-

{selectedPlan().subline}

+

{selectedPlanPrice().annualSummary}

+
+
+

+ {selectedPlanPrice().monthlyPrice} + {selectedPlanPrice().cadence} +

+ +

+ {selectedPlanPrice().compareAtMonthlyPrice} + {selectedPlanPrice().cadence} +

+
-

{selectedPlan().price}

diff --git a/frontend-modern/src/pages/__tests__/CloudPricing.test.tsx b/frontend-modern/src/pages/__tests__/CloudPricing.test.tsx index a38447ad5..9e9493ccc 100644 --- a/frontend-modern/src/pages/__tests__/CloudPricing.test.tsx +++ b/frontend-modern/src/pages/__tests__/CloudPricing.test.tsx @@ -24,6 +24,8 @@ describe('CloudPricing', () => { 'href', '/cloud/signup?tier=starter', ); + expect(screen.getByText('$19')).toBeInTheDocument(); + expect(screen.getByText('$29/month')).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'Choose Power' })).toHaveAttribute( 'href', '/cloud/signup?tier=power', diff --git a/frontend-modern/src/pages/__tests__/HostedSignup.test.tsx b/frontend-modern/src/pages/__tests__/HostedSignup.test.tsx index ea04884b2..6eaa61148 100644 --- a/frontend-modern/src/pages/__tests__/HostedSignup.test.tsx +++ b/frontend-modern/src/pages/__tests__/HostedSignup.test.tsx @@ -81,4 +81,19 @@ describe('HostedSignup', () => { }); }); }); + + it('renders signup plan pricing from the shared cloud pricing contract', async () => { + window.history.replaceState({}, '', '/cloud/signup?tier=starter'); + + render(() => ( + + + + )); + + expect(await screen.findByText('Plan')).toBeInTheDocument(); + expect(screen.getByText('$19/month')).toBeInTheDocument(); + expect(screen.getByText('$29/month')).toBeInTheDocument(); + expect(screen.getByText('or $249/year (save 29%)')).toBeInTheDocument(); + }); }); diff --git a/frontend-modern/src/utils/__tests__/cloudPlans.test.ts b/frontend-modern/src/utils/__tests__/cloudPlans.test.ts new file mode 100644 index 000000000..d9f0256f4 --- /dev/null +++ b/frontend-modern/src/utils/__tests__/cloudPlans.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; + +import { + CLOUD_PLAN_BY_TIER, + getCloudPlanPricePresentation, +} from '@/utils/cloudPlans'; + +describe('cloudPlans', () => { + it('keeps founding-rate display in the shared cloud pricing contract', () => { + expect(getCloudPlanPricePresentation(CLOUD_PLAN_BY_TIER.starter)).toEqual({ + monthlyPrice: '$19', + cadence: '/month', + annualSummary: 'or $249/year (save 29%)', + compareAtMonthlyPrice: '$29', + }); + }); + + it('keeps non-founding tiers on their standard monthly display', () => { + expect(getCloudPlanPricePresentation(CLOUD_PLAN_BY_TIER.power)).toEqual({ + monthlyPrice: '$49', + cadence: '/month', + annualSummary: 'or $449/year (save 24%)', + compareAtMonthlyPrice: undefined, + }); + }); +}); diff --git a/frontend-modern/src/utils/cloudPlans.ts b/frontend-modern/src/utils/cloudPlans.ts index aa813b6ca..c2bf34daf 100644 --- a/frontend-modern/src/utils/cloudPlans.ts +++ b/frontend-modern/src/utils/cloudPlans.ts @@ -4,14 +4,21 @@ export interface CloudPlanDefinition { tier: CloudTierKey; planVersion: string; name: string; - price: string; - subline: string; + monthlyPrice: string; + annualSummary: string; monitoredSystems: number; support: 'Community' | 'Priority'; - foundingPrice?: string; + foundingMonthlyPrice?: string; highlighted?: boolean; } +export interface CloudPlanPricePresentation { + monthlyPrice: string; + cadence: '/month'; + annualSummary: string; + compareAtMonthlyPrice?: string; +} + export const DEFAULT_CLOUD_TIER: CloudTierKey = 'starter'; export const CLOUD_PLAN_DEFINITIONS: readonly CloudPlanDefinition[] = [ @@ -19,19 +26,19 @@ export const CLOUD_PLAN_DEFINITIONS: readonly CloudPlanDefinition[] = [ tier: 'starter', planVersion: 'cloud_starter', name: 'Starter', - price: '$29/month', - subline: 'or $249/year (save 29%)', + monthlyPrice: '$29', + annualSummary: 'or $249/year (save 29%)', monitoredSystems: 10, support: 'Community', - foundingPrice: '$19/month', + foundingMonthlyPrice: '$19', highlighted: true, }, { tier: 'power', planVersion: 'cloud_power', name: 'Power', - price: '$49/month', - subline: 'or $449/year (save 24%)', + monthlyPrice: '$49', + annualSummary: 'or $449/year (save 24%)', monitoredSystems: 30, support: 'Priority', }, @@ -39,8 +46,8 @@ export const CLOUD_PLAN_DEFINITIONS: readonly CloudPlanDefinition[] = [ tier: 'max', planVersion: 'cloud_max', name: 'Max', - price: '$79/month', - subline: 'or $699/year (save 26%)', + monthlyPrice: '$79', + annualSummary: 'or $699/year (save 26%)', monitoredSystems: 75, support: 'Priority', }, @@ -77,3 +84,14 @@ export function parseCloudTier(value?: string | null): CloudTierKey { export function getCloudPlanForTier(value?: string | null): CloudPlanDefinition { return CLOUD_PLAN_BY_TIER[parseCloudTier(value)]; } + +export function getCloudPlanPricePresentation( + plan: CloudPlanDefinition, +): CloudPlanPricePresentation { + return { + monthlyPrice: plan.foundingMonthlyPrice || plan.monthlyPrice, + cadence: '/month', + annualSummary: plan.annualSummary, + compareAtMonthlyPrice: plan.foundingMonthlyPrice ? plan.monthlyPrice : undefined, + }; +}