Inject a step into the setup wizard from an add-on
Add-ons inject additional steps into the Setup Wizard by hooking into the steps array before the wizard runs. The wizard re-evaluates which steps are active whenever its state changes, so steps can be conditional — shown only when a specific feature is enabled.
Installed after setup? Ask for the step. Most add-ons are installed after the podcaster has finished the wizard. Declare the step through this filter exactly as below, then call benecaster_request_wizard_step( $step_id, $show_id ) for each show that still needs it. Benecaster shows a notice on its admin pages — “An add-on has a new setup step for {show}.” — with Set it up and No thanks. Set it up runs only that show’s pending add-on steps and then returns the podcaster to the page they were on. Nobody is redirected into it.
add_action( 'admin_init', function (): void {
foreach ( my_addon_shows_needing_setup() as $show_id ) {
// Does nothing once the podcaster has finished or declined it on that show.
benecaster_request_wizard_step( 'my-addon-config', $show_id );
}
} );
add_action( 'benecaster_wizard_late_step_completed', function ( string $step_id, int $show_id ): void {
if ( 'my-addon-config' === $step_id ) {
// For example, stop showing your own "finish setup" prompt for this show.
}
}, 10, 2 );
- Every answer is final for that show. Continue completes the step. Skip, No thanks, and closing the run part-way all decline it, and closing declines the current step and every later one in the run. A declined step is never offered again, so calling the function on every request is safe. There is no undo: if you want a way back, put a link in your own settings that does the work directly.
- Per show. Each show is offered the step once. Completion is recorded for that show only and does not write the site-wide
benecaster_wizard_step_{id}_completeoption, so do not read that option to ask whether a show is set up. Listen forbenecaster_wizard_late_step_completedandbenecaster_wizard_late_step_declined, or check your own data. - A step answered in the first-run wizard is settled for the wizard’s show, Continue and Skip alike. Requesting it afterwards for that show returns
false, and needs no record of your own. Benecaster reads both answers as complete and fires neither late-step action, so keep your own record only if you need to tell a skip from a finish. - Already done? Withdraw the request with benecaster_withdraw_wizard_step(), from your own settings screen rather than your step’s save. The notice and the late run drop the step, and neither late-step action fires. Not asking again does not take back a pending request.
- Your
conditionis ignored in a late run. The request is the condition, so decide whether a show still needs the step before you request it. - Give the step a
titleso the notice can name it. Without one the notice only counts the steps. - Your component saves its own data, as in the first-run wizard: Continue does not call into an add-on step.
- Benecaster’s own step IDs and
donecannot be requested. A request for a step no active add-on declares stays pending but is not shown, so deactivating and reactivating your add-on does not lose it. See benecaster_request_wizard_step().
Resetting the wizard while you build the step. Rather than rebuilding a site to see your step again, reset the wizard to its first-run state: a Reset Wizard panel appears in Benecaster → Settings → Tools, and the same thing is available over REST as POST /benecaster/v1/wizard/reset. Both clear the completion and dismissal flags, the recorded show, every per-step marker and every after-setup step record on every show, then fire benecaster_setup_wizard_reset.
The reset is a development affordance, not a customer-facing one. It is offered only on local, development and staging environments, or wherever WP_DEBUG is on, and returns 403 anywhere else — so do not design a step that depends on the podcaster being able to run the wizard a second time.
When to Use This
Use this recipe when:
-
Your add-on needs the podcaster to complete a configuration step before they can use the plugin effectively
-
You want to surface the configuration in the familiar wizard flow rather than asking the podcaster to find it separately after setup
-
Your step should only appear under a specific condition (e.g. only after a subscription plugin has been connected)
How It Works
The filter receives the ordered array of step definitions. Find your insertion point by searching the array for a Benecaster step ID, then splice your own definition in at that position — inserting after subscription, for example, places your step immediately following the bridge picker.
The component value names a property of window.BenecasterExtensions, and the wizard renders whatever function it finds there. Assign your component to that property in a script enqueued on the wizard page.
Step Definition Keys
| Key | Type | Description |
|---|---|---|
id |
string | Unique slug for this step. Used in completion tracking and wizard state. |
component |
string | React component name registered on window.BenecasterExtensions. |
required |
bool | When true, the Skip button is disabled for this step. |
condition |
string|null | State flag that must be true for the step to appear. null = always shown. Ignored when the step is requested after setup. |
title |
string | Optional, already translated. Names the step in the notice that offers it after setup. |
Your component’s props. The wizard renders your component with showId, the show being set up (null in the first-run wizard until the show has been created), lateRun, which is true only when the step was requested after setup, and setCanContinue( boolean ). Ignoring all three is fine.
Hold Continue until your step is ready
Call setCanContinue( false ) and the wizard’s Continue → (or Finish →) button stays disabled until you call setCanContinue( true ), for a step that must check something first, such as an API key. It works in the first-run wizard and when the step is requested after setup.
- Continue starts enabled on every visit to your step, including a return with ← Back. A step that should open held calls
setCanContinue( false )from a mount effect, not only when an input changes. - Skip is never held, nor is the exit button when the step runs after setup: the podcaster can always decline your step.
- The function belongs to one visit. A call that lands after the podcaster has left your step (a check finishing late) does nothing, so it can never hold the next step. It keeps the same identity for the visit, so it is safe in an effect’s dependency list.
- Call it from an effect or an event handler, never during render.
- Holding Continue does not save anything: your step still saves its own data.
function MyAddonWizardStep( { showId, setCanContinue } ) {
const [ key, setKey ] = React.useState( '' );
const [ status, setStatus ] = React.useState( 'idle' ); // idle | checking | valid | invalid
// Open held: nothing has been checked on this visit yet.
React.useEffect( () => { setCanContinue( false ); }, [ setCanContinue ] );
const testConnection = async () => {
setStatus( 'checking' );
// Your own endpoint, localised by your add-on (wp_localize_script on your wizard script).
const res = await window.fetch( window.myAddonWizard.testKeyUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': window.benecasterWizard.nonce },
body: JSON.stringify( { show_id: showId, key } ),
} ).catch( () => null );
const ok = !! res && res.ok;
setStatus( ok ? 'valid' : 'invalid' );
setCanContinue( ok );
};
return (
<div>
<input value={ key } onChange={ ( e ) => { setKey( e.target.value ); setCanContinue( false ); } } />
<button type="button" onClick={ testConnection } disabled={ status === 'checking' }>Test connection</button>
</div>
);
}
window.benecasterWizard.nonce is the wizard’s wp_rest nonce, valid for your own REST routes too. The three setCanContinue calls are the point: held on mount, held again on edit, and released only by a passing check.
Benecaster Step IDs
The full ordered list, for finding your insertion point:
language, show-name, artwork, description, category, details, subscribers, migration, ssp-import, subscription, builtin-stripe, builtin-tier, tiers, import, limit-enforcement, permalink, done
done is the last entry, so appending to the array puts your step after the finish screen. Splice at a named index instead of pushing onto the end.
Note that subscribers and subscription are two different steps: subscribers is where the podcaster chooses how subscriptions work, subscription is the bridge picker.
Condition Flags
Six flags are available. A step declaring one appears only while that flag is true:
| Flag | True when |
|---|---|
bridge_connected |
Any membership option is selected for the show, Built-in membership included. |
builtin_chosen |
Built-in membership is the selected option. Benecaster’s own Stripe-keys and first-tier steps use it. |
external_bridge_connected |
A membership plugin other than Built-in is selected. Benecaster’s own level-mapping step uses it. |
ssp_or_powerpress_detected |
Seriously Simple Podcasting or PowerPress is active on the site and has published episodes to import. |
license_active |
The show has an active license. |
license_limited |
The license is active but on a plan carrying a show limit. |
bridge_connected is true whenever any option is selected, not only yours — check the selected bridge slug inside your component if your step should be specific to it. A step that maps or reads an external plugin’s membership levels should use external_bridge_connected instead, because Built-in membership has no external levels.
A step with an unrecognised condition string defaults to hidden — safe to deploy before the flag-setting logic exists.
Notes
required: false is the standard default. Mark a step required only when it blocks the add-on from functioning at all. The wizard’s design philosophy is that skipped steps surface as dashboard tips — podcasters can return to them after setup.
Register the component by assigning it. Enqueue your script with benecaster-wizard as a dependency, then set the property your step definition names:
window.BenecasterExtensions = window.BenecasterExtensions || {};
window.BenecasterExtensions.MyAddonWizardStep = MyAddonWizardStep;
The wizard renders a step only when that property is a function, and shows the step ID as a placeholder otherwise. The same object carries Benecaster’s React runtime (React, ReactDOM, ReactDOMClient, ReactJSXRuntime, i18n, extensionRuntimeVersion), so do not name your component after one of those keys.
Your step can use React hooks when its bundle maps Benecaster’s runtime as externals, as described in Inject a React Component into a Named Slot. Map react, react-dom, react-dom/client, react/jsx-runtime and @wordpress/i18n. Do not map @tanstack/react-query: the wizard does not publish ReactQuery or queryClient, so that external would resolve to undefined. Fetch with window.fetch, as the wizard’s own steps do.
Combine with benecaster_setup_wizard_bridge_options to add a custom bridge card to the Subscription step. When the user selects your bridge, bridge_connected becomes true and this step appears. See Add a Custom Bridge Card to the Wizard Subscription Step.
Related
- Setup Wizard Walkthrough — customer-facing wizard documentation
Code
<?php
add_filter( 'benecaster_setup_wizard_steps', function ( array $steps ): array {
// Insert after the 'subscription' step.
$insert_after = array_search( 'subscription', array_column( $steps, 'id' ), true );
if ( false !== $insert_after ) {
array_splice( $steps, $insert_after + 1, 0, [[
'id' => 'my-addon-config',
'component' => 'MyAddonWizardStep',
'required' => false,
'condition' => 'bridge_connected',
]] );
}
return $steps;
} );
Hooks Used
Need this built rather than just documented? See our services →