diff --git a/components/onboarding/NewUserChecklist.tsx b/components/onboarding/NewUserChecklist.tsx index d2cd2084..e8efad56 100644 --- a/components/onboarding/NewUserChecklist.tsx +++ b/components/onboarding/NewUserChecklist.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import Link from 'next/link' import { useRouter } from 'next/navigation' import { useTranslations } from 'next-intl' @@ -8,6 +8,7 @@ import { Check } from 'lucide-react' import posthog from 'posthog-js' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' +import JourneyOrb from '@/components/onboarding/journey/JourneyOrb' import { cn } from '@/lib/utils' import { useErrorToast } from '@/lib/hooks/use-error-toast' import { useFormat } from '@/lib/hooks/use-format' @@ -76,6 +77,12 @@ export default function NewUserChecklist({ const hasAi = useCapability(CAPABILITY.ai) const [state, setState] = useState(initialState) const [saving, setSaving] = useState(null) + // The completion signature: 'verdict' shows the orb check-morph and the + // verdict line, 'closing' fades the block, 'done' keeps it retired for the + // rest of the session. Plays only in the session that finishes the last + // step (companies whose completedAt arrives from the server never see it). + const [retiring, setRetiring] = useState<'verdict' | 'closing' | 'done' | null>(null) + const retireStartedRef = useRef(false) const hasMigration = ENABLED_EXTENSION_IDS.has('arcim-migration') const hasBanking = ENABLED_EXTENSION_IDS.has('enable-banking') @@ -118,12 +125,17 @@ export default function NewUserChecklist({ useEffect(() => { // The block retires itself once every step is done; Dölj remains the - // manual way out. + // manual way out. The signature beat latches via retireStartedRef so a + // failed persist can retry the PATCH without replaying the beat. if ( !state.completedAt && step1Done && step2Done && step3Done && step4Done && step5Done && saving === null ) { + if (!retireStartedRef.current) { + retireStartedRef.current = true + setRetiring('verdict') + } void persist({ completed: true }, 'complete').then((updated) => { if (updated) captureSetup('onboarding_setup_completed', { path: updated.path }) }) @@ -133,7 +145,48 @@ export default function NewUserChecklist({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [step1Done, step2Done, step3Done, step4Done, step5Done, saving, state.completedAt]) - if (state.dismissedAt || state.completedAt) return null + // Beat timing: hold the verdict, then fade, then stay retired. + useEffect(() => { + if (retiring !== 'verdict') return + const toClosing = window.setTimeout(() => setRetiring('closing'), 2600) + const toGone = window.setTimeout(() => setRetiring('done'), 3200) + return () => { + window.clearTimeout(toClosing) + window.clearTimeout(toGone) + } + }, [retiring]) + + const numbers = checklistNumbers({ hasSkatteverket, hasInbox }) + const stepCount = numbers.count + + if (state.dismissedAt) return null + // After the beat, stay retired even while the completion PATCH is still in + // flight or retrying: falling through to the full checklist here would + // flash it after the verdict already played. A failed PATCH keeps retrying + // invisibly; the next visit renders from server truth either way. + if (retiring === 'done') return null + if (state.completedAt && !retiring) return null + + if (retiring) { + return ( +
+
+ {/* The orb draws at the top quarter of its canvas (CY = height/4), + so a 100px canvas clipped to 52px shows exactly the check. */} + +

{t('completed_verdict')}

+
+
+ ) + } const goMigration = async () => { const updated = await persist({ path: 'migration' }, 'migration') @@ -167,8 +220,6 @@ export default function NewUserChecklist({ }) const activeStep = !step1Done ? 1 : !step2Done ? 2 : !step3Done ? 3 : !step4Done ? 4 : 5 - const numbers = checklistNumbers({ hasSkatteverket, hasInbox }) - const stepCount = numbers.count return (
diff --git a/messages/en.json b/messages/en.json index 3c36fbe6..7c427cac 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1336,6 +1336,7 @@ "toast_status_update_failed": "Could not update status" }, "initial_setup": { + "completed_verdict": "Your bookkeeping is up and running.", "title": "{count} steps and your bookkeeping is running", "step_books_title": "Get your books in", "step_books_description": "Import from another system or upload a SIE file. New business?", diff --git a/messages/sv.json b/messages/sv.json index 325d48bd..e263b5c7 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -1336,6 +1336,7 @@ "toast_status_update_failed": "Kunde inte uppdatera status" }, "initial_setup": { + "completed_verdict": "Bokföringen är igång.", "title": "{count} steg så är bokföringen igång", "step_books_title": "Få in din bokföring", "step_books_description": "Importera från ett annat system eller ladda upp en SIE-fil. Ny verksamhet?",