fix(skattekonto): stop conflating live-call auth failures with "inte anslutet" + surface SKV connection first in tax settings (#887)

The sync endpoint returns 401 for six distinct auth states (handleSkvError):
NOT_CONNECTED, SESSION_EXPIRED, REFRESH_EXHAUSTED, MISSING_SCOPE,
TOKEN_CORRUPTED, TOKEN_REVOKED. The page treated every 401 as "Skatteverket
är inte anslutet" — directly contradicting Inställningar, which reads the
stored token metadata and truthfully shows "Ansluten" for all but the first.

- syncNow now checks the structured error code: only NOT_CONNECTED flips to
  the not-connected empty state. Every other auth failure renders the
  server's actual Swedish message as a persistent banner with an "Anslut
  igen" CTA, keeping the stored saldo/transactions visible.
- SkatteverketConnectPanel moves to the top of /settings/tax — the
  skattekonto and momsdeklaration pages send users there specifically to
  (re)connect, and below the tax form it sat out of view.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-03 16:35:09 +02:00
committed by GitHub
parent 9f7c842a33
commit 70e893b8d4
2 changed files with 41 additions and 3 deletions
+36 -1
View File
@@ -67,6 +67,10 @@ export default function SkattekontoPage() {
const [bookingId, setBookingId] = useState<string | null>(null)
const [notConnected, setNotConnected] = useState(false)
const [loadError, setLoadError] = useState(false)
// Set when a sync fails with an auth error while a connection exists
// (expired session, missing scope, revoked token). Rendered as a banner —
// the stored data below stays visible and usable.
const [reconnectMessage, setReconnectMessage] = useState<string | null>(null)
const [matchOpenFor, setMatchOpenFor] = useState<StoredSkattekontoTransaction | null>(
null,
)
@@ -122,12 +126,28 @@ export default function SkattekontoPage() {
})
const json = await res.json()
if (!res.ok) {
// 401 covers several distinct auth states (see handleSkvError in the
// skatteverket extension). Only NOT_CONNECTED means "no connection
// exists" — the rest (SESSION_EXPIRED, MISSING_SCOPE, TOKEN_REVOKED,
// …) fire while Inställningar truthfully shows the stored token as
// "Ansluten". Showing the full "inte anslutet"-tomvy for those
// contradicts the settings panel; show the server's actual reason
// with a reconnect CTA instead.
if (res.status === 401) {
setNotConnected(true)
if (json.code === 'NOT_CONNECTED') {
setNotConnected(true)
} else {
setReconnectMessage(
typeof json.error === 'string' && json.error
? json.error
: 'Anslutningen mot Skatteverket behöver förnyas. Anslut igen med BankID.',
)
}
return
}
throw new Error(json.error || 'Synk misslyckades')
}
setReconnectMessage(null)
toast({
title: 'Skattekonto synkroniserat',
description: `${json.data.booked} bokförda, ${json.data.upcoming} kommande`,
@@ -292,6 +312,21 @@ export default function SkattekontoPage() {
}
/>
{reconnectMessage && (
<div className="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-destructive/20 bg-destructive/5 px-4 py-3">
<div className="flex items-center gap-3">
<AlertCircle className="h-4 w-4 shrink-0 text-destructive" />
<p className="text-sm">{reconnectMessage}</p>
</div>
<Button asChild variant="outline" size="sm">
<Link href="/settings/tax">
<ExternalLink className="mr-2 h-3.5 w-3.5" />
Anslut igen
</Link>
</Button>
</div>
)}
<BalanceHero saldo={saldo} loading={loading} onCopyOcr={copyOcr} />
<Card>
@@ -77,11 +77,14 @@ export function TaxSettingsContent() {
return (
<div className="space-y-8">
{/* Connection panel first: the skattekonto and momsdeklaration pages
send users here specifically to (re)connect — below the long tax
form it sat out of view. */}
{showSkatteverket && <SkatteverketConnectPanel />}
<SettingsFormWrapper onSave={handleSave} className="space-y-0">
<TaxSettingsForm settings={settings} />
</SettingsFormWrapper>
{showSkatteverket && <SkatteverketConnectPanel />}
</div>
)
}