feat: migrate the vendor setup wizard to React (flat-array settings architecture) - #3325
feat: migrate the vendor setup wizard to React (flat-array settings architecture)#3325MdAsifHossainNadim wants to merge 28 commits into
Conversation
…at-array' into feat/vendor-onboarding-react
…at-array' into feat/vendor-onboarding-react
…boarding-react Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the flat-array settings architecture
Steps render through plugin-ui's <Settings> engine from server-bootstrapped
flat-array schemas (store/payment in Lite; Pro injects fields and steps via
the new dokan_setup_wizard_schema filter). Saves go through
PUT /dokan/v1/vendor-onboarding/{store,payment}: WizardStoreSaver keeps byte
parity with the legacy save (Seam A suppressed, the legacy profile-completion
math replayed) while firing Seam B, and SetupWizardCompat replays the legacy
wizard actions with the $_POST bag Pro's handlers read. The legacy step views
are removed — the shell (sticky topbar, progress rail, viewport-bounded card
with scroll-driven edge shadows) stays PHP with real per-step navigation.
Rides along: settings-page fixes surfaced by this work — REST validation
errors re-keyed to the engine's dependency_key contract, and a shim for
plugin-ui v2's literal dependency-key matching that silently hid every
same-section dependent field.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…nd' into feat/vendor-onboarding-react
…boarding-react # Conflicts: # includes/REST/Manager.php
…eact Restores the legacy setup wizard alongside the React one and puts an admin switcher in front of both, mirroring the Vendor Store Settings contract. New option `dokan_appearance['vendor_setup_wizard']` (legacy|latest): - `LegacySwitcher::is_setup_wizard_legacy_preferred()` — same shape as `is_store_settings_legacy_preferred()`. - Legacy admin settings: `dokan_appearance`, right after `vendor_store_settings`. - New admin settings: `vendor_dashboard_section` (Appearance → Vendor Panel), right after `vendor_store_settings`. - `FullWidthVendorLayout::update_layout_style()` flips it to `latest` on the admin setup wizard, so the stored default keeps upgraded sites on the legacy wizard while fresh installs onboard on React. `SetupWizard::use_react_wizard()` resolves the preference once per request and gates the chrome plus every step. The legacy header/logo, the parent's step pills, the "Return to the Marketplace" footer, the introduction, the store form (with its four in-form extension hooks), the payment gateway callback loop and the Ready screen all come back, and `dokan_setup_payment_save()` is re-wired as the payment handler — inert in React mode, which saves over REST and never posts `save_step`. The React chrome was scoped to `body.dokan-vendor-setup-wizard`, a class the legacy body also carries; it now hangs off a `dokan-vsw` marker the React shell alone adds, and the legacy map-wrapper styles are restored. Pro needs no change: its verification step already falls back to the legacy template whenever the React bundle isn't registered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lidation The store step's Next resolved without saving when nothing was touched, so the schema's server-side validate_address() never ran and a vendor could walk past the required address with an empty form. Every Next now saves, touched or not. Required subfields carry a "(Required)" badge and each reports "This field is required" under its own input, instead of stacking the server's messages into one run-on paragraph beneath the whole block. The badged list and validate_address() now read a single source, which also closes a real drift: State was badged required for the 39 countries WooCommerce lists with no states of their own, where the save accepts it empty. Payment drops its required-field validation — the step can be completed later from the dashboard, so a half-filled bank form is accepted and only a malformed email is rejected. Also fixes the SmartSelect popup's doubled search-box border, the card and popup scrollbars, and toast icons that took no state colour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A pending row offered only Cancel, so correcting a wrong upload meant withdrawing the request and starting over. Edit reopens the same upload panel. Vendors cannot amend an existing request: VerificationRequestsApi applies `documents` only for manage_options and pins a vendor-sent status to cancelled. Edit therefore submits a new request and retires the old one — the replacement is created first, so a failure retiring the previous row can't cost the vendor their submission. Also shares the "(Required)" badge with the address field rather than repeating its markup. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every step used to be a real page load. The React wizard now bootstraps all steps at once and swaps them client-side; only saves hit the network. The legacy wizard is untouched and still navigates per step. - `bootstrap_all_steps()` walks each step inside its own `$_GET['step']` context and fires `dokan_setup_wizard_enqueue_scripts`, so Pro's step-gated assets (stripe-express, vendor-verification) all load in the one request. `$_GET['page']` never changes, so mangopay's opt-out holds. - `enqueue_react_step()` accumulates into a keyed registry instead of overwriting one global, so Pro contributes its step without any change. - A `Wizard` shell owns the step, keeps `?step=` honest via pushState and restores it on popstate; the landing entry is stamped with replaceState so Back can't strand the wizard. `ProgressRail` re-renders per step and claims the host so PHP's server-rendered first frame isn't duplicated. - `frontend_enqueue_scripts()` splits into react/legacy paths plus `current_step_payload()`; the rail markup moves to an overridable `templates/vendor-setup-wizard/progress-rail.php`. Fixes found while verifying: - Steps remounted from their page-load snapshot, so a saved store step came back empty and a submitted verification reverted to "Start Verification". Both now write the server's answer back into the bootstrap. - The bank attestation was stripped on save and never hydrated, so it could not round-trip. It persists in the legacy `'on'` shape the dashboard form, its validation and the withdraw-log export all read. - The "Creating your Store" overlay had no page load to end it. It now covers the real save and clears when the request settles. - Back/Skip take the secondary button's border and fill on hover. Restores the legacy `setup_wizard_message` default; `default_wizard_message()` stays the React intro's fallback. Comments trimmed to single lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Enqueue callbacks that print (Pro's media templates, a gateway's inline script) ran before the document opened, and the SPA fires that action once per step, so every wizard page began with ~165 KB of markup ahead of the doctype (quirks mode) and carried three copies of the media templates. That output is now captured, de-duplicated and replayed inside the body. The SPA also stepped over any registered step that bootstrapped no React payload -- an older Pro's verification view, a third-party step -- so the vendor could reach a dead end. Every step now carries a server-minted URL and the shell hands the ones it cannot mount a real page load. Also in this pass: - The rail and the SPA counted different step sets; both read wizard_step_order() now. The rail stops being an overridable template, since React empties that node the moment it mounts. - The React gate checks the built bundle exists, so a checkout without one falls back to the legacy wizard instead of rendering an empty step. - New dokan_setup_wizard_step_payload filter: the seam Pro uses to drop the store step's Skip when a verification method is required. - Payment marks the bank fields required again (badges only, the step stays optional) and gives every active method with a callable callback that no schema describes a row linking to the dashboard payment settings, so Paystack and friends stop vanishing from onboarding. - Verification pre-fills the uploader only from a pending submission, so a withdrawn or rejected document no longer reappears. - Drops the payload keys the SPA no longer reads and rewords the two step descriptions that read as machine-written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lite no longer knows Pro's step names. Each step records the payload key it bootstrapped under as it registers, so `payload_step_map()` and its unused `dokan_setup_wizard_payload_step_map` filter are gone, `current_step_payload()` reads a `payload` callback off the step definition instead of switching on hardcoded names, and the SPA shell is emitted last on the enqueue action, once every step -- Lite's, Pro's, a third party's -- has had its say. On a `step=verifications` load the bootstrap loop served the step and the outer action pass then re-entered Pro's builder, printing the same payload JSON twice. The loop now skips the step being viewed, which the outer pass serves in its own real context, and step registration is idempotent. `StoreStep`, `PaymentStep` and `VerificationStep` were three wrappers around one engine shell; `SchemaStep` now renders anything carrying a schema and treats a missing endpoint as nothing to save, so a Pro or third-party schema step needs no Lite release. Their field variants moved into one `register-fields.ts`. Steps are keyed on mount, since they share a component and each has to seed from its own payload. Smaller cleanups: - `shell.baseUrl`/`shell.nonce` dropped: pushState uses the same server-minted step URL the no-payload fallback navigates to. - `centred` rides on the step order, instead of the same rule living in PHP and TS under two different key vocabularies. - LegacySwitcher's three identical preference bodies collapse into `is_surface_legacy_preferred( $option_key )`. - SetupWizardCompat's two near-identical action firers collapse into `fire()`. - `WC()->countries->get_states()` instead of a fresh `WC_Countries`, which re-parses WooCommerce's 96 KB states file on every call. - One step nonce per request instead of six, one asset-path helper instead of two literals, and out go a dead `file_exists` guard, an unused `nextLabel` prop, the `isNextReady` context field and a hand-rolled `fieldKeyOf`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cher The wizard's single page load rests on seams a browser only shows indirectly: one bootstrap pass that registers every step, one step order both the PHP rail and the SPA count from, one payload per step. A drift there surfaces as a wizard that skips a step or mounts an empty one, so each is pinned: - the step order carries key, numbering, centring and a nonce-bearing URL, and every link shares one nonce - a step declares the payload key it bootstrapped under, and registers once even when the enqueue action reaches it twice - `dokan_setup_wizard_step_payload` can tune a step - a step with no payload keeps its own key and URL, so the SPA can hand it a page load instead of stepping over it - the bootstrap pass skips the step being viewed and restores `$_GET['step']` - deferred enqueue-time markup is deduped and replayed once - the shell carries the order and the landing step, falling back to the step key while a step is still unregistered The switcher gets its own cover: stored default is legacy, `latest` opts in, an unknown value stays legacy, anonymous contexts get the new UI, each surface reads its own `dokan_appearance` key, and a checkout with no built bundle falls back to the legacy wizard. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e files The missing-bundle case renamed the built manifest on disk and restored it in a `finally` — a crashed run would have left every site on that checkout silently on the legacy wizard. `asset_manifest_path()` now resolves through late static binding, so the test points a subclass at a path that doesn't exist instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review verdict: ✅ ApproveReviewed at What this shipsThe vendor setup wizard (
How the risky parts are handledRollout is opt-in. One page load, without breaking the extension surface. Steps swap client-side, but Nothing gets stepped over. A step that bootstraps no React payload — an older Pro's verification view, a third-party step — keeps its own key and a server-minted URL, and the shell hands it a real page load. Verified against a Pro checkout without the companion PR: HTTP 200, zero fatals, legacy template renders inside the new chrome, flow continues. Steps declare themselves. Legacy hooks keep working. Seams are deliberate, and pinned. A pre-existing bug fixed on the way. Enqueue callbacks that print (Pro's media templates, a gateway's inline script) ran before the document opened, so every wizard page began with ~165 KB of markup ahead of the doctype, in quirks mode. That output is captured, de-duplicated and replayed once inside the body: 169,450 bytes → 4 before the doctype, 3 media-template copies → 1, page 275 KB → 162 KB. Deliberate product decisions worth knowing
Verification
Before QA sign-offThe Playwright coverage (#3326) has not been executed against an env running this branch — it's written and typecheck-clean, but one green run is the last gap. Recommend The red check here is infrastructure: Review performed with Claude Code. |
Review — feat: migrate the vendor setup wizard to React (#3325)Verdict: ✅ Approve, with 3 changes requested (all small, contained fixes) — and one 🔴 release-blocking base-branch issue (Store Categories missing on all vendor surfaces) that must be root-caused and fixed before this stack is finally merged to Reviewed at Scope & method
Test results — 32/32 checks greenFlow (headed, live): React chrome + "Step N of 4" rail → intro card (brand, admin copy, Skip exits to the real dashboard) → Store step: required badges, Pro Store Category field, show-email switch, map correctly gated out (no API key) → empty-state save rejected HTTP 400 with "State is required." → complete save 200 → client-side advance to Payment (no reload) → PayPal/Bank/Skrill accordions, Paystack correctly absent (module inactive), malformed PayPal email 400 with inline error, half-filled bank accepted by design → Verification as a React schema step: wp.media upload → POST /verification-requests 201 → "Pending review" chip → Ready card → browser Back/Forward drive the SPA with rail in sync → re-entry shows saved values → required verification method removes Skip (Pro lock) → legacy switch restores the old wizard, then back. Zero console errors. Database (vendor 37 after the run):
Manual verification: the legacy wizard was exercised by hand with UK + empty state — confirming the legacy form's client-side gate blocks it there too (screenshot), which settles that the new server rule is correct legacy parity (see finding 1 for what still needs fixing). Requested changes1. WARNING — State field: server enforces what the UI doesn't markLocation: 2. WARNING — Internal callables leak into payloads and REST responsesLocation: 3. WARNING —
|


What
Migrates the vendor setup wizard (
?page=dokan-seller-setup) to React on the flat-array settings architecture, behind an admin switch that keeps upgraded sites on the legacy wizard. Storage stays byte-identical with the legacy wizard for every owned key indokan_profile_settings— only the rendering and the save transport change.Plan + scope decisions: getdokan/plugin-internal-tasks#2130. Pro companion: the sibling
feat/vendor-onboarding-reactPR on dokan-pro.Related Pull Request(s)
Closes
Architecture
dokan_appearance['vendor_setup_wizard'](legacy|latest) mirrors the Store Settings switcher: the stored default keeps an upgraded site on the legacy wizard, andFullWidthVendorLayout::update_layout_style()flips it tolateston the admin setup wizard so fresh installs onboard on React.SetupWizard::use_react_wizard()resolves it once per request and gates the chrome and every step — and also checks the built bundle exists, so a checkout without one falls back to legacy rather than rendering an empty step.setup_wizard_header/footer) and its per-step URLs (?step=…&_admin_sw_nonce=…), but the React steps swap client-side:bootstrap_all_steps()walks every registered step inside its own$_GETcontext — which is how Pro's step-gated enqueues (stripe-express, vendor-verification) still fire — and each step bootstraps its payload up front.pushStatekeeps the URL honest, Back/Forward move the wizard, and a step that bootstrapped no payload (an older Pro's verification view, a third-party step) gets a real page load instead of being stepped over.enqueue_react_step()records the payload key a step registered under, so nothing in Lite hardcodes a Pro step name; the shell is emitted last on the enqueue action, once every step has had its say. Step definitions gained apayloadcallback besideview/handler.wp_add_inline_script— the page render is the only context where the wizard's$_GETstate is faithful.SchemaStep.tsxrenders any schema step through plugin-ui's<Settings>(hookPrefixdokan_vendor, same variant registry as the vendor Store Settings page) and drives the save from the wizard footer; a payload with no endpoint simply has nothing to save. Only the intro and ready cards are bespoke, so a Pro or third-party schema step needs no Lite release.VendorOnboardingController extends VendorStoreSettingsController(PUT /dokan/v1/vendor-onboarding/{store|payment}) — it inherits the sanitize/validate pipeline and swaps only the schema source and the persistence seam (save_slice()).WizardStoreSaverwrites the store slice with Seam A (dokan_store_profile_settings_args) suppressed — the legacy wizard never applied it — while Seam B (dokan_store_profile_saved) fires, soVendorCacheand 15 Pro consumers behave exactly as after a legacy save. It also replays the legacy address profile-completion math. The payment save folds gateway values over untouched keys, awards the legacy completion weight, and fires Seam B (which the legacy payment save never did — stale-cache fix).SetupWizardCompatre-firesdokan_seller_wizard_{store|payment}_field_savewith a hydrateddokan()->seller_wizardand the legacy$_POSTbag (legacy_post_keyfields), so Pro's existing handlers (store categories, Skrill, …) keep working unchanged.dokan_setup_wizard_schema( $elements, $vendor_id, $step ); a step's chrome is tuned viadokan_setup_wizard_step_payload( $payload, $step, $store_id )(Pro drops the store step's Skip when a verification method is required); third-party steps enqueue the registereddokan-vendor-setup-wizardhandle with their own payload through the publicSetupWizard::enqueue_react_step()/get_step_link().File structure
Riding along
Only
store/fields/AddressFields.tsx(SmartSelect country/state, schema-driven part order/columns, required markers) — the wizard's store step and the settings page share the address field, so it moves once for both.The two plugin-ui v2 regressions this work surfaced live on the base branch (#3310), not here.
How to test
npm run build, then set Admin → Dokan → Appearance → Vendor Panel → Vendor Setup Onboarding to New UI (fresh installs get it automatically).?page=dokan-seller-setup.(Required)markers, category chips (Pro), map (with a Maps key), show-email switch. Next saves and lands on Payment; re-entering shows saved values. Verify meta:wp user meta get <id> dokan_profile_settings—address,location,find_address,show_emailbyte-match input.npm run phpunit -- --filter "SetupWizard|VendorOnboardingController"→ 34 tests.Compatibility
dokan_setup_store_save()/dokan_setup_payment_save()— stays intact behind the switch, and the golden test pins the new writer against it.Changelog
Before: the vendor setup wizard rendered legacy PHP forms, saved through the
$_POSTpipeline, and printed enqueue-time markup ahead of the doctype.After: vendors onboard through a React wizard on the flat-array settings schema, saving over
PUT /dokan/v1/vendor-onboarding/{store|payment}. Admins choose the UI in Appearance → Vendor Panel; upgraded sites keep the legacy wizard until they opt in.🤖 Generated with Claude Code