Skip to main content

Register a guided product tour from an add-on JS bundle

Premium Intermediate

An add-on that adds screens to the Benecaster admin can also add a guided walkthrough of them, using the same tour system as the built-in ones — so an add-on tour looks and behaves like part of the product rather than a bolted-on overlay.

Register the tour and Benecaster handles the rest: it starts when the tour is named in the page URL, and it remembers per user that they have finished it, so nobody is walked through the same thing twice. Registering early or late both work, so you do not have to race the admin interface as it loads.

Offering the tour again after you change it. Give the tour a contentVersion and raise it by hand when the tour’s content meaningfully changes. Everyone who completed an earlier version then sees a bar at the top of the next Benecaster screen, “The {your label} tour has been updated since you last took it.”, with Take the tour and No thanks. Nobody who never took the tour is offered it, so a new tour needs nothing: leave contentVersion out, it defaults to 1, and nothing is offered until you raise it.

window.BenecasterExtensions.tours.push( {
    id:             'my-addon',
    label:          'My Add-on',
    contentVersion: 2, // was 1: the settings screen was redesigned
    steps:          [ /* … */ ],
} );
  • Raise it by hand; never compute it from your steps or their conditions. It means “this tour changed”, not “what this user would see changed”. A version worked out from conditions would offer the tour again because of the podcaster’s own data, not because of anything you shipped.
  • Only a positive whole number counts. 0, negatives, fractions and strings are read as 1, so a typo can never make your tour look newer than every completion, but it won’t offer it again either.
  • Each tour and each user is separate. Raising yours never re-offers Benecaster’s own tour or another add-on’s, and one admin’s answer doesn’t settle anyone else’s. No thanks holds until your next raise.
  • You don’t call anything. Benecaster records the version when the user reaches Done. Stopping part-way records nothing, so the bar comes back on the next page load.

A site that doesn’t want the offer at all can switch it off; see Stop Benecaster offering updated tours again.

Anchor each step to a data-tour="[id]" attribute. Those attributes are a public contract, the same as a hook name, and are safe to depend on: Benecaster’s own tests list every anchor it renders, so one cannot disappear by accident, and renaming or removing one is announced in the changelog. Class names and structural selectors are not — they change without notice, and a tour built on them will break.

tours is a plain array: push onto it, as the example does. There is no register() method. A tour carries id (1 to 64 lowercase letters, digits, - or _; a tour with any other id is not registered, and the browser console says why), label (shown in the tour catalog), an optional condition, an optional contentVersion (below), and steps; every step needs its own id, a target, a title and its text in content, and may add a position and a condition of its own. The tour’s condition decides whether the tour is offered at all and must return true or false straight away; a step’s condition receives the current show’s ID and may return a promise. A step’s screen is where the tour navigates before showing it — settings, episodes, subscribers, addons (Settings → Add-ons) and the like. Name a screen the admin does not have and the tour stops on that step with no message.

A step whose target is not on screen does not fail loudly. The tour waits two seconds and then shows the tip floating over the page, attached to nothing. If a step points at something the screen only sometimes draws, give the step its own condition and it is left out when it would not work. See Gate a tour step on what the screen actually renders.

Pointing a tour at your add-on’s card. Every card on Settings → Add-ons carries data-tour="addon-card-{entitlement slug}", whether the add-on is bought yet or not — so a tour can introduce an add-on to someone still deciding. Use the full entitlement slug, for example [data-tour="addon-card-benecaster-addon-blocks-widgets"]; the short slug (addon-card-blocks-widgets) matches nothing. Pair it with screen: 'addons'.

Pointing a tour at your add-on’s tab in a show’s settings. Use screen: 'show-settings:{page id}': it opens that tab for the current show when the tour starts, and the step is left out when your add-on is off for that show. Never write a raw /shows/{id}/settings/… path. The tab button carries data-tour="{page id}-nav", added automatically. See Point your add-on’s tour at your own Show Settings tab.

Pointing a step at a real episode’s References tab. screen: 'episode-references' is worked out when the tour starts, for the current show: it opens the most recent episode that cites at least one reference, at /episodes/{id}?tab=references. The target is yours to render, inside each reference card through the BenecasterEpisodeReferenceRow slot.

{
    id:      'my-addon-reference-control',
    target:  '[data-tour="my-reference-control"]',
    screen:  'episode-references',
    title:   'Your control, on a real reference',
    content: 'Explain the control here.',
}

When the show has no such episode, the step is left out of the tour, and that is the case to design for, not an error. It happens on a new show, on one that has never used references, when there is no current show, and when the lookup fails. If every step of a tour is left out, the tour doesn’t start. A step that resolves and still waits forever means your fill isn’t rendering the data-tour target.

The lookup behind it is GET /benecaster/v1/shows/{id}/references/latest-episode (see the References REST API), and the tab works as a plain link: /episodes/{id}?tab=references.

window.BenecasterExtensions is shared. Besides tours and registerFill, it carries Benecaster’s React runtime (React, ReactDOM, ReactJSXRuntime, i18n and others) for add-on bundles to use as externals — see Inject a React Component into a Named Slot. Add to the object as the example does; never replace it.

Code

window.BenecasterExtensions = window.BenecasterExtensions || {};
window.BenecasterExtensions.tours = window.BenecasterExtensions.tours || [];

window.BenecasterExtensions.tours.push( {
    id:    'email-editor',
    label: 'Email Editor Tour',
    // `addons` holds full entitlement slugs; the short slug 'email-editor' matches nothing.
    condition: () => window.benecasterAdmin.addons.includes( 'benecaster-addon-email-editor' ),
    steps: [
        {
            id:       'pick-template',
            target:   '[data-tour="email-editor-template-picker"]',
            screen:   'settings',
            title:    'Pick a template',
            content:  'Start from a built-in template or duplicate one of your own.',
            position: 'bottom',
        },
        // …
    ],
} );

View on GitHub →

Need this built rather than just documented? See our services →