import { useState } from 'react' import { Link } from 'react-router-dom' import { ArrowRight, X } from 'lucide-react' import { dismissOnboarding } from '@/api/onboarding' import type { OnboardingStatus } from '@/api/onboarding' import { useTrialBanner } from '@/hooks/useTrialBanner' import type { TrialBannerStage } from '@/hooks/useTrialBanner' import { useOnboardingStatus } from '@/hooks/useOnboardingStatus' /** * Next-step card — surfaces the single highest-priority incomplete onboarding * item with a primary CTA. Replaces the old multi-item `OnboardingChecklist` * widget at the top of the dashboard. * * `useOnboardingStatusQuery` is exported as a tiny shared hook so the parent * page can decide whether to render the surrounding "Show all setup steps" * toggle without duplicating the fetch. * * Returns `null` when: * - status hasn't loaded yet * - `status.dismissed` is true * - all items are complete * * Priority order (first incomplete wins): * 1. Verify your email * 2. Set up your shop * 3. Run your first FlowPilot session * 4. Connect your PSA * 5. Invite a teammate * 6. Pick a plan (only when trial stage is warning / urgent / expired) */ export interface NextStepItem { /** Stable id used in tests + analytics. */ key: string title: string description: string ctaLabel: string ctaPath: string } const PLAN_GATE_STAGES: ReadonlyArray = [ 'warning', 'urgent', 'expired', ] /** * Pure helper — picks the highest-priority incomplete item, or `null` when * all relevant items are done. Exported for direct unit testing. */ // eslint-disable-next-line react-refresh/only-export-components -- pure helper exported for focused unit tests export function pickNextStep( status: OnboardingStatus, trialStage: TrialBannerStage | null, ): NextStepItem | null { if (!status.email_verified) { return { key: 'verify_email', title: 'Verify your email', description: 'Confirm your address to keep your account active after the grace period.', ctaLabel: 'Verify email', ctaPath: '/verify-email', } } if (!status.shop_setup_done) { return { key: 'shop_setup', title: 'Set up your shop', description: 'Tell us a bit about your team so ResolutionFlow can tailor itself.', ctaLabel: 'Set up shop', ctaPath: '/welcome/step-1', } } if (!status.ran_session) { return { key: 'ran_session', title: 'Run your first FlowPilot session', description: 'Paste a ticket or pick a flow to see ResolutionFlow in action.', ctaLabel: 'Start a session', ctaPath: '/', } } if (!status.connected_psa) { return { key: 'connected_psa', title: 'Connect your PSA', description: 'Sync tickets from ConnectWise, Autotask, or HaloPSA.', ctaLabel: 'Connect PSA', ctaPath: '/account/integrations', } } if (!status.invited_teammate) { return { key: 'invited_teammate', title: 'Invite a teammate', description: 'ResolutionFlow gets stronger when your whole team is on it.', ctaLabel: 'Invite teammate', ctaPath: '/account', } } if (trialStage && PLAN_GATE_STAGES.includes(trialStage)) { return { key: 'pick_plan', title: 'Pick a plan', description: 'Your trial is wrapping up — pick a plan to keep using ResolutionFlow.', ctaLabel: 'Pick a plan', ctaPath: '/account/billing/select-plan', } } return null } export function NextStepCard() { const status = useOnboardingStatus() const [locallyDismissed, setLocallyDismissed] = useState(false) const { stage } = useTrialBanner() if (!status || status.dismissed || locallyDismissed) return null const next = pickNextStep(status, stage) if (!next) return null const handleDismiss = async () => { setLocallyDismissed(true) try { await dismissOnboarding() } catch { // Already hidden locally — best-effort persist. } } return (

Next step

{next.title}

{next.description}

{next.ctaLabel}
) } export default NextStepCard