diff --git a/frontend-modern/src/components/BusinessEstateCard.tsx b/frontend-modern/src/components/BusinessEstateCard.tsx index be285a449..01f6b42f4 100644 --- a/frontend-modern/src/components/BusinessEstateCard.tsx +++ b/frontend-modern/src/components/BusinessEstateCard.tsx @@ -11,6 +11,7 @@ import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentat import { getSelfHostedBillingHref, PURCHASE_HANDOFF_SOURCE_ESTATE_CARD, + SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_INTENT, } from '@/utils/pricingHandoff'; import { ActionIconButton, Button } from '@/components/shared/Button'; import BriefcaseIcon from 'lucide-solid/icons/briefcase'; @@ -107,7 +108,12 @@ export function BusinessEstateCard() { const handleSeePlans = () => { setDismissed(true); setShowCard(false); - navigate(getSelfHostedBillingHref('plan', { source: PURCHASE_HANDOFF_SOURCE_ESTATE_CARD })); + navigate( + getSelfHostedBillingHref('plan', { + intent: SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_INTENT, + source: PURCHASE_HANDOFF_SOURCE_ESTATE_CARD, + }), + ); }; createEffect(() => { diff --git a/frontend-modern/src/components/__tests__/BusinessEstateCard.test.tsx b/frontend-modern/src/components/__tests__/BusinessEstateCard.test.tsx index 27efe9575..f9d32296e 100644 --- a/frontend-modern/src/components/__tests__/BusinessEstateCard.test.tsx +++ b/frontend-modern/src/components/__tests__/BusinessEstateCard.test.tsx @@ -167,7 +167,7 @@ describe('BusinessEstateCard', () => { expect(JSON.parse(localStorage.getItem(DISMISSED_KEY) ?? 'false')).toBe(true); }); - it('navigates to the plan route and dismisses permanently on the primary action', async () => { + it('navigates to plan selection and dismisses permanently on the primary action', async () => { seedEligibleState(); await renderCard(); @@ -176,7 +176,7 @@ describe('BusinessEstateCard', () => { fireEvent.click(screen.getByRole('button', { name: 'See business plans' })); expect(mockNavigate).toHaveBeenCalledWith( - '/settings/pulse-intelligence/billing/plan?source=estate-card', + '/settings/pulse-intelligence/billing/plan?intent=self_hosted_plan&source=estate-card', ); expect(queryCard()).toBeNull(); expect(JSON.parse(localStorage.getItem(DISMISSED_KEY) ?? 'false')).toBe(true); diff --git a/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx b/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx index adb829172..fa5337f4d 100644 --- a/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx +++ b/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx @@ -142,7 +142,10 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState }), ); const shouldShowAutonomyOptions = createMemo( - () => canChooseAutonomyLevel() || autonomyAvailability().kind === 'runtime_locked', + () => + canChooseAutonomyLevel() || + autonomyAvailability().kind === 'runtime_locked' || + (autonomyAvailability().kind === 'plan_locked' && !commercialSurfacesHidden()), ); const showAutonomyUpgradeAction = createMemo( () => diff --git a/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts b/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts index 3e00f0249..5db27474f 100644 --- a/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts +++ b/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts @@ -99,7 +99,7 @@ describe('PatrolIntelligenceHeader', () => { expect(headerSource).toContain('sm:min-h-0'); }); - it('makes Patrol mode a simple four-level choice without rendering plan-locked paid modes', () => { + it('makes Patrol mode a simple four-level choice with plan-locked paid modes disabled and Pro-badged', () => { expect(PATROL_AUTONOMY_POLICY_PRESENTATION).toEqual({ monitor: { label: 'Watch only', @@ -222,7 +222,7 @@ describe('PatrolIntelligenceHeader', () => { locked: true, title: 'Watch only', body: 'This install watches infrastructure and shows issues.', - actionLabel: 'Plans & Billing', + actionLabel: 'Unlock Patrol modes', destination: { href: '/settings/pulse-intelligence/billing/plan', external: false, diff --git a/frontend-modern/src/features/patrol/__tests__/patrolAutonomyAvailability.test.ts b/frontend-modern/src/features/patrol/__tests__/patrolAutonomyAvailability.test.ts index 100d81e51..162012597 100644 --- a/frontend-modern/src/features/patrol/__tests__/patrolAutonomyAvailability.test.ts +++ b/frontend-modern/src/features/patrol/__tests__/patrolAutonomyAvailability.test.ts @@ -263,12 +263,12 @@ describe('patrolAutonomyAvailability', () => { }); expect(result.kind).toBe('plan_locked'); - expect(result.actionLabel).toBe('Plans & Billing'); + expect(result.actionLabel).toBe('Unlock Patrol modes'); }); }); describe('plan_locked presentation', () => { - it('shows the Plans & Billing action when surfaces are visible', () => { + it('shows the unlock action when surfaces are visible', () => { const result = getPatrolAutonomyAvailabilityPresentation({ autoFixLocked: true, planUpgradeDestination: planDestination, @@ -279,7 +279,7 @@ describe('patrolAutonomyAvailability', () => { locked: true, title: 'Watch only', body: 'This install watches infrastructure and shows issues.', - actionLabel: 'Plans & Billing', + actionLabel: 'Unlock Patrol modes', destination: planDestination, }); }); diff --git a/frontend-modern/src/features/patrol/__tests__/patrolCommercialBoundary.test.ts b/frontend-modern/src/features/patrol/__tests__/patrolCommercialBoundary.test.ts index 5954158c4..9aefbc857 100644 --- a/frontend-modern/src/features/patrol/__tests__/patrolCommercialBoundary.test.ts +++ b/frontend-modern/src/features/patrol/__tests__/patrolCommercialBoundary.test.ts @@ -22,7 +22,7 @@ describe('patrol commercial boundary', () => { expect(patrolIntelligenceHeaderSource).toContain( "autonomyAvailability().kind === 'plan_locked'", ); - expect(patrolAutonomyAvailabilitySource).toContain('Plans & Billing'); + expect(patrolAutonomyAvailabilitySource).toContain('Unlock Patrol modes'); expect(patrolAutonomyAvailabilitySource).toContain('input.upgradePromptsHidden'); expect(patrolIntelligenceHeaderSource).toContain('getPatrolAutonomyAvailabilityPresentation'); expect(patrolIntelligenceHeaderSource).toContain('!presentationPolicyHidesUpgradePrompts()'); diff --git a/frontend-modern/src/features/patrol/patrolAutonomyAvailability.ts b/frontend-modern/src/features/patrol/patrolAutonomyAvailability.ts index f23791bd3..ce33bc01b 100644 --- a/frontend-modern/src/features/patrol/patrolAutonomyAvailability.ts +++ b/frontend-modern/src/features/patrol/patrolAutonomyAvailability.ts @@ -83,7 +83,7 @@ export function getPatrolAutonomyAvailabilityPresentation( ...(input.upgradePromptsHidden ? {} : { - actionLabel: 'Plans & Billing', + actionLabel: 'Unlock Patrol modes', destination: input.planUpgradeDestination, }), }; diff --git a/frontend-modern/src/utils/__tests__/pricingHandoff.branchcov0712b.test.ts b/frontend-modern/src/utils/__tests__/pricingHandoff.branchcov0712b.test.ts index be806cdf3..5da37fc73 100644 --- a/frontend-modern/src/utils/__tests__/pricingHandoff.branchcov0712b.test.ts +++ b/frontend-modern/src/utils/__tests__/pricingHandoff.branchcov0712b.test.ts @@ -123,13 +123,17 @@ describe('pricingHandoff (branch coverage)', () => { describe('getInProductPricingDestination', () => { it('maps known in-product feature keys to their plan/selection hrefs', () => { - expect(getInProductPricingDestination('relay')).toBe(SELF_HOSTED_PRO_BILLING_PLAN_HREF); - expect(getInProductPricingDestination('mobile_app')).toBe(SELF_HOSTED_PRO_BILLING_PLAN_HREF); + expect(getInProductPricingDestination('relay')).toBe( + SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF, + ); + expect(getInProductPricingDestination('mobile_app')).toBe( + SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF, + ); expect(getInProductPricingDestination('long_term_metrics')).toBe( - SELF_HOSTED_PRO_BILLING_PLAN_HREF, + SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF, ); expect(getInProductPricingDestination('agent_profiles')).toBe( - SELF_HOSTED_PRO_BILLING_PLAN_HREF, + SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF, ); expect(getInProductPricingDestination('self_hosted_plan')).toBe( SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF, diff --git a/frontend-modern/src/utils/__tests__/pricingHandoff.test.ts b/frontend-modern/src/utils/__tests__/pricingHandoff.test.ts index 329aace13..c7687fe55 100644 --- a/frontend-modern/src/utils/__tests__/pricingHandoff.test.ts +++ b/frontend-modern/src/utils/__tests__/pricingHandoff.test.ts @@ -46,27 +46,27 @@ describe('pricingHandoff', () => { expect(getInProductPricingDestination('cloud')).toBeUndefined(); }); - it('routes paid self-hosted feature upgrades to the in-product billing plan page with gate attribution', () => { + it('routes paid self-hosted feature upgrades to in-product plan selection with gate attribution', () => { expect(getUpgradeFallbackDestination('relay')).toBe( - `${SELF_HOSTED_PRO_BILLING_PLAN_HREF}?source=gate-relay`, + `${SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF}&source=gate-relay`, ); expect(getUpgradeFallbackDestination('mobile_app')).toBe( - `${SELF_HOSTED_PRO_BILLING_PLAN_HREF}?source=gate-mobile-app`, + `${SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF}&source=gate-mobile-app`, ); expect(getUpgradeFallbackDestination('push_notifications')).toBe( - `${SELF_HOSTED_PRO_BILLING_PLAN_HREF}?source=gate-push-notifications`, + `${SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF}&source=gate-push-notifications`, ); expect(getUpgradeFallbackDestination('ai_alerts')).toBe( - `${SELF_HOSTED_PRO_BILLING_PLAN_HREF}?source=gate-ai-alerts`, + `${SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF}&source=gate-ai-alerts`, ); expect(getUpgradeFallbackDestination('ai_autofix')).toBe( `${SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF}&source=gate-ai-autofix`, ); expect(getUpgradeFallbackDestination('rbac')).toBe( - `${SELF_HOSTED_PRO_BILLING_PLAN_HREF}?source=gate-rbac`, + `${SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF}&source=gate-rbac`, ); expect(getUpgradeFallbackDestination('advanced_reporting')).toBe( - `${SELF_HOSTED_PRO_BILLING_PLAN_HREF}?source=gate-reporting`, + `${SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF}&source=gate-reporting`, ); }); @@ -106,9 +106,7 @@ describe('pricingHandoff', () => { }; for (const key of paidCatalogFeatureKeys) { expect(getUpgradeFallbackDestination(key)).toBe( - key === 'ai_autofix' - ? `${SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF}&source=gate-ai-autofix` - : `${SELF_HOSTED_PRO_BILLING_PLAN_HREF}?source=${expectedGateSources[key]}`, + `${SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF}&source=${expectedGateSources[key]}`, ); } }); @@ -143,7 +141,9 @@ describe('pricingHandoff', () => { `${SELF_HOSTED_PURCHASE_START_PATH}?feature=relay`, ); // The public /pricing resolver stays unsourced for mapped features. - expect(getPricingRouteDestination('?feature=rbac')).toBe(SELF_HOSTED_PRO_BILLING_PLAN_HREF); + expect(getPricingRouteDestination('?feature=rbac')).toBe( + SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF, + ); }); it('keeps retired trial-expired out of upgrade fallbacks while preserving neutral legacy arrival', () => { diff --git a/frontend-modern/src/utils/pricingHandoff.ts b/frontend-modern/src/utils/pricingHandoff.ts index 0ad6f728c..b6e1cc3f9 100644 --- a/frontend-modern/src/utils/pricingHandoff.ts +++ b/frontend-modern/src/utils/pricingHandoff.ts @@ -102,17 +102,17 @@ const IN_PRODUCT_PRICING_DESTINATIONS: Record = { // Paid self-hosted feature keys: route to the owned billing plan page instead // of the Pulse Account purchase-start handoff, which fails for local instances // without PublicURL. - mobile_app: SELF_HOSTED_PRO_BILLING_PLAN_HREF, - push_notifications: SELF_HOSTED_PRO_BILLING_PLAN_HREF, - ai_alerts: SELF_HOSTED_PRO_BILLING_PLAN_HREF, + mobile_app: SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF, + push_notifications: SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF, + ai_alerts: SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF, ai_autofix: SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF, - relay: SELF_HOSTED_PRO_BILLING_PLAN_HREF, - rbac: SELF_HOSTED_PRO_BILLING_PLAN_HREF, - audit_logging: SELF_HOSTED_PRO_BILLING_PLAN_HREF, - advanced_reporting: SELF_HOSTED_PRO_BILLING_PLAN_HREF, - agent_profiles: SELF_HOSTED_PRO_BILLING_PLAN_HREF, - external_probe: SELF_HOSTED_PRO_BILLING_PLAN_HREF, - long_term_metrics: SELF_HOSTED_PRO_BILLING_PLAN_HREF, + relay: SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF, + rbac: SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF, + audit_logging: SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF, + advanced_reporting: SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF, + agent_profiles: SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF, + external_probe: SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF, + long_term_metrics: SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF, }; const RETIRED_TRIAL_PRICING_FEATURES = new Set(['trial_expired']);