From 9dfa6c6708edfa5dcc11aa8a7a0a1946a0751365 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:37:51 +0200 Subject: [PATCH] feat(home): first-run block as a numbered four-step thread with partner marks (#1149) * feat(home): first-run block as a numbered three-step thread with partner marks The founder-picked stepped shape for 'Hur vill du komma igang?': 1 Fa in din bokforing (primary Flytta bokforingen + Fortnox/Visma/Bokio marks + '+ SIE'; Starta fran borjan as an inline alternative that just checks the step off), 2 Koppla banken (Enable Banking mark only), 3 Bygg din bokforingsassistent (Beta chip, no vendor logo). Dots walk number -> filled active -> sage check; the persisted state machine is unchanged, but choosing a path no longer auto-completes the setup: the block retires when all three steps are done (or via Dolj). DashboardContent's build-assistant hero now waits until the checklist is gone so the assistant is not pitched twice. initial_setup i18n rewritten for the stepped copy (sv+en), unused selected-state keys removed. Co-Authored-By: Claude Fable 5 * feat(home): Skatteverket as step three, assistant last, ticked steps collapse Founder feedback on #1149: the thread is now four steps: 1 Fa in din bokforing, 2 Koppla banken, 3 Anslut Skatteverket (with the SKV mark, BankID authorize link; skipped entirely in builds without the skatteverket extension), 4 Bygg din bokforingsassistent. A completed step drops its description and actions and keeps only the checked muted title, so the fresh-start pitch never lingers after the books are in. The heading counts honestly ({count} steg) and completion now requires all four steps. Co-Authored-By: Claude Fable 5 * polish(home): one-line checklist header Founder feedback: the title and sub-line said the same thing twice; only '4 steg sa ar bokforingen igang' remains (Dolj stays beside it). Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- app/api/onboarding/state/route.ts | 5 +- components/dashboard/DashboardContent.tsx | 6 +- components/onboarding/NewUserChecklist.tsx | 332 ++++++++++++++------- messages/en.json | 36 ++- messages/sv.json | 36 ++- 5 files changed, 268 insertions(+), 147 deletions(-) diff --git a/app/api/onboarding/state/route.ts b/app/api/onboarding/state/route.ts index 4fe6db68..bfa65458 100644 --- a/app/api/onboarding/state/route.ts +++ b/app/api/onboarding/state/route.ts @@ -87,7 +87,10 @@ export const PATCH = withRouteContext( const update: Record = {} if (body.path !== undefined) { update.initial_setup_path = body.path - update.initial_setup_completed_at = body.path === 'fresh' ? now : null + // Choosing a path no longer completes the setup: since the stepped + // Hem block (rest-of-nav 2), "fresh" just checks off step one and the + // block completes when every step is done. + update.initial_setup_completed_at = null update.initial_setup_dismissed_at = null } if (body.completed !== undefined) { diff --git a/components/dashboard/DashboardContent.tsx b/components/dashboard/DashboardContent.tsx index e6c131cf..aacae550 100644 --- a/components/dashboard/DashboardContent.tsx +++ b/components/dashboard/DashboardContent.tsx @@ -101,8 +101,10 @@ export default function DashboardContent({ {/* Build-assistant hero: 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. */} - {!agentBuilt && ( + of a full-screen onboarding takeover. While the stepped first-run + checklist is visible it already carries the assistant as step 3, so + the hero waits until that block is dismissed or completed. */} + {!agentBuilt && (initialSetup.dismissedAt || initialSetup.completedAt) && (
{/* Non-payers keep seeing the hero (conversion surface) but it routes to billing instead of a build flow that would 403. */} diff --git a/components/onboarding/NewUserChecklist.tsx b/components/onboarding/NewUserChecklist.tsx index ae2f7ae0..3017fb98 100644 --- a/components/onboarding/NewUserChecklist.tsx +++ b/components/onboarding/NewUserChecklist.tsx @@ -2,18 +2,9 @@ import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' -import Link from 'next/link' import { useTranslations } from 'next-intl' -import { - ArrowRight, - ArrowRightLeft, - FileCheck, - Landmark, - MessageCircle, - Sparkles, -} from 'lucide-react' +import { Check } from 'lucide-react' import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { cn } from '@/lib/utils' import { useErrorToast } from '@/lib/hooks/use-error-toast' import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' @@ -30,12 +21,14 @@ interface NewUserChecklistProps { hasAgentBuilt?: boolean } -const pathIcons = { - migration: ArrowRightLeft, - bank: Landmark, - fresh: Sparkles, -} as const - +/** + * First-run getting-started block on Hem, in the founder-picked stepped + * shape: a numbered three-step thread (get the books in, connect the bank, + * build the assistant) with the partner marks on the steps that have them. + * The persisted state machine is unchanged (path / completedAt / + * dismissedAt via /api/onboarding/state); "Starta från början" records the + * fresh path and simply checks off step one. + */ export default function NewUserChecklist({ initialState, className, @@ -81,111 +74,238 @@ export default function NewUserChecklist({ } } + const step1Done = hasBookkeepingImported || state.path === 'fresh' + const step2Done = hasBankConnected + // Companies built without the skatteverket extension skip that step. + const step3Done = !hasSkatteverket || hasSkatteverketConnected + const step4Done = hasAgentBuilt + useEffect(() => { - const selectedPathComplete = - (state.path === 'migration' && hasBookkeepingImported) || - (state.path === 'bank' && hasBankConnected) - if (!state.completedAt && selectedPathComplete && saving === null) { + // The block retires itself once every step is done; Dölj remains the + // manual way out. + if (!state.completedAt && step1Done && step2Done && step3Done && step4Done && saving === null) { void persist({ completed: true }, 'complete') } // persist intentionally stays out: its identity follows the toast hook and // would retrigger this completion sync after every render. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hasBankConnected, hasBookkeepingImported, saving, state.completedAt, state.path]) + }, [step1Done, step2Done, step3Done, step4Done, saving, state.completedAt]) if (state.dismissedAt || state.completedAt) return null - const choosePath = async (path: InitialSetupPath) => { - const updated = await persist({ path }, path) - if (!updated || path === 'fresh') return - if (path === 'migration') { - router.push(hasMigration ? '/import?mode=migration' : '/import?mode=sie') - } else { - router.push(hasBanking ? '/import?mode=psd2' : '/import?mode=bank') - } + const goMigration = async () => { + const updated = await persist({ path: 'migration' }, 'migration') + if (updated) router.push(hasMigration ? '/import?mode=migration' : '/import?mode=sie') + } + const goFresh = () => void persist({ path: 'fresh' }, 'fresh') + const goBank = async () => { + const updated = await persist({ path: state.path ?? 'bank' }, 'bank') + if (updated) router.push(hasBanking ? '/import?mode=psd2' : '/import?mode=bank') } - if (!state.path) { - return ( - - - {t('title')} - {t('description')} - - - {(['migration', 'bank', 'fresh'] as const).map((path) => { - const Icon = pathIcons[path] - return ( - - ) - })} - - - ) - } - - const primaryHref = state.path === 'migration' - ? (hasMigration ? '/import?mode=migration' : '/import?mode=sie') - : (hasBanking ? '/import?mode=psd2' : '/import?mode=bank') - const PrimaryIcon = pathIcons[state.path] + const activeStep = !step1Done ? 1 : !step2Done ? 2 : !step3Done ? 3 : 4 return ( - - -
-
- {t(`${state.path}_selected_title`)} - {t(`${state.path}_selected_description`)} +
+
+

{t('title', { count: hasSkatteverket ? 4 : 3 })}

+ +
+ +
+ +

+ {t('step_books_description')}{' '} + {' '} + {t('step_books_fresh_suffix')} +

+
+ + + + + + {t('step_books_sie')} + +
+
+ + +

{t('step_bank_description')}

+
+ + {hasBanking && ( + + )}
- -
- - - -
- {hasSkatteverket && !hasSkatteverketConnected && ( - // The authorize endpoint redirects off-site to Skatteverket. - // eslint-disable-next-line @next/next/no-html-link-for-pages - - - {t('optional_skatteverket')} - +

{t('step_skv_description')}

+
+ + +
+ + )} + + +

{t('step_assistant_description')}

+
+ +
+
+
+
+ ) +} + +/** One numbered step on the thread: dot + hairline down to the next step. */ +function Step({ + number, + done, + active, + title, + badge, + last = false, + children, +}: { + number: number + done: boolean + active: boolean + title: string + badge?: string + last?: boolean + children: React.ReactNode +}) { + return ( +
+ {!last && ( +
+
+ ) +} + +/** Tiny partner mark: real logo on a white chip; `mono` darkens a white + * source logo in light mode and lifts it in dark (Enable Banking). */ +function LogoMark({ src, name, mono = false }: { src: string; name: string; mono?: boolean }) { + return ( + + {/* eslint-disable-next-line @next/next/no-img-element */} + {name} + ) } diff --git a/messages/en.json b/messages/en.json index 906a6a7b..4f989385 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1132,26 +1132,24 @@ "security_note": "Your data is encrypted and stored securely in Sweden" }, "initial_setup": { - "title": "How would you like to get started?", - "description": "Choose the path that gets you to value fastest. You can switch or do the rest later.", - "migration_title": "Move existing books", - "migration_description": "Import from another system or upload a SIE file.", - "bank_title": "Connect your bank", - "bank_description": "Fetch transactions automatically or import a bank statement.", - "fresh_title": "Start from scratch", - "fresh_description": "Continue to the overview without prior bookkeeping.", - "migration_selected_title": "Move your books", - "migration_selected_description": "Import your history first. The overview stays available while you work.", - "migration_action": "Open import", - "bank_selected_title": "Connect your bank", - "bank_selected_description": "Fetch transactions so you can start reconciling and bookkeeping.", - "bank_action": "Connect or import bank", - "fresh_selected_title": "You are ready", - "fresh_selected_description": "Start working directly from the overview.", - "fresh_action": "Continue", + "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?", + "step_books_fresh_link": "Start from scratch", + "step_books_fresh_suffix": "instead.", + "step_books_action": "Move the books", + "step_books_sie": "+ SIE", + "step_bank_title": "Connect the bank", + "step_bank_description": "Fetch transactions automatically, or import a bank statement.", + "step_bank_action": "Connect the bank", + "step_assistant_title": "Build your bookkeeping assistant", + "step_assistant_beta": "Beta", + "step_assistant_description": "A few questions about your business calibrate an assistant that suggests bookkeeping for you.", + "step_assistant_action": "Get started", "dismiss": "Hide", - "optional_skatteverket": "Connect Skatteverket, optional", - "optional_assistant": "Create assistant, optional" + "step_skv_title": "Connect Skatteverket", + "step_skv_description": "See the tax account and file VAT and employer declarations right from here. Connects with BankID in a couple of minutes.", + "step_skv_action": "Connect Skatteverket" }, "tax_assessment_notices": { "title": "Final tax notices and remaining tax", diff --git a/messages/sv.json b/messages/sv.json index 2f983b58..add4e410 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -1132,26 +1132,24 @@ "security_note": "Din data är krypterad och lagras säkert i Sverige" }, "initial_setup": { - "title": "Hur vill du komma igång?", - "description": "Välj den väg som ger dig första nyttan snabbast. Du kan alltid byta eller göra resten senare.", - "migration_title": "Flytta befintlig bokföring", - "migration_description": "Importera från ett annat system eller ladda upp en SIE-fil.", - "bank_title": "Koppla banken", - "bank_description": "Hämta transaktioner automatiskt eller importera ett kontoutdrag.", - "fresh_title": "Starta från början", - "fresh_description": "Fortsätt direkt till översikten utan tidigare bokföring.", - "migration_selected_title": "Flytta in din bokföring", - "migration_selected_description": "Importera historiken först. Översikten är tillgänglig under tiden.", - "migration_action": "Öppna importen", - "bank_selected_title": "Koppla din bank", - "bank_selected_description": "Hämta transaktionerna så att du kan börja stämma av och bokföra.", - "bank_action": "Koppla eller importera bank", - "fresh_selected_title": "Du är redo", - "fresh_selected_description": "Börja arbeta direkt i översikten.", - "fresh_action": "Fortsätt", + "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?", + "step_books_fresh_link": "Starta från början", + "step_books_fresh_suffix": "i stället.", + "step_books_action": "Flytta bokföringen", + "step_books_sie": "+ SIE", + "step_bank_title": "Koppla banken", + "step_bank_description": "Hämta transaktioner automatiskt, eller importera ett kontoutdrag.", + "step_bank_action": "Koppla banken", + "step_assistant_title": "Bygg din bokföringsassistent", + "step_assistant_beta": "Beta", + "step_assistant_description": "Några frågor om din verksamhet kalibrerar en assistent som föreslår bokföring åt dig.", + "step_assistant_action": "Kom igång", "dismiss": "Dölj", - "optional_skatteverket": "Anslut Skatteverket, valfritt", - "optional_assistant": "Skapa assistent, valfritt" + "step_skv_title": "Anslut Skatteverket", + "step_skv_description": "Se skattekontot och lämna moms- och arbetsgivardeklarationer direkt härifrån. Ansluts med BankID på ett par minuter.", + "step_skv_action": "Anslut Skatteverket" }, "tax_assessment_notices": { "title": "Slutskattebesked och kvarskatt",