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
+20
View File
@@ -15,6 +15,9 @@ import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { useCapability } from '@/contexts/CompanyContext'
import { CAPABILITY } from '@/lib/entitlements/keys'
import { UpgradeNote } from '@/components/billing/UpgradeNote'
import ApprovalCard from './ApprovalCard'
// Reusable chat surface: used both inside the right-hand AgentSheet and on
@@ -123,6 +126,7 @@ export default function AgentChat({
const firstTurnFiredRef = useRef(false)
const conversationIdRef = useRef<string | null>(initialConversationId ?? null)
const [messages, setMessages] = useState<ChatMessage[]>(initialMessages ?? [])
const hasAi = useCapability(CAPABILITY.ai)
const [input, setInput] = useState('')
const [streaming, setStreaming] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
@@ -158,6 +162,10 @@ export default function AgentChat({
const hasResumeState = !!initialConversationId
if (hasResumeState) return
// Paywall: never auto-fire the first invoke without the ai capability;
// the composer is already replaced by the upgrade note.
if (!hasAi) return
// Seed-message path: render the user's pre-baked starter in the timeline
// and send it as the first turn's user_message (skips intent.capture +
// promptTemplate). Empty seed runs the normal capture-driven flow.
@@ -339,6 +347,7 @@ export default function AgentChat({
}
function handleRegenerate() {
if (!hasAi) return
// Re-run the last user message and let the agent produce a fresh
// response. UI truncates back to the last user message; DB rows are
// append-only, so the previous assistant turn stays in agent_messages
@@ -361,6 +370,7 @@ export default function AgentChat({
// user turn so the agent re-proposes inline: no synthetic user bubble (we
// don't add a user row, and the turn is persisted hidden).
function handleCorrection(correctionMessage: string) {
if (!hasAi) return
void startTurn({ conversationId, userMessage: correctionMessage, hidden: true })
}
@@ -549,6 +559,7 @@ export default function AgentChat({
streamingTail={streaming && i === messages.length - 1}
showRegenerate={
!streaming &&
hasAi &&
i === lastAssistantIdx &&
m.role === 'assistant' &&
m.text.length > 0
@@ -566,6 +577,14 @@ export default function AgentChat({
)}
</div>
{/* Paywall: /api/agent/invoke 403s without the ai capability. Replace
the composer with an upsell so an already-open conversation (or a
deep link to /chat/*) never offers an input that can't send. */}
{!hasAi ? (
<div className="border-t border-border px-5 pt-4 pb-[calc(env(safe-area-inset-bottom,0px)+1rem)]">
<UpgradeNote>AI-assistenten kräver ett abonnemang.</UpgradeNote>
</div>
) : (
<form
// padding-bottom = base 1rem + safe-area-inset-bottom on phones so
// the iOS home indicator / Android gesture bar doesn't overlap the
@@ -619,6 +638,7 @@ export default function AgentChat({
Enter att skicka · Shift+Enter för ny rad
</p>
</form>
)}
</div>
)
}