From 67a4b89907d54b555c4dbaba4849fcf8e19640f0 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 20 Apr 2026 10:35:26 +0100 Subject: [PATCH] Clarify v6 self-hosted plans and activation copy --- .../v6/internal/subsystems/cloud-paid.md | 31 ++++++++++--------- .../__tests__/ProLicensePanel.test.tsx | 12 +++---- .../Settings/selfHostedBillingPresentation.ts | 14 ++++----- .../Settings/useProLicensePanelState.ts | 2 +- .../__tests__/licensePresentation.test.ts | 20 ++++++------ .../src/utils/licensePresentation.ts | 8 ++--- 6 files changed, 45 insertions(+), 42 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 188a980d32..b457822123 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -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 diff --git a/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx index 42fb98d5a8..2fe901e597 100644 --- a/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx @@ -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'); diff --git a/frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts b/frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts index c9556b4833..6a6792ce2b 100644 --- a/frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts +++ b/frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts @@ -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.', }; diff --git a/frontend-modern/src/components/Settings/useProLicensePanelState.ts b/frontend-modern/src/components/Settings/useProLicensePanelState.ts index 6ffab8b74a..8877c3b55f 100644 --- a/frontend-modern/src/components/Settings/useProLicensePanelState.ts +++ b/frontend-modern/src/components/Settings/useProLicensePanelState.ts @@ -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); diff --git a/frontend-modern/src/utils/__tests__/licensePresentation.test.ts b/frontend-modern/src/utils/__tests__/licensePresentation.test.ts index edd36cfdae..2fab0c3444 100644 --- a/frontend-modern/src/utils/__tests__/licensePresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/licensePresentation.test.ts @@ -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({ diff --git a/frontend-modern/src/utils/licensePresentation.ts b/frontend-modern/src/utils/licensePresentation.ts index 914fbfa45a..5ee5c38f15 100644 --- a/frontend-modern/src/utils/licensePresentation.ts +++ b/frontend-modern/src/utils/licensePresentation.ts @@ -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',