Skip to content

feat: migrate the vendor setup wizard to React (flat-array settings architecture) - #3325

Open
MdAsifHossainNadim wants to merge 28 commits into
feat/vendor-store-settings-backendfrom
feat/vendor-onboarding-react
Open

feat: migrate the vendor setup wizard to React (flat-array settings architecture)#3325
MdAsifHossainNadim wants to merge 28 commits into
feat/vendor-store-settings-backendfrom
feat/vendor-onboarding-react

Conversation

@MdAsifHossainNadim

@MdAsifHossainNadim MdAsifHossainNadim commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

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 in dokan_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-react PR on dokan-pro.

Related Pull Request(s)

Closes

Architecture

  • Admin switch, legacy by default. dokan_appearance['vendor_setup_wizard'] (legacy|latest) mirrors the Store Settings switcher: the stored default keeps an upgraded site on the legacy wizard, and FullWidthVendorLayout::update_layout_style() flips it to latest on 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.
  • PHP shell + a single page load. The wizard keeps its standalone document (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 $_GET context — which is how Pro's step-gated enqueues (stripe-express, vendor-verification) still fire — and each step bootstraps its payload up front. pushState keeps 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.
  • Steps declare themselves. 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 a payload callback beside view/handler.
  • Schema bootstrapped inline, never fetched. Each step's flat-array schema (with values) is built server-side and attached via wp_add_inline_script — the page render is the only context where the wizard's $_GET state is faithful.
  • One shared engine shell. SchemaStep.tsx renders any schema step through plugin-ui's <Settings> (hookPrefix dokan_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.
  • REST save path. 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()).
  • Persistence seams. WizardStoreSaver writes 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, so VendorCache and 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).
  • Legacy hook compatibility. SetupWizardCompat re-fires dokan_seller_wizard_{store|payment}_field_save with a hydrated dokan()->seller_wizard and the legacy $_POST bag (legacy_post_key fields), so Pro's existing handlers (store categories, Skrill, …) keep working unchanged.
  • Enqueue-time output. Consumers that print while assets are enqueued (Pro's media templates, a gateway's inline script) ran before the document opened — every wizard page began with ~165 KB of markup ahead of the doctype, in quirks mode. That output is now captured, de-duplicated and replayed once inside the body.
  • Extension surface. Fields inject via dokan_setup_wizard_schema( $elements, $vendor_id, $step ); a step's chrome is tuned via dokan_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 registered dokan-vendor-setup-wizard handle with their own payload through the public SetupWizard::enqueue_react_step() / get_step_link().

File structure

includes/
├── Admin/
│   ├── Dashboard/LegacySwitcher.php       # + is_setup_wizard_legacy_preferred() (three surfaces, one body)
│   ├── Settings.php                       # + dokan_appearance.vendor_setup_wizard, wizard welcome default
│   └── Settings/Schema/SettingsSchema.php # + the same switch on Appearance → Vendor Panel
├── Shortcodes/FullWidthVendorLayout.php   # fresh installs opt into the React wizard
├── Vendor/
│   ├── SetupWizard.php                    # React shell + legacy wizard behind the switch
│   ├── SetupWizardCompat.php              # NEW — legacy action replay + $_POST bag overlay
│   └── Settings/
│       ├── Schema/SetupWizardSchema.php   # NEW — store + payment step schemas (values, gates, validators)
│       ├── WizardStoreSaver.php           # NEW — store-slice writer (Seam A off, completion replay)
│       ├── StoreSettingsWriter.php        # + $options['apply_settings_args_filter'] seam
│       └── ValueMapper.php                # fix: registration seeds address as '' — don't cast to [0 => '']
└── REST/
    ├── VendorOnboardingController.php     # NEW — /dokan/v1/vendor-onboarding/{store|payment}
    ├── VendorStoreSettingsController.php  # + get_input_schema()/persist()/save_slice() template seams
    └── Manager.php                        # + controller registration

src/
├── dashboard/settings/store/fields/AddressFields.tsx   # SmartSelect country/state, schema-driven order/columns/required
└── vendor-dashboard/setup-wizard/         # NEW — the wizard bundle (entry: vendor-setup-wizard)
    ├── index.tsx / Wizard.tsx             # mount + the SPA shell (step order, history, body chrome)
    ├── SchemaStep.tsx                     # the engine shell every schema step renders through
    ├── IntroCard.tsx / ReadyCard.tsx      # the two bespoke cards
    ├── register-fields.ts                 # one registry for the wizard's field variants
    ├── PaymentMethodsField.tsx            # `payment_methods` variant — gateway accordion
    ├── VerificationMethodsField.tsx       # `verification_methods` variant — rows, wp.media picker, cancel/re-submit
    ├── VerificationSocialNote.tsx / ProgressRail.tsx / WizardFooter.tsx / CreatingOverlay.tsx
    └── style.scss                         # engine skinning, scroll-driven edge shadows, tokens
assets/src/less/setup.less                 # wizard chrome: sticky topbar, progress rail, bounded card layout

tests/php/src/
├── Vendor/SetupWizardStoreSaveGoldenTest.php   # golden master pinned against the legacy save
├── Vendor/SetupWizardSpaTest.php               # NEW — bootstrap pass, step order, self-registration, shell
├── Vendor/SetupWizardSwitcherTest.php          # NEW — the legacy/React switch and its fallbacks
└── REST/VendorOnboardingControllerTest.php     # differential + payment + permission coverage

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

  1. npm run build, then set Admin → Dokan → Appearance → Vendor Panel → Vendor Setup Onboarding to New UI (fresh installs get it automatically).
  2. As a vendor open ?page=dokan-seller-setup.
  3. Intro — site icon + name, welcome copy (admin-configurable), Skip → dashboard, Start Journey → store.
  4. Store — country/state searchable combobox cascade, city/zip/street with (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_settingsaddress, location, find_address, show_email byte-match input.
  5. Payment — accordion (admin Withdraw icon set); PayPal email, full bank form (required markers, but a half-filled form is accepted — payment can be finished from the dashboard), Skrill (Pro), and a "connect from your dashboard" row for gateways whose flow can't be schema-driven (Paystack, Stripe Express, …). A malformed email is rejected inline.
  6. Verification (Pro) — rows with method descriptions, wp.media upload, Submit → "Pending review" chip, Edit replaces the document in place, Cancel withdraws it and re-opens submission.
  7. Ready — Congratulations, View Site, Explore Dashboard → the sidebar Dashboard target.
  8. Browser Back/Forward move the wizard and keep the URL, rail and chrome in step.
  9. Flip the switch back to Legacy UI — the legacy wizard renders unchanged.
  10. npm run phpunit -- --filter "SetupWizard|VendorOnboardingController" → 34 tests.

Compatibility

  • Old Pro + new Lite: verified — no fatals, and Pro's legacy verification step renders inside the new chrome; the SPA hands it a real page load rather than stepping over it.
  • New Pro + old Lite: Pro's verification step detects the missing bundle registration and falls back to its legacy template (gate kept in Pro deliberately).
  • Legacy mode: the whole legacy wizard — views, in-form extension hooks, 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 $_POST pipeline, 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

MdAsifHossainNadim and others added 4 commits July 16, 2026 16:54
…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>
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 283917c4-47d3-4990-aceb-cd9e7a05ef74

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/vendor-onboarding-react

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

MdAsifHossainNadim and others added 23 commits July 17, 2026 17:15
…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>
@MdAsifHossainNadim

MdAsifHossainNadim commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Review verdict: ✅ Approve

Reviewed at ae4edf6e8, end to end: architecture, the compatibility matrix, security-relevant paths, and a live walk through the wizard in the browser against Lite + Pro.

What this ships

The vendor setup wizard (?page=dokan-seller-setup) becomes a React surface on the flat-array settings architecture, behind an admin switch. Storage is byte-identical with the legacy wizard for every owned key in dokan_profile_settings — only the rendering and the save transport change.

Step Before After
Intro PHP echo React card (branding, admin-configurable copy)
Store <table class="form-table"> + $_POST Schema-driven React, saved over PUT /dokan/v1/vendor-onboarding/store
Payment gateway callback loop Schema-driven accordion, saved over PUT /dokan/v1/vendor-onboarding/payment
Verification (Pro) PHP template React schema step rendered by Lite's engine shell
Ready PHP echo React card

How the risky parts are handled

Rollout is opt-in. dokan_appearance['vendor_setup_wizard'] (legacy|latest) mirrors the Store Settings switcher. The stored default keeps upgraded sites on the legacy wizard — which is restored intact, in-form extension hooks and save handlers included — while FullWidthVendorLayout::update_layout_style() opts fresh installs in. The gate also requires the built bundle, so a checkout without one falls back rather than rendering an empty step.

One page load, without breaking the extension surface. Steps swap client-side, but bootstrap_all_steps() walks every registered step inside its own $_GET context first, which is how Pro's step-gated enqueues (stripe-express, razorpay, vendor-verification) still fire. pushState keeps the URL and nonce honest, and Back/Forward move the wizard.

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. enqueue_react_step() records the payload key a step registered under, so Lite hardcodes no Pro step name; the shell is emitted last on the enqueue action, once every step has had its say.

Legacy hooks keep working. SetupWizardCompat re-fires dokan_seller_wizard_{store|payment}_field_save with a hydrated dokan()->seller_wizard and the legacy $_POST bag, so Pro's existing handlers (store categories, Skrill, …) need no changes.

Seams are deliberate, and pinned. WizardStoreSaver suppresses Seam A (dokan_store_profile_settings_args) — the legacy wizard never applied it — while Seam B (dokan_store_profile_saved) fires, so VendorCache and the Pro consumers behave exactly as after a legacy save. The payment save now fires Seam B too, which the legacy save never did (stale-cache fix). SetupWizardStoreSaveGoldenTest pins all of it against legacy, with the accepted deltas listed in the file.

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

  • Connect-style gateways (Stripe Express, Paystack, Razorpay, PayPal Marketplace, MangoPay) can't be completed inside the wizard — their OAuth flows aren't schema-able. Rather than vanishing as they did in an earlier revision, each active one now gets a row linking to the dashboard payment settings.
  • The payment step is optional. The fields a withdrawal needs are marked (Required), but a half-filled bank form saves and Next proceeds — payment can be finished from the dashboard.

Verification

Check Result
npm run build exit 0
ESLint clean
PHPCS (branch ruleset, incl. DokanFlatShowIf + DokanSettingsRepository) clean on all changed PHP, production and test
PHPUnit 34 tests / 137 assertions green (golden master, REST controller, SPA seams, switcher)
Browser walk intro → store → payment → verification → ready, Back/Forward in sync, zero console errors
Server-side, all five step URLs 200, doctype at byte 4, media templates once, no duplicate payloads, no fatals
Old Pro + new Lite 200, no fatals, legacy verification step renders

Before QA sign-off

The 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 npx playwright test tests/e2e/vendor-setup-wizard --grep @lite.

The red check here is infrastructure: npm i can't reach the private getdokan/dokan-ui over SSH on the runner. The same job fails on develop, so it needs a runner credential, not a change in this PR.

Review performed with Claude Code.

@MdAsifHossainNadim MdAsifHossainNadim self-assigned this Aug 14, 2026
@MdAsifHossainNadim MdAsifHossainNadim added Needs: Testing This requires further testing Needs: Dev Review It requires a developer review and approval labels Aug 14, 2026
@akzmoudud

akzmoudud commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

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 develop. See "Blocker for final merge" below.

Reviewed at ae4edf6e, together with the Pro companion getdokan/dokan-pro#5942 (7f7c9388b), against the plan in plugin-internal-tasks#2130.


Scope & method

  • Plan issue fix: When a customer buys digital and physical products from different vendors, shipping charges are applied separately to each vendor #2130 read in full, including all implementation-update comments (the §1/§3 SPA reversal, the payload step contract, the admin switch).
  • Full diff read line by line (39 files, +5,280/−219), cross-checked against: Admin\SetupWizard (parent), the VendorStoreSettingsController sanitize/validate pipeline, StoreSettingsWriter/ValueMapper, Registration::check_and_set_address_profile_completion(), Pro's legacy handlers (StoreCategory, Hooks::update_progressbar_for_payment_gateway), and WooCommerce's i18n/states.php data.
  • Automated headed browser walkthrough (Playwright/Chromium) against a live Lite + Pro site on the PR branches — 32 scripted checks covering both PRs' "How to test" sections end to end, with the network log captured.
  • Server-side verification of every save (user meta byte-level, verification-request rows), plus targeted REST probes (rest_do_request) and WP Console reproductions.
  • CI failure inspected from the runner log.

Test results — 32/32 checks green

Flow (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):

Key Result
address byte-exact (GB / England / all six subkeys)
show_email yes
payment.paypal / partial bank as submitted; half-filled bank persisted
payment.skrill written via the Pro compat $_POST replay — the SetupWizardCompat contract works
top-level keys only owned keys + profile_completionno pollution (no enable_tnc/store_seo/icon materialization)
verification request method 1, pending

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). api-fetch tunnels the PUT as POST on this stack — harmless, the route registers EDITABLE.


Requested changes

1. WARNING — State field: server enforces what the UI doesn't mark

Location: src/dashboard/settings/store/fields/AddressFields.tsx (stateIsRequired, ~L126)
The server rule (SetupWizardSchema::country_requires_state()) correctly replicates the legacy form's operative client-side gate (verified against the legacy inline JS, SetupWizard.php:1135, and live on both wizards). But the React client cannot reproduce it: /dokan/v1/data/countries serves "absent from WC's states table" (state required — GB and ~141 others) and "listed with zero states" (state not required — SG/FR class) identically as empty lists. Result, verified live: UK vendors see no "(Required)" badge on State, submit, get a 400 whose message renders unpinned below the grid. Legacy showed the asterisk and pinned the error.
Fix: ship the distinction server-side (e.g. a stateless-country list or per-part required flag on the address element) and correct the "Mirrors validate_address()" comment.

2. WARNING — Internal callables leak into payloads and REST responses

Location: includes/REST/VendorOnboardingController.php::get_response_schema() (~L149) + store_step_payload() / payment_step_payload()
Verified empirically: the bootstrapped page payload and the GET/PUT responses contain "validation_func":["WeDevs\\Dokan\\Vendor\\Settings\\Schema\\SetupWizardSchema","validate_address"]. The parent controller strips exactly these with the comment "callable arrays leak internal class names"; the onboarding override skips that hygiene.
Fix: apply the parent's stripping loop in get_response_schema() and before wp_json_encode() in enqueue_react_step() (or extract a shared helper).

3. WARNING — persist_payment() persists gateways that don't exist

Location: includes/REST/VendorOnboardingController.php::persist_payment() (~L220)
Verified empirically: PUT /vendor-onboarding/payment with payment_methods: { totally_fake_gateway: {...} } returns HTTP 200 and writes payment.totally_fake_gateway into the vendor's profile meta. Vendor-own meta only, so no privilege escalation — but it's an input-surface gap against the schema-driven design (legacy wrote only known keys).
Fix: intersect submitted keys with wp_list_pluck( $fields_by_id['payment_methods']['gateways'] ?? [], 'id' ) before folding.

Suggestions (non-blocking)

  • SetupWizardCompat::with_legacy_post_context(): wp_slash() the bag — legacy consumers wp_unslash() it (Pro Hooks.php:101), so backslash-bearing values lose a level. Edge-case only.
  • SchemaStep.tsx: setSaving( false ) is missing on the success path — masked today by the per-step remount, but a latent trap.
  • 'skrill' hard-coded in Lite's validate_payment_methods() — a Pro gateway name in Lite; validate any gateway with an email-shaped field, or let Pro add it via the existing filter.
  • show_email micro-delta: legacy forced 'no' when the field is admin-hidden; the REST path leaves the prior value (field gated out of the schema). Arguably better — but undocumented; name it in the golden-master docblock like the other accepted deltas.
  • Wizard.tsx: a bootstrapped payload with neither a schema nor a bespoke card renders null (blank step). Falling back to entry.url like payload-less steps would be safer.
  • Country/State render as plain text inputs for a beat until /data/countries resolves — consider a skeleton state.

Verified non-issues (checked so they don't need re-litigating)

Seam contract (A suppressed, B fired, order pinned by tests) · dokan_store_name mirror correctly gated on the slice · legacy mode delegating to WizardStoreSaver = the reviewed accepted deltas, pinned by the golden master · category double-write (REST seam then legacy replay) resolves last-write-wins, preserving assign-default-on-empty (observed live: "Uncategorized" chip) · deferred-output capture dedupes Pro's media templates and fixes the pre-existing ~165 KB-before-doctype quirks-mode bug — legacy mode benefits too · update_layout_style() firing on the vendor wizard's dokan_setup_wizard_styles is defused by its is_admin() guard · wp_json_encode's slash-escaping makes </script> breakout via payloads impossible · bootstrap_all_steps() restores $_GET['step'] (test-pinned) · one nonce serves all step links · old-Pro + new-Lite verified: payload-less steps get a real page load · CI's red PHPCS job is infrastructure (npm can't SSH to the private dokan-ui on the runner — fails before PHPCS runs; same on develop).


🔴 Blocker for final merge — Store Categories disappear from both the legacy and the new UI

During this review's browser testing, a defect was found that makes Store Categories vanish from every vendor-facing surface — the React wizard, the legacy wizard, and the legacy Store Settings page — while the admin settings screen continues to show the feature as enabled. The defect originates in the settings-migration base branch, not in this PR's diff, but since this PR ships on top of it, it must be root-caused and fixed before this PR is finally merged to develop.

Steps to reproduce

Preconditions

  • Dokan Lite + Dokan Pro checked out on this PR stack (or any branch containing the settings bridge, i.e. refactor/simplify-settings-to-flat-array and later). No Pro modules required — verified reproducible with zero modules active.
  • At least one store_category term existing (the default term is sufficient).

Steps

  1. As admin, go to Dokan → Settings, set Store Category to Multiple (or Single), and Save. ⚠️ The Save itself arms the defect: the value is written to the new canonical store (dokan_admin_settings.store_category_mode) and simultaneously stripped from the legacy dokan_general row.
  2. Reload the admin settings screen — it correctly shows the saved value (it reads the canonical store directly).
  3. As a vendor, open the setup wizard (?page=dokan-seller-setup, New UI) → Store step.
  4. Switch Appearance → Vendor Panel → Vendor Setup Onboarding to Legacy UI and open the wizard again → Store step.
  5. As a vendor, open Dashboard → Settings → Store (legacy Store Settings).

Actual result

  • Steps 3, 4 and 5: no Store Category field renders anywhere — new UI, legacy wizard, and legacy store settings alike. No error, no warning; the feature is silently absent.
  • Console confirmation: dokan_get_option( 'store_category_type', 'dokan_general', 'none' ) returns 'none' in the same request where get_option( 'dokan_admin_settings' )['store_category_mode'] returns 'multiple'.

Expected result

  • The Store Category picker renders on all three surfaces, honoring the admin's saved setting — which is exactly the behavior on develop.

Behavior on develop (control)

On develop (both repos), the same admin setting renders the Store Category select in the legacy wizard and legacy Store Settings as expected. develop has no settings bridge: dokan_get_option() reads the legacy dokan_general row directly, and the value is still stored there. The regression window opens the moment the bridge branches are merged.

Root cause (identified during this review)

Introduced by #3202 (merged into #3141, refactor/simplify-settings-to-flat-array):

  1. LegacySettingsRepository caches a per-request snapshot of each legacy section on its first read.
  2. Lite's container boots — and reads dokan_general via Product\VendorStoreInfo::__construct() (long-standing code, PR Enha: Added seller info on product single page #1506) — before Dokan Pro loads and registers its settings mappings (ProSettingsSchema, hooked from dokan-pro.php). The plugin load order guarantees this sequencing on every request.
  3. The snapshot is therefore always built without Pro's key mappings and is never rebuilt — the bridge's build_map() invalidates itself when new schema filters register, but the repository's snapshot cache has no equivalent invalidation.
  4. Result: every Pro-mapped legacy read returns its default for the entire request. dokan_is_store_categories_feature_on()false, so StoreCategory (constructor, line 23) skips registering all ~15 of its hooks at boot — removing the category UI and its save handlers from every surface, legacy and React alike.

Smoking-gun proof (same request, same database): dokan_get_option(…) returns 'none'; after LegacySettingsRepository::flush_cache() the identical call returns 'multiple'. Only the stale snapshot changed.

Why branch testing missed it: until the first repository-routed save strips the legacy row, the poisoned snapshot silently falls back to the raw row's old value and everything looks correct. The mask drops the moment an admin saves that settings section — i.e., on every production site shortly after upgrading.

Additional exposure: the bridge strips mapped keys from the legacy row without backfilling the canonical store ("we never backfill"). Under the same ordering defect this creates a path where a saved setting is permanently deleted from both stores with no error.

Required before final merge

  • Fix in the base branch (refactor: simplify admin settings to flat array schema #3141/feat(settings): LegacySettingsRepository for per-section dokan_* options #3202): invalidate the repository's section snapshots whenever the bridge mapping changes (mirror build_map()'s existing filter-count invalidation), and re-evaluate boot-time feature gates (StoreCategory) or move them to lazy checks.
  • Add a regression test: a Pro-mapped setting read through dokan_get_option() in a front-end context after a canonical-store save must return the saved value, not the default.
  • Re-verify Store Categories on all three vendor surfaces (React wizard, legacy wizard, legacy Store Settings) after the fix, with this PR's stack applied.

Process

  • Labels: add Dependency With Pro and REST API; release-couple with #5942 (fallbacks for version mismatch verified in both directions).
  • Playwright coverage (test: Playwright e2e coverage for the React vendor setup wizard #3326) still needs one green run against this branch before QA sign-off.
  • Prior approvals on the PR are the author's self-review; this review is the independent pass.

Review performed with Claude Code.

Screenshots
Screenshot 2026-08-18 at 5 39 57 PM

Screenshot 2026-08-18 at 3 42 31 PM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs: Dev Review It requires a developer review and approval Needs: Testing This requires further testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants