From 66002e8ba7d9f9e85a570b15dd9edd4073bac6b3 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 13 Jun 2026 20:37:12 +0100 Subject: [PATCH] Converge Pro license actions on shared Button primitives --- .../v6/internal/subsystems/cloud-paid.md | 4 ++ .../subsystems/frontend-primitives.md | 5 ++ .../scripts/shared-template-registry.json | 22 +++++++++ .../components/Settings/ProLicensePanel.tsx | 9 ++-- .../Settings/ProLicensePlanSection.tsx | 10 ++-- .../SelfHostedCommercialRecoverySection.tsx | 15 +++--- .../__tests__/ProLicensePanel.test.tsx | 22 +++++++++ .../SharedPrimitives.guardrails.test.ts | 47 +++++++++++++++++++ 8 files changed, 121 insertions(+), 13 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index e5b11d1e0..a9efe71fe 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -594,6 +594,10 @@ or other self-hosted uncapped continuity plans. URLs must compose `ButtonLink`. `ProLicensePlanSection.tsx` must not recreate local anchor button shells for purchase returns, private-runtime actions, or plan-comparison CTAs. + Self-hosted plan refresh, entitlement retry, activation, and clear-license + actions are cloud-paid behaviors, but their button chrome must compose the + frontend-primitives `Button` family instead of carrying Pro-license-local + primary, secondary, or warning action class strings. 18. Add or change paid relay settings and pairing presentation through `frontend-modern/src/components/Settings/RelaySettingsPanel.tsx`, `frontend-modern/src/components/Settings/RelayPairingSection.tsx`, and `frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts`. The retired Dashboard shell must not be restored to carry a Relay onboarding card or equivalent blanket upsell — relay discovery stays inside its owning settings surface. Public demo and other read-only presentation policy states must suppress relay setup and upsell onboarding instead of inviting pairing or commercial diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 96a39e9f7..e8a2b84a5 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -559,6 +559,11 @@ not a replacement status card, CTA band, or page-local nested card. restoring footer-local action shells. Reporting surfaces must use the same primitive for retry, generate, and export actions rather than restoring large local CTA button shells. + Self-hosted commercial plan, retry, activation, and clear-key actions follow + the same shared Button boundary: commercial surfaces own the labels, + entitlement state, and click handlers, while `Button`, `ButtonLink`, and + `UpgradeButtonLink` own the primary, outline, warning, and upgrade/link + chrome. If a new surface needs a variant that the shared primitive does not expose, extend the primitive and registry guard rather than adding a page-local class string. diff --git a/frontend-modern/scripts/shared-template-registry.json b/frontend-modern/scripts/shared-template-registry.json index 4952639a3..ab3938578 100644 --- a/frontend-modern/scripts/shared-template-registry.json +++ b/frontend-modern/scripts/shared-template-registry.json @@ -936,8 +936,11 @@ { "path": "src/components/Settings/InfrastructureInstallerSection.tsx" }, { "path": "src/components/Settings/InfrastructureSourceManager.tsx" }, { "path": "src/components/Settings/InfrastructureWorkspace.tsx" }, + { "path": "src/components/Settings/ProLicensePanel.tsx" }, + { "path": "src/components/Settings/ProLicensePlanSection.tsx" }, { "path": "src/components/Settings/ReportingPanel.tsx" }, { "path": "src/components/Settings/ResourcePicker.tsx" }, + { "path": "src/components/Settings/SelfHostedCommercialRecoverySection.tsx" }, { "path": "src/components/UpdateConfirmationModal.tsx" }, { "path": "src/components/UpdateProgressModal.tsx" }, { "path": "src/components/Workloads/GuestDrawer.tsx" }, @@ -979,6 +982,25 @@ "bg-emerald-600 text-white hover:bg-emerald-700" ] }, + { + "path": "src/components/Settings/ProLicensePanel.tsx", + "patterns": [ + "inline-flex min-h-10 sm:min-h-9 items-center gap-2 px-3 py-2 text-sm font-medium rounded-md border border-border text-base-content hover:bg-surface-hover transition-colors disabled:opacity-60" + ] + }, + { + "path": "src/components/Settings/ProLicensePlanSection.tsx", + "patterns": [ + "mt-2 inline-flex min-h-10 sm:min-h-9 items-center gap-2 px-3 py-2 text-xs font-medium rounded-md border border-amber-300 dark:border-amber-700 text-amber-800 dark:text-amber-200 hover:bg-amber-100 dark:hover:bg-amber-800 transition-colors disabled:opacity-60" + ] + }, + { + "path": "src/components/Settings/SelfHostedCommercialRecoverySection.tsx", + "patterns": [ + "min-h-10 sm:min-h-9 px-4 py-2.5 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 transition-colors disabled:opacity-60 disabled:cursor-not-allowed", + "min-h-10 sm:min-h-9 px-4 py-2.5 text-sm font-medium rounded-md border border-border text-base-content hover:bg-surface-hover transition-colors disabled:opacity-60 disabled:cursor-not-allowed" + ] + }, { "path": "src/components/Settings/InfrastructureInstallerSection.tsx", "patterns": [ diff --git a/frontend-modern/src/components/Settings/ProLicensePanel.tsx b/frontend-modern/src/components/Settings/ProLicensePanel.tsx index 0ba7cfc6c..5087a1a13 100644 --- a/frontend-modern/src/components/Settings/ProLicensePanel.tsx +++ b/frontend-modern/src/components/Settings/ProLicensePanel.tsx @@ -5,6 +5,7 @@ import { ProLicensePlanSection } from './ProLicensePlanSection'; import { SelfHostedCommercialRecoverySection } from './SelfHostedCommercialRecoverySection'; import { useProLicensePanelState } from './useProLicensePanelState'; import { SELF_HOSTED_PRO_BILLING_PRESENTATION } from './selfHostedBillingPresentation'; +import { Button } from '@/components/shared/Button'; import { Subtabs } from '@/components/shared/Subtabs'; import { presentationPolicyHidesCommercialSurfaces, @@ -53,14 +54,16 @@ const ProLicensePanelContent: Component = () => { {SELF_HOSTED_PRO_BILLING_PRESENTATION.refreshLabel} - + } loading={false} > diff --git a/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx b/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx index 561d1921f..89c7ddff8 100644 --- a/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx +++ b/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx @@ -1,6 +1,6 @@ import { Component, For, Show } from 'solid-js'; import RefreshCw from 'lucide-solid/icons/refresh-cw'; -import { ButtonLink } from '@/components/shared/Button'; +import { Button, ButtonLink } from '@/components/shared/Button'; import { UpgradeButtonLink } from '@/components/shared/UpgradeLink'; import { licenseEntitlementsLoadError } from '@/stores/licenseEntitlements'; import { @@ -356,15 +356,17 @@ export const ProLicensePlanSection: Component = (pro

The license server could not be reached. Some features may be temporarily restricted.

- + diff --git a/frontend-modern/src/components/Settings/SelfHostedCommercialRecoverySection.tsx b/frontend-modern/src/components/Settings/SelfHostedCommercialRecoverySection.tsx index 6d3ba7496..d66acc546 100644 --- a/frontend-modern/src/components/Settings/SelfHostedCommercialRecoverySection.tsx +++ b/frontend-modern/src/components/Settings/SelfHostedCommercialRecoverySection.tsx @@ -2,6 +2,7 @@ import { Component, Show } from 'solid-js'; import { formField, controlClass } from '@/components/shared/Form'; import { ExternalTextLink } from '@/components/shared/ExternalTextLink'; import { FormTextarea } from '@/components/shared/FormTextarea'; +import { Button } from '@/components/shared/Button'; import { SELF_HOSTED_RECOVERY_PRESENTATION } from '@/utils/licensePresentation'; import { TERMS_DOC_URL } from '@/utils/docsLinks'; import { CommercialSection } from './CommercialBillingSections'; @@ -91,24 +92,26 @@ export const SelfHostedCommercialRecoverySection: Component<
- - +
diff --git a/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx index b9fbdf5c0..aad55235d 100644 --- a/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx @@ -1138,6 +1138,16 @@ describe('ProLicensePanel', () => { expect(proLicensePanelSource).toContain('SelfHostedCommercialRecoverySection'); expect(selfHostedCommercialRecoverySectionSource).toContain('FormTextarea'); expect(selfHostedCommercialRecoverySectionSource).not.toContain(' { expect(proLicensePanelStateSource).not.toContain('runStartProTrialAction({'); expect(proLicensePanelStateSource).not.toContain('startProTrial()'); expect(proLicensePanelStateSource).toContain("'A license or activation key is required'"); + expect(proLicensePanelSource).toContain('@/components/shared/Button'); + expect(proLicensePanelSource).toContain('variant="outline"'); + expect(proLicensePanelSource).toContain('size="settingsAction"'); + expect(proLicensePanelSource).not.toContain( + 'inline-flex min-h-10 sm:min-h-9 items-center gap-2 px-3 py-2 text-sm font-medium rounded-md border border-border text-base-content hover:bg-surface-hover transition-colors disabled:opacity-60', + ); expect(proLicensePlanSectionSource).toContain('getLicenseStatusLoadingState'); expect(proLicensePlanSectionSource).toContain('getNoActiveSelfHostedActivationState'); expect(proLicensePlanSectionSource).not.toContain('getTrialEndedProLicenseNotice'); @@ -1167,7 +1183,13 @@ describe('ProLicensePanel', () => { expect(proLicensePlanSectionSource).toContain('currentPlanSummary.privateRuntimeAction'); expect(proLicensePlanSectionSource).toContain('props.activationSuccessSummary'); expect(proLicensePlanSectionSource).toContain('summary().actionUrl'); + expect(proLicensePlanSectionSource).toContain('Button'); expect(proLicensePlanSectionSource).toContain('ButtonLink'); + expect(proLicensePlanSectionSource).toContain('variant="warning"'); + expect(proLicensePlanSectionSource).toContain('size="settingsActionXs"'); + expect(proLicensePlanSectionSource).not.toContain( + 'mt-2 inline-flex min-h-10 sm:min-h-9 items-center gap-2 px-3 py-2 text-xs font-medium rounded-md border border-amber-300 dark:border-amber-700 text-amber-800 dark:text-amber-200 hover:bg-amber-100 dark:hover:bg-amber-800 transition-colors disabled:opacity-60', + ); expect(proLicensePlanSectionSource).toContain('UpgradeButtonLink'); expect(selfHostedCommercialRecoverySectionSource).toContain('ExternalTextLink'); expect(selfHostedCommercialRecoverySectionSource).not.toContain('target="_blank"'); diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index be781b18a..18214416a 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -254,6 +254,7 @@ import organizationAccessMembersSectionSource from '@/components/Settings/Organi import organizationIncomingSharesSectionSource from '@/components/Settings/OrganizationIncomingSharesSection.tsx?raw'; import organizationOutgoingSharesSectionSource from '@/components/Settings/OrganizationOutgoingSharesSection.tsx?raw'; import organizationOverviewMembersSectionSource from '@/components/Settings/OrganizationOverviewMembersSection.tsx?raw'; +import proLicensePanelSource from '@/components/Settings/ProLicensePanel.tsx?raw'; import proLicensePlanSectionSource from '@/components/Settings/ProLicensePlanSection.tsx?raw'; import reportingPanelSource from '@/components/Settings/ReportingPanel.tsx?raw'; import rolesPanelSource from '@/components/Settings/RolesPanel.tsx?raw'; @@ -2949,8 +2950,11 @@ describe('shared primitive guardrails', () => { 'src/components/Settings/InfrastructureInstallerSection.tsx', 'src/components/Settings/InfrastructureSourceManager.tsx', 'src/components/Settings/InfrastructureWorkspace.tsx', + 'src/components/Settings/ProLicensePanel.tsx', + 'src/components/Settings/ProLicensePlanSection.tsx', 'src/components/Settings/ReportingPanel.tsx', 'src/components/Settings/ResourcePicker.tsx', + 'src/components/Settings/SelfHostedCommercialRecoverySection.tsx', 'src/components/UpdateConfirmationModal.tsx', 'src/components/UpdateProgressModal.tsx', 'src/components/Workloads/GuestDrawer.tsx', @@ -2993,6 +2997,25 @@ describe('shared primitive guardrails', () => { 'bg-emerald-600 text-white hover:bg-emerald-700', ]), }), + expect.objectContaining({ + path: 'src/components/Settings/ProLicensePanel.tsx', + patterns: expect.arrayContaining([ + 'inline-flex min-h-10 sm:min-h-9 items-center gap-2 px-3 py-2 text-sm font-medium rounded-md border border-border text-base-content hover:bg-surface-hover transition-colors disabled:opacity-60', + ]), + }), + expect.objectContaining({ + path: 'src/components/Settings/ProLicensePlanSection.tsx', + patterns: expect.arrayContaining([ + 'mt-2 inline-flex min-h-10 sm:min-h-9 items-center gap-2 px-3 py-2 text-xs font-medium rounded-md border border-amber-300 dark:border-amber-700 text-amber-800 dark:text-amber-200 hover:bg-amber-100 dark:hover:bg-amber-800 transition-colors disabled:opacity-60', + ]), + }), + expect.objectContaining({ + path: 'src/components/Settings/SelfHostedCommercialRecoverySection.tsx', + patterns: expect.arrayContaining([ + 'min-h-10 sm:min-h-9 px-4 py-2.5 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 transition-colors disabled:opacity-60 disabled:cursor-not-allowed', + 'min-h-10 sm:min-h-9 px-4 py-2.5 text-sm font-medium rounded-md border border-border text-base-content hover:bg-surface-hover transition-colors disabled:opacity-60 disabled:cursor-not-allowed', + ]), + }), expect.objectContaining({ path: 'src/components/Settings/InfrastructureInstallerSection.tsx', patterns: expect.arrayContaining([ @@ -3581,6 +3604,30 @@ describe('shared primitive guardrails', () => { ); expect(reportingPanelSource).not.toContain('bg-blue-600 text-white hover:bg-blue-700'); expect(reportingPanelSource).not.toContain('bg-emerald-600 text-white hover:bg-emerald-700'); + expect(proLicensePanelSource).toContain('@/components/shared/Button'); + expect(proLicensePanelSource).toContain('