refactor(ui): drive ConfigureSSO wizard navigation with a state machine#8715
Draft
iagodahlem wants to merge 19 commits into
Draft
refactor(ui): drive ConfigureSSO wizard navigation with a state machine#8715iagodahlem wants to merge 19 commits into
iagodahlem wants to merge 19 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: d88d8ea The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@clerk/astro
@clerk/backend
@clerk/chrome-extension
@clerk/clerk-js
@clerk/expo
@clerk/expo-passkeys
@clerk/express
@clerk/fastify
@clerk/hono
@clerk/localizations
@clerk/nextjs
@clerk/nuxt
@clerk/react
@clerk/react-router
@clerk/shared
@clerk/tanstack-react-start
@clerk/testing
@clerk/ui
@clerk/upgrade
@clerk/vue
commit: |
… part Introduce the React primitives the ConfigureSSO state-machine cutover builds on, without changing the live wizard yet: - useMachine(facts): owns the reducer state and feeds the pure reducer the current facts at dispatch time via a render-updated ref (no effect-based facts sync), keeping dispatch identity stable across server-state refetches. - useSubmitRunner: centralizes the submit lifecycle (clear error -> loading -> advance/jump via NEXT/GOTO or surface error -> idle), building the step ctx from context + the machine dispatch. - Step.Footer.Submit: composed Continue button wired to the runner; owns its in-flight state from the card. - WizardMachineProvider/useWizardMachine: a single sibling context that exposes the machine to steps, footer, and header. - Wizard gains an onComplete terminal hook so a nested flow can bubble its last step into a host state machine instead of a parent wizard. Thread primaryEmailAddress through the data hook + context so the runner can derive the connection name without each step calling useUser.
Cut the live wizard over from the imperative <Wizard> engine to the pure state
machine. Top-level navigation is now reduce/initialState from the reducer, the
ordered STEPS from transitions, and the bodies from STEP_BODIES; steps no longer
own routing.
- ConfigureSSO.tsx mounts useMachine(facts) and renders STEP_BODIES[current]
inside the existing ProfileCard/NavBar chrome, via WizardMachineProvider. The
step-change error effect is gone (the runner clears the card error per submit).
- The header reads the machine + transitions: visible steps are the enabled
steps minus select-provider, completion stays positional (behavior-equivalent
to the old breadcrumb), current is machine.current, clicks dispatch GOTO.
- Simple steps (select-provider, test, confirmation) use Step.Footer.Submit +
the runner; Previous dispatches BACK; confirmation's reconfigure dispatches
GOTO configure and reset dispatches RESET.
- submitSelectProvider returns { ok: true, goTo: 'configure' }: a successful
create flips hasConnection, disabling select-provider, so a plain NEXT would
no-op; GOTO is required.
- Nested-delegating steps (verify-domain, configure) keep their inner <Wizard>
untouched; only their terminal step advances the machine via an injected
onComplete.
Re-point the select-provider tests at the machine dispatch and the submit test
at the new goTo result.
deriveInitialStep (and its test) are dead now that the machine's initialState is the sole authority for where the wizard mounts on (re)load; initialStepId no longer lives on the context. The ResetCardErrorOnStepChange effect sentinel is also gone — the submit runner clears the card error per submit.
6e9a085 to
5524ad3
Compare
…ed primitive
Replace the effect-timed step registration with a domain-agnostic state
machine whose step graph is derived synchronously from <Wizard.Step> children
during render, so the active step is always resolved against a known graph (no
inconsistency window).
- useWizardMachine(config) takes derived step descriptors + a named-guards
record ({ [name]: () => boolean }) and returns the nav surface; guards are
evaluated in the hook layer before dispatch. No domain types leak in.
- The pure reducer (NEXT/PREV/GOTO/RESET) reads the descriptors + guards as an
argument, kept fresh via a render-updated ref so dispatch identity stays
stable.
- useWizard() is now the only consumer surface: goNext / goPrev / goToStep /
reset + the derived current / activeSteps / isFirstStep / isLastStep. The
machine is an internal detail of the primitive.
- <Wizard.Step> carries id / label / guard / enabled / terminal / hidden as
props; children-derivation descends through fragments and intermediate
elements, stopping at nested-wizard and component boundaries.
- Remove the old parallel machine (per-component reducer + machine context +
the layout-effect step registry + the stepBodies/transitions tables).
Wire ConfigureSSO onto the generic Wizard primitive: a new ConfigureSSOSteps component declares the five top-level steps (verify-domain, select-provider hidden, configure, test, confirmation) as <Wizard.Step> children with a named guards record sourced from the existing derived facts. The step graph is now the JSX, replacing the stepBodies map + transitions table. - ConfigureSSO renders <ConfigureSSOSteps/> inside the existing providers; the loading gate stays one level up. - Steps, the breadcrumb, the submit runner, and the reset dialog consume useWizard() only (goNext / goPrev / goToStep / reset) — the state machine is hidden behind the primitive. - Each SAML provider sub-flow now hosts its own nested <Wizard> with statically declared steps so the graph is derivable at that boundary; the configure body mounts the provider component and passes onComplete through to bubble the inner terminal step into the top-level wizard.
Add a pure, tech-agnostic domain module (no React, no hooks) holding the connection-level predicates the ConfigureSSO wizard derives state from: isActive, hasMinimumConfiguration, getProvider, isOwnedByOtherOrganization. Connection facts live here; user/session and test-run facts are composed in the data hooks instead. Unit-tested in isolation.
…tionMutations Move the mutations hook into hooks/ and rename it (and its EnterpriseConnectionMutations type) to drop the wizard-specific naming. The surface is connection-domain only — no wizard/step/navigation concepts — so it can be reused for custom self-serve SSO flows. Every mutation stays useReverification-wrapped. Update all importers.
Move the test-run probe into hooks/ and drop "controller" from the name. The hook becomes the single source of test-run state and now exposes both an initial isLoading (no data yet → full skeleton) and an isFetching signal for background refetches that keep previously-loaded data visible. The Test step's own paginated list is unchanged here; folding it onto this source is a follow-up.
Compose the enterprise-connection flow into one SDK-grade hook: the source query (the single swappable seam for a future endpoints migration), the pure domain predicates, the reverification-wrapped mutations, and the test-run state. It returns the connection data, named derived state, mutations, and a test-run refresh handle — never an opaque facts bag. Delete deriveFacts and the old data/ hook; fold their logic into the domain module + this hook. Retire machine/guards.ts: the wizard guards are now trivial boolean reads sourced from the hook's derived state via context. The context keeps its shape, now fed by this hook. The deriveFacts unit coverage moves into the domain module's tests (including the verified-gated domain-taken predicate).
The Test step ran its own paginated test-run fetch while the umbrella hook also fetched a success probe, so test-runs were fetched in two places. Fold both onto useEnterpriseConnectionTestRuns as the single source: it now owns the success probe, the paginated list the table renders, and the page cursor. The Test step reads rows, totalCount, polling, page, and the table-level loading flag from context and no longer fetches anything itself. Loading is split into two signals: the cold isLoading drives the full skeleton on initial load (the global fetch covers the test-runs), and the list isFetching drives only the table spinner on re-entry while keeping previous rows visible. To honor "initial-load landing on the test step does not refetch, but navigating in later does", the generic Wizard now exposes isInitialStep, backed by a latched hasNavigated flag in the reducer (more robust than history length, which a back-step to the mount step would reset). The step fires a one-shot, mount-only refresh when isInitialStep is false.
A nested wizard's terminal goNext now bubbles to the parent wizard's goNext automatically — the nested wizard already holds the parent in context, so the explicit onComplete callback was redundant. Remove onComplete from the Wizard primitive and its callsites (the configure sub-flow, the verify-domain inner flow, and the four SAML provider sub-flows). A top-level wizard with no parent treats a terminal goNext as a no-op.
Delete the central submit runner and the pure submit use-cases that only existed to feed it. The footer Continue is a plain button again; each step owns a local handleContinue (clear error → set loading → await the context mutation → advance → catch via handleError → settle to idle) and passes it to the footer. SelectProviderStep creates the connection and jumps to configure (goToStep, since the create disables select-provider). TestConfiguration gates Continue on a successful test run, surfacing the inline validation message when none exists. The four SAML provider sub-flows already used this shape. Navigation stays on the useWizard facade; the state machine is never touched directly by a step.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
<ConfigureSSO />'s wizard had accreted four structural problems that make it hard to extend and reason about — and would calcify as more providers (Google, Entra, OIDC) and DNS/TXT domain verification land:useUser/useSession/test-run reads were scattered across steps;isDomainTakenByOtherOrgwas computed twice; test runs were read three times.useReverificationwas hand-wrapped in some files and missing on others (connection create, the SAML configure submit, test-run create).setError → setLoading → await → advance → catch → setIdle, patched over with a step-change error-reset effect.This refactors the wizard onto a small state machine, with no changes to the public API.
What changed
NEXT/BACK/GOTO/RESETare pure functions of(state, event, facts); the breadcrumb and footer read from it. Steps no longer route — they emit submit intent and the machine decides where to go. Rendering is match-based (no first-frame flashes).factsobject consumed everywhere. The provider never sees a loading state — the skeleton is gated one level up.useSubmitRunnerowns the submit lifecycle; each step composes its own footer (Submit/Previous/Reset) with local state — no footer-action registry. The two SAML configure twins collapse into one shared step.Test plan
pnpm type-checkclean; no new lint findings.Notes
@clerk/uipatch changeset included.