Clarify v6 self-hosted plans and activation copy

This commit is contained in:
rcourtman 2026-04-20 10:35:26 +01:00
parent c4142ae115
commit 67a4b89907
6 changed files with 45 additions and 42 deletions

View file

@ -206,7 +206,7 @@ Community limit enforcement.
`/settings/system/billing/usage?details=counting-rules` for explanation and
`/settings/system/billing/plan?intent=self_hosted_plan` for plan-selection
intent. That plan-selection arrival must reopen an explicit
`Compare self-hosted plans` prompt inside Pulse Pro billing so operators can
`Compare self-hosted plans` prompt inside Plans & Billing so operators can
review the self-hosted upgrade path without falling back to the legacy
usage surface or a dead-end placeholder CTA.
24. Keep public-demo dashboard bootstrap route-owned on the adjacent
@ -753,7 +753,7 @@ Pulse Account portal URLs, `return_url` parameters, or other portal-entry
contract state in the browser once the server owns that handoff boundary.
`GET /auth/license-purchase-activate` bridge, which auto-submits into the
owned POST activation path; the portal must not render a second manual
`Activate in Pulse Pro` step after checkout. Stripe cancel must return
`Activate in Plans & Billing` step after checkout. Stripe cancel must return
directly to the owned Pulse billing plan route rather than back into the
portal. The owned activation callback must accept the signed state, redeem the
completed checkout, and return the operator to the canonical billing plan
@ -1122,10 +1122,11 @@ owns the plan/read-model presentation. Future Pro-license work must extend
that split instead of pulling route state, entitlement derivation, and retry
lifecycle back into the shell component.
That owned presentation boundary includes the settings shell itself: the
top-level Pulse Pro surface must keep its page-shell title and leading
SettingsPanel title aligned so commercial activation, trial, and pricing state
do not present as one surface in navigation and a differently named surface in
the actual paid settings UI.
top-level self-hosted billing surface must keep its page-shell title and
leading SettingsPanel title aligned on the canonical `Plans & Billing` label
so commercial activation, trial, and pricing state do not present as one
surface in navigation and a differently named surface in the actual paid
settings UI.
That same boundary also owns the shell and section framing copy for the
self-hosted Pro billing surface. `ProLicensePanel.tsx` must consume shared
presentation for its shell title/description, refresh CTA label, and section
@ -1136,20 +1137,22 @@ header metadata so the nav, shell, and route header do not narrate the same
commercial surface differently.
That same shared presentation owner also carries the canonical cross-surface
referral copy used outside the billing shell itself. When infrastructure or
other adjacent settings surfaces need to point operators toward Pulse Pro for
billing, monitored-system limits, or license status, they must consume the
other adjacent settings surfaces need to point operators toward Plans & Billing
for billing, monitored-system limits, or license status, they must consume the
settings-owned referral strings from
`frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts`
instead of drafting route-local commercial guidance or reaching directly into
generic commercial helpers from hosted settings routes.
That same hosted-settings presentation boundary is explicit about bundle
ownership. `frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts`
is the canonical settings-shell adapter for self-hosted Pro shell framing and
referral copy, while `frontend-modern/src/utils/licensePresentation.ts`
remains the shared commercial notice/label owner. Hosted settings surfaces
must not import self-hosted billing framing straight from the generic helper
module when doing so would reintroduce top-level bundle-init cycles into
hosted tenant settings routes.
is the canonical settings-shell adapter for self-hosted Plans & Billing shell
framing and referral copy, while
`frontend-modern/src/utils/licensePresentation.ts` remains the shared
commercial notice/label owner for activation, trial, purchase, and recovery
language such as `License or Activation Key`. Hosted settings surfaces must
not import self-hosted billing framing straight from the generic helper module
when doing so would reintroduce top-level bundle-init cycles into hosted
tenant settings routes.
Paid Pulse Pro v5 grandfathering is now part of that same canonical boundary:
when a recurring v5 customer migrates into v6, billing persistence,
entitlement evaluation, renewal handling, and Pro-license presentation must

View file

@ -179,7 +179,7 @@ describe('ProLicensePanel', () => {
expect(loadCommercialPostureMock).not.toHaveBeenCalled();
expect(screen.getByText('License and billing details are hidden')).toBeInTheDocument();
expect(screen.getByText(/instead of creating a demo license/i)).toBeInTheDocument();
expect(screen.queryByText('Pulse Pro')).not.toBeInTheDocument();
expect(screen.queryByText('Plans & Billing')).not.toBeInTheDocument();
expect(screen.queryByText('Monitored Systems')).not.toBeInTheDocument();
expect(
screen.queryByRole('button', { name: /start 14-day pro trial/i }),
@ -193,7 +193,7 @@ describe('ProLicensePanel', () => {
expect(loadLicenseEntitlementsMock).toHaveBeenCalled();
});
expect(screen.getByText('Pulse Pro')).toBeInTheDocument();
expect(screen.getByText('Plans & Billing')).toBeInTheDocument();
expect(screen.queryByRole('tab', { name: 'Usage' })).not.toBeInTheDocument();
expect(
screen.queryByRole('button', { name: /start 14-day pro trial/i }),
@ -628,7 +628,7 @@ describe('ProLicensePanel', () => {
fireEvent.click(screen.getByText('Redeem existing key'));
fireEvent.input(screen.getByLabelText(/pulse pro key/i), {
fireEvent.input(screen.getByLabelText(/license or activation key/i), {
target: { value: 'header.payload.signature' },
});
@ -660,7 +660,7 @@ describe('ProLicensePanel', () => {
it.each([
{
purchase: SELF_HOSTED_PRO_BILLING_PURCHASE_ACTIVATED,
title: 'Pulse Pro activated',
title: 'Plan activated',
actionLabel: null,
actionHref: null,
},
@ -739,7 +739,7 @@ describe('ProLicensePanel', () => {
renderPanel();
expect(screen.getByText('Pulse Pro activated')).toBeInTheDocument();
expect(screen.getByText('Plan activated')).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'Review plan' })).toHaveAttribute(
'href',
SELF_HOSTED_PRO_BILLING_PLAN_HREF,
@ -972,7 +972,7 @@ describe('ProLicensePanel', () => {
expect(proLicensePanelStateSource).toContain('buildSelfHostedCommercialPlanModel');
expect(proLicensePanelStateSource).toContain('runStartProTrialAction({');
expect(proLicensePanelStateSource).not.toContain('startProTrial()');
expect(proLicensePanelStateSource).toContain("'A Pulse Pro key is required'");
expect(proLicensePanelStateSource).toContain("'A license or activation key is required'");
expect(proLicensePlanSectionSource).toContain('getLicenseStatusLoadingState');
expect(proLicensePlanSectionSource).toContain('getNoActiveProLicenseState');
expect(proLicensePlanSectionSource).toContain('getTrialEndedProLicenseNotice');

View file

@ -33,13 +33,13 @@ export interface SelfHostedProBillingPresentation {
}
export const SELF_HOSTED_PRO_BILLING_PRESENTATION: SelfHostedProBillingPresentation = {
shellTitle: 'Pulse Pro',
shellTitle: 'Plans & Billing',
shellDescription:
'Manage self-hosted billing, plan features, and Pulse Pro license status.',
infrastructureRouteReferral: 'Billing and self-hosted plan features live in Pulse Pro.',
'Manage self-hosted billing, plan features, and activation status.',
infrastructureRouteReferral: 'Billing and self-hosted plan features live in Plans & Billing.',
infrastructureWorkspaceReferral:
'Billing, self-hosted plan features, and Pulse Pro license status live in Pulse Pro, not here.',
sectionSelectorAriaLabel: 'Pulse Pro billing section',
'Billing, self-hosted plan features, and activation status live in Plans & Billing, not here.',
sectionSelectorAriaLabel: 'Self-hosted billing section',
refreshLabel: 'Refresh',
planTabLabel: 'Plan',
usageTabLabel: 'Usage',
@ -57,7 +57,7 @@ export const SELF_HOSTED_PRO_BILLING_PRESENTATION: SelfHostedProBillingPresentat
'Pulse waits for the session presentation policy before showing license, billing, or usage details.',
planSelectionPromptTitle: 'Compare self-hosted plans',
planSelectionPromptBody:
'Community keeps core monitoring free. Compare Relay and Pro in Pulse Account, then return here with Pulse Pro activated automatically.',
'Community keeps core monitoring free. Compare Relay and Pro in Pulse Account, then return here with the new plan activated automatically.',
planSelectionPromptActionLabel: 'Compare plans',
purchaseActivatedPlanActionLabel: 'Review plan',
purchaseCancelledActionLabel: 'Compare plans',
@ -70,5 +70,5 @@ export const SELF_HOSTED_PRO_BILLING_PRESENTATION: SelfHostedProBillingPresentat
trialStartPendingActionLabel: 'Starting...',
recoverySectionTitle: 'Recovery',
recoverySectionDescription:
'Use recovery tools only when you already have a Pulse Pro key or need to remove a local key from this instance.',
'Use recovery tools only when you already have an activation key or need to remove a local key from this instance.',
};

View file

@ -521,7 +521,7 @@ export function useProLicensePanelState() {
const handleActivate = async () => {
const trimmedKey = licenseKey().trim();
if (!trimmedKey) {
notificationStore.error('A Pulse Pro key is required');
notificationStore.error('A license or activation key is required');
return;
}
setActivating(true);

View file

@ -55,7 +55,7 @@ describe('licensePresentation', () => {
});
expect(SELF_HOSTED_RECOVERY_PRESENTATION).toMatchObject({
disclosureLabel: 'Redeem existing key',
fieldLabel: 'Pulse Pro Key',
fieldLabel: 'License or Activation Key',
activateIdleLabel: 'Activate License',
clearIdleLabel: 'Clear License',
legacyNotice: {
@ -63,13 +63,13 @@ describe('licensePresentation', () => {
},
});
expect(SELF_HOSTED_PRO_BILLING_PRESENTATION).toEqual({
shellTitle: 'Pulse Pro',
shellTitle: 'Plans & Billing',
shellDescription:
'Manage self-hosted billing, plan features, and Pulse Pro license status.',
infrastructureRouteReferral: 'Billing and self-hosted plan features live in Pulse Pro.',
'Manage self-hosted billing, plan features, and activation status.',
infrastructureRouteReferral: 'Billing and self-hosted plan features live in Plans & Billing.',
infrastructureWorkspaceReferral:
'Billing, self-hosted plan features, and Pulse Pro license status live in Pulse Pro, not here.',
sectionSelectorAriaLabel: 'Pulse Pro billing section',
'Billing, self-hosted plan features, and activation status live in Plans & Billing, not here.',
sectionSelectorAriaLabel: 'Self-hosted billing section',
refreshLabel: 'Refresh',
planTabLabel: 'Plan',
usageTabLabel: 'Usage',
@ -87,7 +87,7 @@ describe('licensePresentation', () => {
'Pulse waits for the session presentation policy before showing license, billing, or usage details.',
planSelectionPromptTitle: 'Compare self-hosted plans',
planSelectionPromptBody:
'Community keeps core monitoring free. Compare Relay and Pro in Pulse Account, then return here with Pulse Pro activated automatically.',
'Community keeps core monitoring free. Compare Relay and Pro in Pulse Account, then return here with the new plan activated automatically.',
planSelectionPromptActionLabel: 'Compare plans',
purchaseActivatedPlanActionLabel: 'Review plan',
purchaseCancelledActionLabel: 'Compare plans',
@ -100,7 +100,7 @@ describe('licensePresentation', () => {
trialStartPendingActionLabel: 'Starting...',
recoverySectionTitle: 'Recovery',
recoverySectionDescription:
'Use recovery tools only when you already have a Pulse Pro key or need to remove a local key from this instance.',
'Use recovery tools only when you already have an activation key or need to remove a local key from this instance.',
});
});
@ -294,7 +294,7 @@ describe('licensePresentation', () => {
});
expect(getTrialActivationNotice('invalid')).toMatchObject({
title: 'Activation link invalid',
body: expect.stringContaining('fresh secure trial handoff'),
body: expect.stringContaining('Return to Plans & Billing'),
});
expect(getTrialActivationNotice('unavailable')).toMatchObject({
title: 'Activation unavailable',
@ -309,7 +309,7 @@ describe('licensePresentation', () => {
it('returns canonical purchase activation notices', () => {
expect(getPurchaseActivationNotice('activated')).toMatchObject({
title: 'Pulse Pro activated',
title: 'Plan activated',
tone: expect.stringContaining('green'),
});
expect(getPurchaseActivationNotice('cancelled')).toMatchObject({

View file

@ -323,7 +323,7 @@ export const getTrialActivationNotice = (result?: string | null): LicenseInlineN
return {
tone: 'border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-900 text-red-900 dark:text-red-100',
title: 'Activation link invalid',
body: 'That activation handoff is invalid or expired. Return to Pulse Pro settings on this instance and start a fresh secure trial handoff.',
body: 'That activation handoff is invalid or expired. Return to Plans & Billing on this instance and start a fresh secure trial handoff.',
};
case 'replayed':
return {
@ -353,7 +353,7 @@ export const getPurchaseActivationNotice = (result?: string | null): LicenseInli
case 'activated':
return {
tone: 'border-green-200 dark:border-green-900 bg-green-50 dark:bg-green-900 text-green-900 dark:text-green-100',
title: 'Pulse Pro activated',
title: 'Plan activated',
body: 'Pulse finished checkout and activated this instance automatically. The plan state below is live.',
};
case 'cancelled':
@ -448,8 +448,8 @@ export const getInactiveProUpsellNotice = (): LicenseActionNotice => ({
export const SELF_HOSTED_RECOVERY_PRESENTATION: SelfHostedRecoveryPresentation = {
disclosureLabel: 'Redeem existing key',
disclosureDescription:
'Use this only if you already have a Pulse Pro key or need to recover a legacy self-hosted purchase on this instance.',
fieldLabel: 'Pulse Pro Key',
'Use this only if you already have an activation key or need to recover a legacy self-hosted purchase on this instance.',
fieldLabel: 'License or Activation Key',
fieldPlaceholder: 'Paste your license key or activation key',
helpTextBeforeTerms:
'Paste the Pulse v6 activation key shown on the hosted checkout success page. A backup copy is also sent by email, but the hosted success page is the primary handoff. You can also paste a legacy Pulse v5 Pro/Lifetime license key and Pulse will exchange it automatically during activation when migration is available. By activating a license, you agree to the',