fix(entitlements): gate remaining paid-feature UI, drop stale beta pricing copy (#921)

* fix(entitlements): gate remaining paid-feature UI and drop stale beta pricing copy

Second post-cutover sweep. Non-payers still hit live controls that 403 at
the server gate, plus dead plan names in the inbox guide:

- document-inbox guide: remove "Gratis under beta för Open-användare /
  Pro-planen / Se priser" (plans that no longer exist); inbox stays free,
  AI extraction is pitched as part of the subscription
- payslip send button (RunProgressBar): disabled + tooltip without
  email_send; the free PDF download next to it is untouched
- skattekonto "Synkronisera nu": disabled + tooltip without skatteverket
- invoice post-create send-now dialog: skipped without email_send (the
  invoice page's SendInvoiceDialog carries the upsell)
- recurring-invoice auto_send checkbox: disabled + UpgradeNote without
  email_send, and force-unchecked so edit mode can't PATCH it back on
- AgentChat composer: replaced with UpgradeNote without ai, covering
  deep links to /chat/* and conversations opened before expiry
- /onboarding/agent server page: hasCapability(ai) redirect to
  /settings/billing before the gated composer stream starts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(entitlements): address review findings on the gating sweep

- AgentChat: the mount effect auto-fired the first /api/agent/invoke on a
  fresh start regardless of capability; now returns early without ai, and
  Regenerate/Correction re-invokes are guarded too (real find by review bot)
- recurring auto_send upsell copy moved to i18n (auto_send_requires_subscription,
  sv+en) matching the rest of the dialog
- disabled-button tooltips wrapped in a span (payslip send + skattekonto
  sync): browsers suppress title on disabled elements

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-07 14:22:14 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent ed7a178ac6
commit a566a42aec
9 changed files with 87 additions and 35 deletions
+10 -4
View File
@@ -27,6 +27,8 @@ import {
Link2,
RefreshCw,
} from 'lucide-react'
import { useCapability } from '@/contexts/CompanyContext'
import { CAPABILITY } from '@/lib/entitlements/keys'
import type {
SkatteverketSaldoResponse,
SkattekontoTransactionWithSuggestion,
@@ -60,6 +62,7 @@ interface MatchCandidate {
export default function SkattekontoPage() {
const { toast } = useToast()
const hasSkvCapability = useCapability(CAPABILITY.skatteverket)
const [saldo, setSaldo] = useState<SaldoEnvelope | null>(null)
const [tx, setTx] = useState<TransaktionerEnvelope['data'] | null>(null)
const [loading, setLoading] = useState(true)
@@ -305,10 +308,13 @@ export default function SkattekontoPage() {
<div className="space-y-6">
<PageHeading
right={
<Button onClick={syncNow} disabled={syncing}>
<RefreshCw className={`mr-2 h-4 w-4 ${syncing ? 'animate-spin' : ''}`} />
{syncing ? 'Synkroniserar…' : 'Synkronisera nu'}
</Button>
// The span carries the tooltip: `title` is suppressed on disabled elements.
<span title={!hasSkvCapability ? 'Synk mot Skatteverket kräver ett abonnemang' : undefined}>
<Button onClick={syncNow} disabled={syncing || !hasSkvCapability}>
<RefreshCw className={`mr-2 h-4 w-4 ${syncing ? 'animate-spin' : ''}`} />
{syncing ? 'Synkroniserar…' : 'Synkronisera nu'}
</Button>
</span>
}
/>
@@ -2,6 +2,8 @@ import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import { headers } from 'next/headers'
import { getActiveCompanyId } from '@/lib/company/context'
import { hasCapability } from '@/lib/entitlements/has-capability'
import { CAPABILITY } from '@/lib/entitlements/keys'
import { ensureTicSnapshot } from '@/lib/agent/composer/tic-fetch'
import AgentOnboarding from '@/components/onboarding/agent/AgentOnboarding'
@@ -21,6 +23,13 @@ export default async function AgentOnboardingPage() {
const companyId = await getActiveCompanyId(supabase, user.id)
if (!companyId) redirect('/onboarding')
// Paywall: the build flow drives the gated composer stream (ai capability).
// Send non-payers to billing where the assistant is pitched, instead of a
// build sequence that 403s mid-stream.
if (!(await hasCapability(supabase, companyId, CAPABILITY.ai))) {
redirect('/settings/billing')
}
// Everything that doesn't depend on the TIC snapshot loads in one batch:
// the settings row (carrying the is_sandbox gate + the onboarding-form
// data, moms_period, fiscal_year_start_month, f_skatt, city, …, that