Move cloud commercial copy into shared contract

This commit is contained in:
rcourtman 2026-03-26 23:03:57 +00:00
parent 266a504f21
commit aa6dc76092
5 changed files with 71 additions and 17 deletions

View file

@ -508,6 +508,10 @@ headline price, founding-rate override, compare-at strike-through copy,
campaign badge 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.
The same owner also holds shared hosted commercial copy such as page title,
introductory description, common Cloud inclusions, and setup-step guidance
when those facts describe the canonical offer rather than one page's local
layout.
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

View file

@ -5,20 +5,12 @@ import { PageHeader } from '@/components/shared/PageHeader';
import { trackPaywallViewed } from '@/utils/upgradeMetrics';
import { onMount } from 'solid-js';
import {
CLOUD_COMMERCIAL_PRESENTATION,
CLOUD_PLAN_DEFINITIONS,
getCloudPlanPricePresentation,
type CloudPlanDefinition,
} from '@/utils/cloudPlans';
const INCLUDED_IN_ALL = [
'All Pro features',
'Managed hosting',
'Daily backups',
'Secure agent connectivity via Relay',
'Mobile app access and push notifications',
'Dedicated workspace URL',
];
function CloudTierCard(props: { tier: CloudPlanDefinition }) {
const t = props.tier;
const price = getCloudPlanPricePresentation(t);
@ -95,8 +87,8 @@ export default function CloudPricing() {
return (
<div class="space-y-8">
<PageHeader
title="Pulse Cloud"
description="Managed Pulse hosting with Pro features included."
title={CLOUD_COMMERCIAL_PRESENTATION.pageTitle}
description={CLOUD_COMMERCIAL_PRESENTATION.pageDescription}
/>
{/* Tier cards */}
@ -106,9 +98,11 @@ export default function CloudPricing() {
{/* What's included in all Cloud plans */}
<Card padding="lg">
<h2 class="text-base font-semibold text-base-content">Included in every Cloud plan</h2>
<h2 class="text-base font-semibold text-base-content">
{CLOUD_COMMERCIAL_PRESENTATION.includedInAllHeading}
</h2>
<ul class="mt-3 grid grid-cols-1 gap-x-8 gap-y-2 sm:grid-cols-2 text-sm text-base-content">
<For each={INCLUDED_IN_ALL}>
<For each={CLOUD_COMMERCIAL_PRESENTATION.includedInAllItems}>
{(item) => (
<li class="flex gap-2">
<span class="text-emerald-600 dark:text-emerald-400 shrink-0 font-bold"></span>
@ -121,11 +115,11 @@ export default function CloudPricing() {
{/* How it works */}
<Card padding="lg">
<h2 class="text-base font-semibold text-base-content">Setup</h2>
<h2 class="text-base font-semibold text-base-content">
{CLOUD_COMMERCIAL_PRESENTATION.setupHeading}
</h2>
<ol class="mt-3 list-decimal space-y-2 pl-5 text-sm text-base-content">
<li>Create your workspace. No credit card is required for the trial.</li>
<li>Install the Pulse agent on any Linux machine.</li>
<li>Connect systems, review findings, and configure alerts.</li>
<For each={CLOUD_COMMERCIAL_PRESENTATION.setupSteps}>{(step) => <li>{step}</li>}</For>
</ol>
</Card>

View file

@ -37,6 +37,10 @@ describe('CloudPricing', () => {
);
expect(screen.queryByText('Starter founding rate')).not.toBeInTheDocument();
expect(screen.getAllByText('All Pro features')).toHaveLength(1);
expect(screen.getByText('Managed hosting')).toBeInTheDocument();
expect(
screen.getByText('Create your workspace. No credit card is required for the trial.'),
).toBeInTheDocument();
expect(screen.getByText('Setup')).toBeInTheDocument();
expect(
screen.queryByText(/provisioned in under 60 seconds/i),

View file

@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import {
CLOUD_COMMERCIAL_PRESENTATION,
CLOUD_PLAN_BY_TIER,
getCloudPlanPricePresentation,
} from '@/utils/cloudPlans';
@ -25,4 +26,26 @@ describe('cloudPlans', () => {
campaignBadge: undefined,
});
});
it('keeps shared cloud commercial copy in the common contract', () => {
expect(CLOUD_COMMERCIAL_PRESENTATION).toEqual({
pageTitle: 'Pulse Cloud',
pageDescription: 'Managed Pulse hosting with Pro features included.',
includedInAllHeading: 'Included in every Cloud plan',
includedInAllItems: [
'All Pro features',
'Managed hosting',
'Daily backups',
'Secure agent connectivity via Relay',
'Mobile app access and push notifications',
'Dedicated workspace URL',
],
setupHeading: 'Setup',
setupSteps: [
'Create your workspace. No credit card is required for the trial.',
'Install the Pulse agent on any Linux machine.',
'Connect systems, review findings, and configure alerts.',
],
});
});
});

View file

@ -21,6 +21,15 @@ export interface CloudPlanPricePresentation {
campaignBadge?: string;
}
export interface CloudCommercialPresentation {
pageTitle: string;
pageDescription: string;
includedInAllHeading: string;
includedInAllItems: readonly string[];
setupHeading: string;
setupSteps: readonly string[];
}
export const DEFAULT_CLOUD_TIER: CloudTierKey = 'starter';
export const CLOUD_PLAN_DEFINITIONS: readonly CloudPlanDefinition[] = [
@ -72,6 +81,26 @@ export const CLOUD_PLAN_LABELS: Record<string, string> = {
msp_scale: 'MSP Scale',
};
export const CLOUD_COMMERCIAL_PRESENTATION: CloudCommercialPresentation = {
pageTitle: 'Pulse Cloud',
pageDescription: 'Managed Pulse hosting with Pro features included.',
includedInAllHeading: 'Included in every Cloud plan',
includedInAllItems: [
'All Pro features',
'Managed hosting',
'Daily backups',
'Secure agent connectivity via Relay',
'Mobile app access and push notifications',
'Dedicated workspace URL',
],
setupHeading: 'Setup',
setupSteps: [
'Create your workspace. No credit card is required for the trial.',
'Install the Pulse agent on any Linux machine.',
'Connect systems, review findings, and configure alerts.',
],
} as const;
export function parseCloudTier(value?: string | null): CloudTierKey {
switch ((value || '').trim().toLowerCase()) {
case 'power':