polish(dashboard): downgrade the build-assistant hero to the quiet-sentence promo (#1731)

Replace the boxed Card hero on Hem with AgentPromo, a clone of the
SkatteverketPromoCard pattern: one 12.5px muted sentence with the action
link at the end, '(beta)' as a word in the sentence, and a per-company
'Dölj förslaget' dismiss persisted in localStorage
(erp_agent_promo_dismissed:<companyId>) via useSyncExternalStore.

Removes the hover:border-primary/50 opacity border and the arrow
translate (both against design.md). Gate (!agentBuilt and checklist
dismissed/completed) and hasAi ? /onboarding/agent : /settings/billing
routing unchanged; SkatteverketPromoCard mutual exclusion on agentBuilt
unchanged. Copy moved to dashboard.agent_promo_* in sv+en.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-20 10:06:24 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5
parent 64fc7c783d
commit e1805125af
5 changed files with 96 additions and 34 deletions
+1
View File
@@ -1089,3 +1089,4 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-08-19] Trial-expired visibility: the 2026-07-11 "no trial-expired nag" call is narrowed at the founder's direction (user-reported discoverability failure 2026-08-18): one on-entry dialog with persisted acknowledgement (user_preferences.ui_state.trial_expired_ack, per user AND company, never localStorage) plus a non-dismissable quiet chrome link (SubscriptionTouchpoint replaces the vanishing countdown pill, stays visible when the rail is collapsed, and adds the first mobile touchpoint). Free tier stays a legitimate resting state: no recurring nag, muted chrome tone, no ochre. getCompanyEntitlements also reads company_subscriptions.status inside its existing Promise.all (zero extra round-trips) so churned payers get "abonnemang" copy instead of "provperiod". Hem AttnLine variant skipped: the page already carries the otherAccountHint AttnLine and design.md allows max one per page.
[2026-08-19] Removed PeriodiseringAutoDetectToggle (settings > Automatik) instead of wiring it: the localStorage key it wrote (periodisering_autodetect_enabled) had no reader anywhere, so it advertised "automatisk periodiseringsdetektering" while changing nothing; auto-detect is already best-effort and review-gated in the wizard, so the row is now a plain link to the periodisering wizard. Deleting the key is safe: it was write-only.
[2026-08-19] Periodisering auto-detect materiality floor uses entity_type as a K1 proxy: no stored flag distinguishes förenklat årsbokslut (K1, BFNAR 2006:1) from full årsbokslut (BFNAR 2017:3) for enskild firma, so every EF gets K1 wording and every AB gets K2, always advisory ("behöver normalt inte"), never prohibitive. Suggestions under 5 000 kr are tagged low-confidence (unticked) rather than dropped because the relief is a MAY, not a MUST; personnel-cost lines (7xxx) are exempt from the floor since K1/K2 require personnel costs to always be accrued.
[2026-08-19] Hem build-assistant hero downgraded to the quiet-sentence pattern (AgentPromo, matches SkatteverketPromoCard; founder direction 2026-08-18 'redesign first, maybe remove later'): dismissal is per-company localStorage (erp_agent_promo_dismissed:<companyId>) like the SKV promo, gate and hasAi/billing routing unchanged.
+80
View File
@@ -0,0 +1,80 @@
'use client'
import { useCallback, useSyncExternalStore } from 'react'
import Link from 'next/link'
import { useTranslations } from 'next-intl'
import { useCapability } from '@/contexts/CompanyContext'
import { CAPABILITY } from '@/lib/entitlements/keys'
const dismissKey = (companyId: string) => `erp_agent_promo_dismissed:${companyId}`
// storage events only fire in OTHER tabs; this custom event covers the
// same-tab dismissal so useSyncExternalStore re-reads localStorage.
const DISMISS_EVENT = 'erp-agent-promo-dismissed'
function subscribeToDismissal(onStoreChange: () => void) {
window.addEventListener('storage', onStoreChange)
window.addEventListener(DISMISS_EVENT, onStoreChange)
return () => {
window.removeEventListener('storage', onStoreChange)
window.removeEventListener(DISMISS_EVENT, onStoreChange)
}
}
interface AgentPromoProps {
companyId: string
}
/**
* Build-assistant nudge on Hem, shown until the company has a verified
* agent_profile (the caller gates on that plus the first-run checklist
* being dismissed or completed, since the checklist already carries the
* assistant as its last step).
* Shape: one quiet sentence with the action link at the end and the same
* "Dölj" text control the Skatteverket promo uses, not a boxed card. It is
* an optional offer, not an exception, so it stays muted (convention 6/12).
* Non-payers keep seeing it (conversion surface) but it routes to billing
* instead of a build flow that would 403.
*/
export function AgentPromo({ companyId }: AgentPromoProps) {
const t = useTranslations('dashboard')
const hasAi = useCapability(CAPABILITY.ai)
// Server snapshot says dismissed: the promo appears only after hydration,
// when localStorage is readable, so server and client never disagree.
const dismissed = useSyncExternalStore(
subscribeToDismissal,
() => localStorage.getItem(dismissKey(companyId)) === 'true',
() => true
)
const dismiss = useCallback(() => {
localStorage.setItem(dismissKey(companyId), 'true')
window.dispatchEvent(new Event(DISMISS_EVENT))
}, [companyId])
if (dismissed) return null
return (
<section className="flex items-start justify-between gap-4">
<p className="text-[12.5px] leading-5 text-muted-foreground">
{t(hasAi ? 'agent_promo_description' : 'agent_promo_description_upgrade')}{' '}
{/* py-3/-my-3: a 44px tap target on a link that still sits inline in
the sentence; the negative margin keeps the line box at 20px so
nothing shifts. */}
<Link
href={hasAi ? '/onboarding/agent' : '/settings/billing'}
className="inline-block -my-3 whitespace-nowrap py-3 text-foreground underline decoration-border underline-offset-4 transition-colors hover:decoration-foreground"
>
{t(hasAi ? 'agent_promo_cta' : 'agent_promo_cta_upgrade')}
</Link>
</p>
<button
type="button"
onClick={dismiss}
className="-my-3 shrink-0 py-3 text-xs text-muted-foreground underline decoration-border underline-offset-4 transition-colors hover:text-foreground"
>
{t('agent_promo_dismiss')}
</button>
</section>
)
}
+5 -34
View File
@@ -1,21 +1,17 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { createClient } from '@/lib/supabase/client'
import { AttnLine } from '@/components/ui/attn-line'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { useCapability, useCompany } from '@/contexts/CompanyContext'
import { CAPABILITY } from '@/lib/entitlements/keys'
import { useCompany } from '@/contexts/CompanyContext'
import NewUserChecklist from '@/components/onboarding/NewUserChecklist'
import AttGoraSection from '@/components/dashboard/AttGoraSection'
import ResumePane from '@/components/dashboard/ResumePane'
import BackupHealthBanner from '@/components/dashboard/BackupHealthBanner'
import { SkatteverketPromoCard } from '@/components/dashboard/SkatteverketPromoCard'
import { ArrowRight } from 'lucide-react'
import { AgentPromo } from '@/components/dashboard/AgentPromo'
import type { InitialSetupState, OnboardingProgress } from '@/types'
import type { SuggestedMatch, WorklistCounts } from '@/lib/worklist/types'
import type { ResumeItem } from '@/lib/worklist/resume'
@@ -85,7 +81,6 @@ export default function DashboardContent({
sieSweep = null,
}: DashboardContentProps) {
const t = useTranslations('dashboard')
const hasAi = useCapability(CAPABILITY.ai)
const { company } = useCompany()
const router = useRouter()
@@ -144,37 +139,13 @@ export default function DashboardContent({
sieSweep={sieSweep}
/>
{/* Build-assistant hero: shown only until the company has a verified
{/* Build-assistant nudge: shown only until the company has a verified
agent_profile, so existing/migrated users get a clear prompt instead
of a full-screen onboarding takeover. While the stepped first-run
checklist is visible it already carries the assistant as its last
step, so the hero waits until that block is dismissed or completed. */}
step, so the promo waits until that block is dismissed or completed. */}
{!agentBuilt && (initialSetup.dismissedAt || initialSetup.completedAt) && (
<section>
{/* Non-payers keep seeing the hero (conversion surface) but it
routes to billing instead of a build flow that would 403. */}
<Link href={hasAi ? '/onboarding/agent' : '/settings/billing'} className="block group">
<Card className="transition-colors hover:border-primary/50">
<CardContent className="p-6 flex items-center gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<p className="font-display text-xl leading-tight">Bygg din bokföringsassistent</p>
<Badge variant="secondary" className="uppercase tracking-wider">Beta</Badge>
</div>
<p className="text-sm text-muted-foreground mt-1">
{hasAi
? 'Några frågor om din verksamhet kalibrerar en assistent som föreslår bokföring åt dig.'
: 'Ingår i abonnemanget: en assistent som föreslår bokföring åt dig.'}
</p>
</div>
<div className="hidden sm:flex items-center gap-1.5 text-sm font-medium text-foreground group-hover:translate-x-0.5 transition-transform">
<span>{hasAi ? 'Kom igång' : 'Uppgradera'}</span>
<ArrowRight className="h-4 w-4" />
</div>
</CardContent>
</Card>
</Link>
</section>
<AgentPromo companyId={companyId} />
)}
{/* The two panes (concept hem-grid). When nothing is in progress the
+5
View File
@@ -6395,6 +6395,11 @@
"skv_promo_description": "See your tax account and file VAT and employer declarations directly from here. Connect with BankID in a couple of minutes.",
"skv_promo_cta": "Connect",
"skv_promo_dismiss": "Dismiss suggestion",
"agent_promo_description": "Build your bookkeeping assistant (beta): a few questions about your business calibrate an assistant that suggests bookkeeping for you.",
"agent_promo_description_upgrade": "The bookkeeping assistant (beta) is included in the subscription: an assistant that suggests bookkeeping for you.",
"agent_promo_cta": "Get started",
"agent_promo_cta_upgrade": "Upgrade",
"agent_promo_dismiss": "Dismiss suggestion",
"result": "Net result",
"this_year_short": "this year",
"this_month": "this month",
+5
View File
@@ -6395,6 +6395,11 @@
"skv_promo_description": "Se skattekontot och lämna moms- och arbetsgivardeklarationer direkt härifrån. Anslut med BankID på ett par minuter.",
"skv_promo_cta": "Anslut",
"skv_promo_dismiss": "Dölj förslaget",
"agent_promo_description": "Bygg din bokföringsassistent (beta): några frågor om din verksamhet kalibrerar en assistent som föreslår bokföring åt dig.",
"agent_promo_description_upgrade": "Bokföringsassistenten (beta) ingår i abonnemanget: en assistent som föreslår bokföring åt dig.",
"agent_promo_cta": "Kom igång",
"agent_promo_cta_upgrade": "Uppgradera",
"agent_promo_dismiss": "Dölj förslaget",
"result": "Resultat",
"this_year_short": "i år",
"this_month": "denna månad",