'use client' import { useCallback, useSyncExternalStore } from 'react' import { useTranslations } from 'next-intl' import { ArrowRight, FileCheck, X } from 'lucide-react' import { Card, CardContent } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' import { useCapability, useCompanyOptional } from '@/contexts/CompanyContext' import { CAPABILITY } from '@/lib/entitlements/keys' const dismissKey = (companyId: string) => `erp_skv_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-skv-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 SkatteverketPromoCardProps { companyId: string /** True when the active user already has a Skatteverket token. */ connected: boolean } /** * Dismissible dashboard nudge for companies that have never connected * Skatteverket. New companies meet the connect step in NewUserChecklist; * this card is the equivalent surface for existing companies, which * otherwise only discover the integration deep inside the VAT/AGI flows. * Capability-less users never see it: the paywall upsell lives where the * intent is (SkatteverketPanel, AGIPanel), not on the dashboard. * Sandbox companies never see it either: Skatteverket is one of the external * services the sandbox blocks outright, so the CTA would lead nowhere. */ export function SkatteverketPromoCard({ companyId, connected }: SkatteverketPromoCardProps) { const t = useTranslations('dashboard') const extensionEnabled = ENABLED_EXTENSION_IDS.has('skatteverket') const hasCapability = useCapability(CAPABILITY.skatteverket) const isSandbox = useCompanyOptional()?.isSandbox ?? false // Server snapshot says dismissed: the card 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 (!extensionEnabled || !hasCapability || isSandbox || connected || dismissed) return null return (

{t('skv_promo_title')}

{t('skv_promo_description')}

) }