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);