import React from 'react'; import { observer } from 'mobx-react-lite'; import { Box } from '@wix/design-system'; import { SetupWidgetState, SetupWidgetStep } from '@wix/bex-core'; import { useWixPatternsContainer } from '@wix/bex-core/react'; import { CardContainer } from '../CardContainer'; import { useIsMobile } from '../../hooks/useIsMobile'; import { useIntersectionObserver } from '../../hooks/useIntersectionObserver'; import { SetupWidgetHeader } from './SetupWidgetHeader'; import { SetupStepRow } from './SetupStepRow'; import { SetupStepCard } from './SetupStepCard'; import { SetupStepGroup } from './SetupStepGroup'; import { AllCompleteState } from './AllCompleteState'; import { SetupWidgetSkeleton } from './SetupWidgetSkeleton'; export interface SetupWidgetProps { /** * A Setup widget state instance created with `useSetupWidget`. The widget * owns loading / error / refetch; the state exposes the steps and a public * API to change a step's status. */ state: SetupWidgetState; /** * Inline-expansion content for a step, supplied at render time (the data does * not carry it). Returned for the currently-expanded inline step. */ renderStepContent?: (step: SetupWidgetStep) => React.ReactNode; /** Fired when a `navigate` step (or its CTA) is activated. */ onStepClick?: (step: SetupWidgetStep) => void; /** * Fired when a *completed* step's CTA (or row) is activated — a distinct * action from `onStepClick` (mirrors `onCompletedCtaClick` in dashboard-setup, * which targets the step's separate `completedCta`). */ onCompletedStepClick?: (step: SetupWidgetStep) => void; /** * Fired once when a step (or group) scrolls into view — for the consumer to * report view telemetry (BI/dealer). The widget itself stays * telemetry-agnostic. Click telemetry hooks into `onStepClick` / * `onCompletedStepClick`. */ onStepView?: (step: SetupWidgetStep) => void; /** Fired when the "all complete" celebratory CTA is clicked. */ onAllCompleteClick?: () => void; /** * Replaces the default error UI when the initial load fails. Receives a * `retry` callback (re-runs the load) and the thrown `error`. When omitted, a * standard error card with a "Try again" retry is shown. */ renderError?: (params: { retry: () => void; error: unknown; }) => React.ReactNode; /** Applies a data-hook attribute for tests. */ dataHook?: string; } // Fires `onView` once when the wrapped step scrolls into view. Only mounted // when the consumer passes `onStepView`, so view tracking adds no DOM/observer // overhead otherwise. const StepViewTracker = ({ stepId, onView, children, }: { stepId: string; onView: () => void; children: React.ReactNode; }) => { const { elementRef } = useIntersectionObserver(onView); return (
} data-hook={`setup-step-view-${stepId}`} > {children}
); }; const _SetupWidget = ({ state, renderStepContent, onStepClick, onCompletedStepClick, onStepView, onAllCompleteClick, renderError, dataHook, }: SetupWidgetProps) => { const { translate: t } = useWixPatternsContainer(); const isMobile = useIsMobile(); const { status } = state._loadTask.status; const activateStep = (step: SetupWidgetStep) => { // Completed steps fire their own action (never expand) — distinct from the // active/navigate `onStepClick`. if (step.status === 'completed') { onCompletedStepClick?.(step); return; } if (step.expansionMode === 'inline') { state._toggleStep(step.id); } else { onStepClick?.(step); } }; // Children of an aggregated group are leaf steps — they never toggle/expand; // completed children fire the completed action, the rest navigate. const activateChild = (child: SetupWidgetStep) => { if (child.status === 'completed') { onCompletedStepClick?.(child); } else { onStepClick?.(child); } }; const renderLeaf = (step: SetupWidgetStep, isChild = false) => { const expanded = !isChild && step.expansionMode === 'inline' && state._expandedStepId === step.id; const onActivate = () => isChild ? activateChild(step) : activateStep(step); // A single step is focused/highlighted at a time — the expanded step when // one is open, otherwise the primary. (Highlighting `primary || expanded` // would mark two rows once the user opens a non-primary inline step.) const isFocused = !isChild && step.id === state._focusedStepId; return isMobile ? ( ) : ( ); }; // All-complete is a distinct success surface with its own card. if (status === 'success' && state._isAllComplete) { return ( ); } // Built-in loading skeleton, shaped like the widget (desktop card / mobile // bare) so there's no layout jump when data resolves. if (status === 'idle' || status === 'loading') { return ; } // Consumer-supplied error UI fully replaces the default error card. if (status === 'error' && renderError) { return ( <> {renderError({ retry: () => state._retry(), error: state._loadTask.errorStatus?.error, })} ); } const header = ( ); const list = ( {state.steps.map((step) => { const node = step.steps?.length ? ( state._toggleStep(step.id)} renderChild={(child) => renderLeaf(child, true)} /> ) : ( renderLeaf(step) ); return onStepView ? ( onStepView(step)} > {node} ) : ( {node} ); })} ); const body = ( <> {header} {list} ); // Mobile success renders bare — no outer card (matches the mobile design; // the consumer composes the surrounding page). Loading / error still flow // through CardContainer so the widget owns those states everywhere. if (isMobile && status === 'success') { return ( {body} ); } return ( state._retry(), }, }} > {body} ); }; export const SetupWidget = observer(_SetupWidget);