mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
fix(commercial): land upgrade entry points on plan selection
Every in-product upgrade destination (gate CTAs, the business-estate card, the public /pricing resolver for mapped features) routed to the bare billing plan page, which renders plan comparison and selection only when the intent=self_hosted_plan query parameter is present — so the surfaces asked users to look at plans and then landed them on a page that showed none. Route them all through the plan-selection href; checkout source attribution composes with the intent param unchanged, and public /pricing arrivals stay unsourced. The Patrol page had the same dead end plus a context gap: plan-locked free sessions saw only a lone "Plans & Billing" button with the mode selector hidden, so the upsell never showed what upgrading unlocks. Render the four Patrol modes with paid modes disabled and Pro-badged for plan-locked sessions (the presentation already used for runtime-locked installs), keep it fully suppressed when commercial surfaces are hidden, and relabel the action "Unlock Patrol modes".
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
() =>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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()');
|
||||
|
||||
@@ -83,7 +83,7 @@ export function getPatrolAutonomyAvailabilityPresentation(
|
||||
...(input.upgradePromptsHidden
|
||||
? {}
|
||||
: {
|
||||
actionLabel: 'Plans & Billing',
|
||||
actionLabel: 'Unlock Patrol modes',
|
||||
destination: input.planUpgradeDestination,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -102,17 +102,17 @@ const IN_PRODUCT_PRICING_DESTINATIONS: Record<string, string> = {
|
||||
// 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']);
|
||||
|
||||
Reference in New Issue
Block a user