diff --git a/app/src/components/layout/Sidebar.tsx b/app/src/components/layout/Sidebar.tsx index 6ba5d43..faf2ead 100644 --- a/app/src/components/layout/Sidebar.tsx +++ b/app/src/components/layout/Sidebar.tsx @@ -9,6 +9,7 @@ import { TooltipTrigger, } from '@/components/ui/tooltip' import { useAppAuth } from '@/lib/auth' +import { useResolvedBillingAccess } from '@/lib/auth/useResolvedBillingAccess' import { hasAgentDashboardAccess } from '@/lib/agentAccess' interface NavItem { @@ -32,7 +33,14 @@ export function Sidebar() { const navItems = getNavItems(isDemo) const settingsPath = isDemo ? '/demo/settings' : '/app/settings' const { hasFeature, hasPlan, isDevAuth } = useAppAuth() - const canAccessAgentDashboard = hasAgentDashboardAccess({ hasFeature, hasPlan, isDevAuth }) + const billingAccess = useResolvedBillingAccess() + const canAccessAgentDashboard = hasAgentDashboardAccess({ + hasFeature, + hasPlan, + isDevAuth, + resolvedFeatureSlugs: billingAccess.subscriptionFeatureSlugs, + resolvedPlanSlugs: billingAccess.subscriptionPlanSlugs, + }) return ( diff --git a/app/src/lib/agentAccess.ts b/app/src/lib/agentAccess.ts index a1c42e2..53e5c6f 100644 --- a/app/src/lib/agentAccess.ts +++ b/app/src/lib/agentAccess.ts @@ -1,13 +1,36 @@ import { AGENT_MONITOR_FEATURE_SLUGS, hasAnyFeature, hasAnyPlan } from '@/lib/auth/billing' +const AGENT_DASHBOARD_PLAN_FALLBACKS = ['producer', 'commercial', 'professional', 'enterprise'] as const + +function normalizeSlug(value: string): string { + return value + .trim() + .toLowerCase() + .replace(/^user:/, '') + .replace(/^org:/, '') + .replace(/^u:/, '') + .replace(/^o:/, '') + .replace(/-/g, '_') +} + +function hasResolvedSlug(entitlements: readonly string[] | undefined, candidates: readonly string[]): boolean { + if (!entitlements || entitlements.length === 0) return false + const normalizedEntitlements = new Set(entitlements.map((slug) => normalizeSlug(slug))) + return candidates.some((slug) => normalizedEntitlements.has(normalizeSlug(slug))) +} + export function hasAgentDashboardAccess(args: { hasFeature: (featureKey: string) => boolean hasPlan: (plan: string) => boolean isDevAuth?: boolean + resolvedFeatureSlugs?: readonly string[] + resolvedPlanSlugs?: readonly string[] }): boolean { if (args.isDevAuth) return true + if (hasResolvedSlug(args.resolvedFeatureSlugs, AGENT_MONITOR_FEATURE_SLUGS)) return true + if (hasResolvedSlug(args.resolvedPlanSlugs, AGENT_DASHBOARD_PLAN_FALLBACKS)) return true return ( hasAnyFeature(args.hasFeature, AGENT_MONITOR_FEATURE_SLUGS) || - hasAnyPlan(args.hasPlan, ['producer', 'commercial', 'professional', 'enterprise']) + hasAnyPlan(args.hasPlan, AGENT_DASHBOARD_PLAN_FALLBACKS) ) } diff --git a/app/src/lib/convex/useAgentDashboard.ts b/app/src/lib/convex/useAgentDashboard.ts index fca1d92..bcd2718 100644 --- a/app/src/lib/convex/useAgentDashboard.ts +++ b/app/src/lib/convex/useAgentDashboard.ts @@ -4,12 +4,20 @@ import type { Id } from '../../../convex/_generated/dataModel' import { useFarmContext } from '@/lib/farm' import { useAppAuth } from '@/lib/auth' import { hasAgentDashboardAccess } from '@/lib/agentAccess' +import { useResolvedBillingAccess } from '@/lib/auth/useResolvedBillingAccess' import type { AgentBehaviorConfig, AgentProfileId, AgentTriggerType } from '@/lib/types' export function useAgentDashboard() { const { activeFarmId, isLoading: farmLoading } = useFarmContext() const { hasFeature, hasPlan, isDevAuth } = useAppAuth() - const canAccess = hasAgentDashboardAccess({ hasFeature, hasPlan, isDevAuth }) + const billingAccess = useResolvedBillingAccess() + const canAccess = hasAgentDashboardAccess({ + hasFeature, + hasPlan, + isDevAuth, + resolvedFeatureSlugs: billingAccess.subscriptionFeatureSlugs, + resolvedPlanSlugs: billingAccess.subscriptionPlanSlugs, + }) const dashboardState = useQuery( api.agentAdmin.getDashboardState, diff --git a/app/src/routes/app/settings.tsx b/app/src/routes/app/settings.tsx index d817094..ce3726b 100644 --- a/app/src/routes/app/settings.tsx +++ b/app/src/routes/app/settings.tsx @@ -16,6 +16,7 @@ import { useFarmSettings } from '@/lib/convex/useFarmSettings' import { useFarmContext } from '@/lib/farm' import type { FarmSettings } from '@/lib/types' import { useAppAuth } from '@/lib/auth' +import { useResolvedBillingAccess } from '@/lib/auth/useResolvedBillingAccess' import { hasAgentDashboardAccess } from '@/lib/agentAccess' export const Route = createFileRoute('/app/settings')({ @@ -26,9 +27,16 @@ function SettingsPage() { const { settings, isLoading, saveSettings, resetSettings } = useFarmSettings() const { activeFarmId } = useFarmContext() const { hasFeature, hasPlan, isDevAuth } = useAppAuth() + const billingAccess = useResolvedBillingAccess() const [pendingSettings, setPendingSettings] = useState(null) const [saved, setSaved] = useState(false) - const canAccessAgentDashboard = hasAgentDashboardAccess({ hasFeature, hasPlan, isDevAuth }) + const canAccessAgentDashboard = hasAgentDashboardAccess({ + hasFeature, + hasPlan, + isDevAuth, + resolvedFeatureSlugs: billingAccess.subscriptionFeatureSlugs, + resolvedPlanSlugs: billingAccess.subscriptionPlanSlugs, + }) const displaySettings = pendingSettings ?? settings const hasChanges = pendingSettings !== null diff --git a/app/src/routes/subscribe.tsx b/app/src/routes/subscribe.tsx index c082e6d..daf069f 100644 --- a/app/src/routes/subscribe.tsx +++ b/app/src/routes/subscribe.tsx @@ -57,11 +57,11 @@ function SubscribePageContent() { if (isUserLoaded && user) { console.log('[Subscribe] User ID:', user.id) console.log( - '[Subscribe] Clerk plan checks:', + '[Subscribe] Clerk session plan checks (claims-based):', billingPlanSlugs.map((plan) => [plan, checkPlan(plan)]) ) console.log( - '[Subscribe] Clerk feature checks:', + '[Subscribe] Clerk session feature checks (claims-based):', billingFeatureSlugs.map((feature) => [feature, checkFeature(feature)]) ) console.log('[Subscribe] Resolved billing access:', {