Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Onboarding Laravel Package

wallacemartinss/filament-onboarding

Database-driven onboarding for Filament v5: a progress checklist that follows users across every panel page, guided spotlight tours, video steps, and condition-based steps that complete automatically so existing users enter journeys already partly done.

View on GitHub
Deep Wiki
Context7
v1.2.0

The 1.x backport of v2.6.0: the same changes, on Filament v4, and every one of them applied unchanged — the two lines share their source and differ only in their Composer constraints and their Livewire internals. No breaking changes, no new migrations, nothing to migrate. Upgrade with composer update wallacemartinss/filament-onboarding — and run php artisan filament:assets this time, because the tour runner changed.

Most of this release was found and fixed by [@jyrkidn](https://github.com/jyrkidn), while measuring the package against a CMS panel whose content resources nest their fields. The rest was filed by [@DamienJolly](https://github.com/DamienJolly) and [@PatrickJunod](https://github.com/PatrickJunod), and the definitions cache was sharpened by [@felipeArnold](https://github.com/felipeArnold). Thank you, all four.

Fixed

A field nested under a state path is found by its name again (#16). A stop stores a field's name; the markup wears its state path. Those are the same string on a flat form and not on a nested one — any Group, Section, Tab or wizard step with a state path of its own prefixes what sits under it, so a field still called title renders as form.details.title. The selector matched the name as the whole path only, so every stop on such a form pointed at nothing. That is not a soft landing: the tour parks, Next stays disabled, and the stall is written to localStorage, so it comes back stalled on every reload until the subject presses Skip. The name is now matched at the end of the path as well as as the whole of it, with the dot included so subtitle cannot answer for title.

A form that asks who is rendering it keeps its fields. The target picker built forms against a bare Schema::make(), and Schema::getLivewire() has a return type — so a form, or one field inside one, that reaches for the component got a TypeError rather than a null. The disclaimer held (nothing was taken down), but the cost was silent and landed on exactly the resources worth touring: on the panel this was measured against, 8 of 34 form resources offered no targets at all for this reason. The schema is now built against the resource's own page, the component Filament itself would use, and the one the table side already asks for its columns.

A stop whose target is hidden after it lands goes back to waiting. Filament does not remove the tab, the wizard step or the collapsed section the subject just left — it hides it. The element stays connected while its rectangle collapses to nothing, so the spotlight was redrawn as a padding-sized square in the corner of the screen with the popover still claiming to point at something. A hidden target is now dropped like a removed one.

A stop reached without being walked to still presses the control it names. advance names the control a stop lives behind — a wizard's next, a tab — and only the forward path ever pressed it. A tour crossing to another page arrives through the render on the far side; so does one picked back up after a reload, and one resumed from the stop the subject parked on. All three sat waiting in front of a closed tab the stop had already said how to open.

A stop reaches a column the table is holding off the side of the screen (#15). "Already visible" was decided on the vertical axis alone, and a table wide enough to scroll sideways — which on a phone is every table — keeps its far columns at a perfectly ordinary height several hundred pixels past the right of the window. Both axes are asked about now, and the scroll asks for inline: 'center': 'nearest' is no movement at all for something already as near as it gets on its own axis.

The watcher stands down while the tour waits. measure() runs once a frame and hands back to render() when the target has gone, but render() does not say it is waiting until it has finished looking — three seconds, when the element is not coming back on its own. So the next frame started the search over, and so did the one after it: sixty renders a second, each with a poller of its own, for as long as the subject stayed on the other tab.

A worker that outlives the request stops serving yesterday's definitions (#19). The definitions are memoised for the length of one request. On FPM that memo cannot outlive the request that made it; under Octane the manager is the same object on every request a worker serves, and under queue:work the same object on every job. "Once per request" quietly became "once per worker" — and since the write that clears it fires its model events in whichever process handled it, every other worker went on serving a flow the author had switched off. The memo is now tied to the boundaries a long-lived worker actually has. What gets dropped there is only the copy that process is carrying, never the copy the processes share.

Added

A multi-tenant panel can send somebody to its tenant profile (#21). Where a step sends somebody is picked from the panel's own dropdowns, so a destination can only ever be one that exists — which means anything the panel can reach and the picker cannot see is, from the author's side, simply not there. The tenant profile was one of those: registered through ->tenantProfile() and never through ->pages(), so getPages() never listed it, while "finish setting up your team" is close to the first thing a multi-tenant onboarding wants to say.

Changed

The JavaScript toolchain, and the pinned actions, catch up with the 2.x line. esbuild 0.20 → 0.28.2, jsdom 30.1.0, vitest 5.0.1, and actions/checkout and actions/setup-node two majors forward onto the SHAs the 2.x workflow is already running. Dependabot only opens pull requests against the default branch, so none of this reaches 1.x on its own. Build output is unchanged.

Full changelog: https://github.com/wallacemartinss/filament-onboarding/compare/v1.1.0...v1.2.0

v1.1.0

The 1.x backport of v2.5.0: the same two changes, on Filament v4. No breaking changes, no new migrations, nothing to migrate — composer update wallacemartinss/filament-onboarding and you are done. Both filed by [@DarkGhostHunter](https://github.com/DarkGhostHunter) — thank you.

Fixed

A panel running ->strictAuthorization() no longer throws on ->manageFlows() (#3). Strict mode throws for any resource model without a policy, and the package registered three that had none — the flows, the steps (the relation manager authorizes against the related model), and the conditions. Entering such a panel died on a LogicException before it drew anything.

Each model now ships a policy that answers yes to every ability Filament probes — deliberately not an opinion, just the access the resources always had, sitting in the package's Policies namespace where Laravel's guesser finds it on its own. Narrowing it is yours, three ways: extend the shipped policy and override only what you mean to restrict; register your own with Gate::policy(), which boots later and therefore wins; or name a class in the new policies config block. The README grew an Authorization section, because "where does the policy even go" was the other half of the report.

Added

A per-user switch: ->skipWhen() (#4). Onboarding used to run for everybody on every page — for a user done for months, that meant reading their progress out of the database to conclude, again, that there was nothing to show.

FilamentOnboardingPlugin::make()
    ->skipWhen(fn (User $user): bool => $user->onboarded_at !== null),

When it answers true, the whole machine steps aside: no surface mounts, no onboarding_* table is read, and the Livewire endpoints a browser could still call by hand write nothing — the switch turns the endpoints off too, not just the rendering. Onboarding::for() ignores it on purpose: resetting a done user's onboarding must reach exactly the people the switch has hidden. The README shows the full wiring — the column, the switch, and a FlowCompleted listener that stamps the user when the last journey closes.

Notes

Filament v4 carries strictAuthorization() and the Filament\authorize() helper just as v5 does, so both fixes applied without a single v4-specific variant — the two lines stay feature-identical. Suite green on Filament v4.12.5: 130 PHPUnit tests, PHP 8.2 through 8.4.

On Filament v5, require ^2.5 from the main branch instead.

Full changelog: https://github.com/wallacemartinss/filament-onboarding/compare/v1.0.0...v1.1.0

v2.5.0

A minor release: no breaking changes, no new migrations, nothing to migrate. Upgrade with composer update wallacemartinss/filament-onboarding — no filament:assets needed this time, nothing in the browser changed. Both changes were filed by [@DarkGhostHunter](https://github.com/DarkGhostHunter) — thank you.

Fixed

A panel running ->strictAuthorization() no longer throws on ->manageFlows() (#3). Strict mode throws for any resource model without a policy, and the package registered three that had none — the flows, the steps (the relation manager authorizes against the related model), and the conditions. Entering such a panel died on a LogicException before it drew anything.

Each model now ships a policy that answers yes to every ability Filament probes — deliberately not an opinion, just the access the resources always had, sitting in the package's Policies namespace where Laravel's guesser finds it on its own. Narrowing it is yours, three ways: extend the shipped policy and override only what you mean to restrict; register your own with Gate::policy(), which boots later and therefore wins; or name a class in the new policies config block. The README grew an Authorization section, because "where does the policy even go" was the other half of the report.

Added

A per-user switch: ->skipWhen() (#4). Onboarding used to run for everybody on every page — for a user done for months, that meant reading their progress out of the database to conclude, again, that there was nothing to show.

FilamentOnboardingPlugin::make()
    ->skipWhen(fn (User $user): bool => $user->onboarded_at !== null),

When it answers true, the whole machine steps aside: no surface mounts, no onboarding_* table is read, and the Livewire endpoints a browser could still call by hand write nothing — the switch turns the endpoints off too, not just the rendering. Onboarding::for() ignores it on purpose: resetting a done user's onboarding must reach exactly the people the switch has hidden. The README shows the full wiring — the column, the switch, and a FlowCompleted listener that stamps the user when the last journey closes.

Full changelog: https://github.com/wallacemartinss/filament-onboarding/compare/v2.4.1...v2.5.0

v1.0.0

First release of the 1.x line: the plugin runs on Filament v4 (Livewire 3).

It is feature-identical to the 2.x / Filament v5 line — same journeys, checklist, tours, conditions and programmatic API. The only differences are the Composer constraints and the Livewire component-registration internals.

Install

composer require wallacemartinss/filament-onboarding:^1.0

Requirements

  • PHP 8.2+
  • Laravel 12+
  • Filament 4.0+
  • PostgreSQL or MySQL/MariaDB

Notes

  • On Filament v5, require ^2.1 from the main branch instead.
  • Verified green on Filament v4.11.8: 116 PHPUnit + 39 JS tests, Pint clean.
v2.4.1

A patch release: no API changes, nothing to migrate. Upgrade with composer update wallacemartinss/filament-onboarding and php artisan filament:assets — the stylesheet changed, and it is versioned by content hash, so that second command is what puts it in front of the browser.

Fixed

"Start over" and "Hide" are buttons now. They sat at the foot of a journey as bare underlined words, which is how a control that does something ends up reading like a footnote: nothing about them said they could be pressed, and so nobody pressed them. They are grey buttons now — with icons, and a rule above them — the same secondary button Filament dresses its own "Cancel" in.

The progress page got its air back. The journey description had no bottom margin at all: zero pixels between the sentence introducing the journey and the first card of it, so the text read as though it were falling into the card rather than presenting it. And the step cards carried 18 px of horizontal padding where Filament gives a card 24 px. Both now match what the panel around them does.

Changed

The Composer archive no longer carries the whole repository. There was no .gitattributes, so tests/, .github/ and the docs were being unpacked into the vendor/ directory of everyone who installs this package.

This release adds 2.7 MB of screenshots to the repository — so the file had to exist before the screenshots did. With it, the install shrinks from 1.0 MB to 0.8 MB instead of growing to roughly 3.5 MB. Nothing needed at runtime was excluded: src, config, database, resources/ and stubs all still ship.

Documentation

The README shows the package instead of describing it. Twenty-two screenshots of it running in a real panel, each placed next to the claim it proves, rather than in a gallery nobody scrolls to:

  • the five-stop wizard sequence sits under the sentence claiming the tour presses the wizard's own Next — and the third shot is the tour having done exactly that;
  • the client-form tour is captioned "a tour with no CSS in it at all", because that is what it is: field:name, field:status, action:submit, picked from dropdowns;
  • the welcome screen sits under the paragraph about accounts that start part-done, because the ring behind that modal reads 38% on a user who has never seen the journey — three of its eight steps were already true.

A new "Writing a journey" section. The README had always claimed that flows, steps and the conditions they hang off are written in the panel, and had never once shown the panel.

Full changelog: https://github.com/wallacemartinss/filament-onboarding/compare/v2.4.0...v2.4.1

v2.4.0

A tour stop is picked, not typed. Writing one meant typing [data-onboarding="client-submit"], which assumed two things about whoever was expected to do it: that they know CSS, and that they know a developer went and put that hook in the code. Journeys are supposed to be product's to write. Product knows neither of those.

Added

  • "What to spotlight" is a dropdown, read out of your own panel. Pick the page a stop lives on, and the package offers what is on it:

    • the fields of that page's form, by the labels they wear — including a Select and a Toggle, which have no id of their own; the spotlight lands on the whole field, label and all, rather than on the box you type in
    • the save button, which answers on a create page and an edit one alike
    • the table, the search box, and each column by its heading
    • the "New client" button — which is not the page's own, it is a link to the create page, and the package has the route
    • any widget of the panel
    • and a CSS selector of your own, for what the panel cannot name

    What is stored is the choice (field:status), not the selector — the same reason a route name is stored and not a URL: the markup is Filament's to change, and a journey should survive it changing.

    A form that cannot be read from the outside — one that leans on the record being edited, or on who is looking — gives up its fields and nothing else: they are not listed, the panel does not fall over, and the CSS box is right there.

Fixed

  • A tour and the welcome screen no longer argue over the same screen. Both are modal, and the state is reachable: a tour parks itself in sessionStorage to cross a page, while "not now" lives in the server's session. Let the session turn over in between — it expires, it rotates on a login — and the page comes back with the tour resuming and the welcome sure it was never answered. Somebody walking a tour has already started; there is nothing left to invite them to, so the welcome stands down while one is running.
v2.3.1

Fixed

  • The step editor was write-once. Every field that depends on what kind of step it is — the whole Tour tab and all of its stops, the condition dropdown, the visit URL, every media field — was there when a step was created and gone when one was opened to be edited. So a tour could be written and then never touched again: to change a stop you had to delete the step and start over.

    The state of a field backed by an enum comes in two shapes, and the editor only knew one. Creating, the state is what the browser posted — the string 'tour'. Editing, the form was filled from the record, whose attribute is cast, so the state is StepType::Tour, the enum itself. $get('type') === StepType::Tour->value is therefore true exactly half the time, and the half it answers no is the half where the field simply is not rendered. Nothing threw. The form quietly showed less of itself, on the one path nobody tests: the second time you open something.

    Both shapes now answer the same. (Two (string) casts over the same state went with it — an enum is not a string, and PHP says so by dying.)

v2.3.0

Conditions no longer need a deploy. The package's argument has always been that a journey is a thing you write, not a thing you ship — and it held right up to the most valuable kind of step, the one that completes itself, which needed a closure in a service provider and a commit to go with it. A product person could rewrite the copy of a journey without asking anybody, and then had to open a pull request to ask "have they added a client yet?".

Added

  • Conditions are written in the panel. Onboarding → Conditions → New, and the two shapes cover what onboarding actually asks:

    • Counts something they have — Client · at least 1 · only where status is active. Optionally scoped to the tenant, so a client added in one does not tick the step in another.
    • Asks about them — email_verified_at is filled in.

    No code, no deploy. And no SQL either: the model comes from an allowlist (conditions_builder.models, which offers app/Models when left empty), the column from that table's real columns, the operator from an enum — and the one thing an author types, the value, is bound rather than interpolated.

  • php artisan make:onboarding-condition HasActivePlan — for the questions a form cannot ask (a subscription, an API call, a score). It writes the class, tells you the key it answers to, and there is nothing to register.

  • Condition classes are discovered. Anything in app/Onboarding/Conditions is found and registered on its own. Writing the class and naming it in a config file was saying the same thing twice, and the second half is the half that gets forgotten in the deploy where it matters. (discovery.enabled => false to opt out.)

  • Code always wins a name clash: a condition written in the panel cannot take a key a class already answers to, so a row can never quietly redefine what a step means.

Fixed

  • The step editor stops showing an empty dropdown with no explanation. Picking the "condition" completion mode with no conditions registered offered nothing and said nothing — the single most reportable thing in the package. It now says where to write one.
  • The journey header is a card again. Folding the steps away in 2.2.1 took the surface with them: .fio-hero was what carried the radial gradient, the border and the shadow, and the new header left each journey as a bare row on the page background. The header carries them now — same gradient, same border, same shadow, holding a summary rather than everything.

Upgrading

New tables ship with new releases, and this one adds onboarding_conditions:

composer update wallacemartinss/filament-onboarding
php artisan vendor:publish --tag=filament-onboarding-migrations   # existing files are left alone
php artisan migrate
php artisan filament:assets

Conditions registered in code keep working exactly as they did — in the config, at runtime, or (now) simply by existing in app/Onboarding/Conditions. Nothing needs to be rewritten.

v2.2.2

Fixed

  • StartTourAction with one tour asked which of the one. It always set a modal heading, and Filament opens a modal for that alone — shouldOpenModal() asks hasCustomModalHeading() long before it looks at whether there is a schema to show. So the common case, StartTourAction::make('servers-tour'), opened an empty modal asking "which tutorial do you want?" and offering no answer. The modal is now turned off unless there is more than one tour to choose between — including when the list names several and only one of them exists.
v2.2.1

Required on Laravel 13 — and on any Laravel 12.63+ whose cache config is current.

Fixed

  • The definitions cache took the whole panel down on the second request. Laravel will not unserialize classes out of the cache unless the application lists them: a stock Laravel 13 ships cache.serializable_classes => false, so that a leaked APP_KEY cannot be turned into a gadget chain through the cache. The manager cached a Collection of Eloquent models — which writes perfectly well, and reads back as __PHP_Incomplete_Class. The first request of the panel's life populated the cache and worked; every request after it died on the return type of cachedFlows(), which is a 500 on every page, since the launcher renders in the layout. Definitions are now cached as plain attributes and rebuilt on the way out — casts, relations and translatable columns intact — so nothing in that cache is an object. A cache holding the old shape reads as a miss and is overwritten, so upgrading needs no cache:clear.

Changed

  • The progress page folds. It used to open every step of every journey at once: a wall of cards, most of them about work already done, with the one thing the person came for buried among them. Each journey is now a collapsible section whose header carries the ring, the count and the next thing to do — which, for a folded journey, is usually all anyone wants. A journey with something pending opens; a finished one starts folded; and the choice is remembered per journey, so a completed one that gets opened stays open.
v2.2.0

The release to upgrade to, from anywhere. The 2.0.0 → 2.1.0 migration now survives the duplicate rows 2.0.0 left behind, so nothing stands between 2.0.0 and here.

Added

  • A welcome screen (->welcome()) — shown once, on the first page after logging in, with "get started", "not now" (this session) and "do not show this again" (which takes the floating button and its ring with it). Reversible from the progress page.
  • StartTourAction accepts several tours — hand it a list and it asks which one, naming each by its own title; a screen can be walked more than one way. With a single tour there is nothing to ask, and it starts outright.
  • A tour stop can be marked optional: about something an account may not have yet (a tag, an empty chart), the tour steps aside instead of waiting for it.

Security

  • Finishing a tour no longer completes a step whose mode says otherwise. Any step carrying a tour — including one bound to a condition ("has two-factor") or marked programmatic ("only my code completes this") — could be completed from the browser by clicking "next" to the end: a second front door around the very checks completeStep() refuses. A tour now completes only a Manual step, where the tour is the task; for every other mode it records that the tour was watched and leaves completion to the mode.

Fixed

  • The scope-hardening migration survives the data it was written for. An installation that ran 2.0.0 can hold duplicate progress rows — the NULL scopes the old unique index could not compare. Converting those NULLs to '' made the duplicates collide with the index mid-UPDATE, and php artisan migrate died on precisely the installations that needed it. Duplicates are now merged first: the oldest row stays and inherits the furthest the others got — the latest of each timestamp, the union of the metas.
  • "Get started" on the welcome screen no longer races itself. It was a link that also fired a Livewire call: the browser navigated while the request recording the answer was still in the air, and when the write lost, the welcome greeted the subject again — on the very page it had just sent them to. One button, one request: the answer is recorded, then the server redirects.
  • Opening a video while another is up no longer crosses the wires. A docked modal leaves the page usable, so a second video can be opened over a first — whose player was never torn down: its poll kept reporting the old video's seconds under the new step's key, and the new player often failed to build at all (the provider's iframe had replaced the mount the template still pointed at). The leaving player now settles its account — reports under its own key, tears down — and the markup is re-stamped fresh.
  • A step key is only accepted once per flow. The database always said so; the form now says it as a validation message under the field instead of a QueryException over the whole modal.
  • A step key shared by two journeys lands on the journey the surface is showing. Keys are unique per flow, and the panel-wide search took the first flow wearing the key — ticking "invite-team" on one journey completed it on another. Surfaces now name the flow they are showing (the step methods take an optional $flowKey, so published views keep working).
  • Visiting a URL makes no progress in a flow the subject cannot see. handleVisit() checked the step's visibility condition but not the flow's, so standing on the right page completed steps of a journey that did not exist for that account.
  • A journey of only optional steps completes when everything is settled — not at the first touch. "Every required step is done" is vacuously true of a journey with no required steps, and used to stamp completed_at and fire FlowCompleted after the first tick, with the journey barely begun.
  • An optional step with nowhere to send you can still be skipped from the launcher. The skip button only rendered next to a destination, so a step with no URL, tour or video could be skipped from the progress page alone.
  • "Advance with" presses the control the way a mouse does — mousedown, mouseup, click. Plenty of things do not listen for a bare click: a Filament dropdown (the filter panel of a table, for one) opens on mousedown, so a tour pointing into it waited forever for a panel that was never going to open.
  • Switching tabs mid-tour no longer reads as a stuck form. Pressing a control is a round trip and a re-render; calling it "blocked" after 1.5 s put a warning on screen for a page that was merely thinking. The window is 3 s, and the message fits both cases.
  • A provider script that never arrives costs the tracking, not the video. When an ad blocker (or a walled network) keeps the YouTube/Vimeo API out, the modal now falls back to the provider's plain embed instead of opening empty. A plain iframe cannot be asked about watch time, so none is invented — a video-completion step completes some other way.
  • The welcome dialog traps focus and answers Escape with "not now" — aria-modal was promising both without delivering either. The duplicated class attribute on the progress page's restart note now applies its spacing.

Changed

  • One SubjectOnboarding per component request instead of one per question: a launcher render asked "current flow?", "hidden?", "welcome?" and each fresh engine re-read all of the subject's progress. The engine keeps its own maps current as it writes, so nothing stale survives the memoisation.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin