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.
Strict panels stop throwing, and finished users stop paying. Both filed by [@DarkGhostHunter](https://github.com/DarkGhostHunter) — thank you.
->strictAuthorization() no longer throws on ->manageFlows()
(#3). Strict mode throws for
any resource model without a policy, and the package's three had none. Each now ships one —
permissive on purpose, the exact access the resources always had — sitting where Laravel's
guesser finds it. Narrow it by extending the shipped policy, registering your own with
Gate::policy() (yours boots later, so yours wins), or naming a class in the new
policies config block. The README grew an Authorization section,
because "where do I even put the policy" was the other half of the report.->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. Now
->skipWhen(fn (User $user): bool => $user->onboarded_at !== null) steps the whole machine
aside for whoever it answers true: no surface mounts, no onboarding_* table is read, and the
Livewire endpoints a browser could still call by hand write nothing. Onboarding::for()
ignores the switch 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.Two controls at the foot of a journey that nobody was pressing. "Start over" and "Hide" sat under the last step as bare underlined words — which is how a control that does something ends up reading like a footnote. And the text above them was touching the cards below it: a description with no margin under it does not introduce the thing it describes, it falls into it.
.fi-section-header). Both now match
what the panel around them does..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
that repository, so the file had to exist before the screenshots did — and with it the
install actually shrinks, from 1.0 MB to 0.8 MB, rather than growing to about 3.5 MB.
Nothing needed at runtime was excluded: src, config, database, resources/ and
stubs all still ship.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.
"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:
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 inWhat 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.
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.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.)
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?".
Conditions are written in the panel. Onboarding → Conditions → New, and the two shapes cover what onboarding actually asks:
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.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.
.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.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.
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.Required on Laravel 13 — and on any Laravel 12.63+ whose cache config is current.
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.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.
->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.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.'' 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.QueryException over the
whole modal.$flowKey, so published views keep
working).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.completed_at and fire FlowCompleted after the
first tick, with the journey barely begun.aria-modal was
promising both without delivering either. The duplicated class attribute on the
progress page's restart note now applies its spacing.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.Upgrade from 2.0.0. Do not run 2.0.0.
completeStep('has-two-factor') reached the server
whatever the screen was showing, and nothing on the server asked whether that step was
the caller's to touch or whether it finishes that way. Steps bound to a condition
("has a server", "enabled two-factor") could be marked done — permanently, since a
condition step is never un-completed — as could Programmatic steps, Visit steps
without the visit, videos without the watching, steps hidden from the account by a
visibility condition, and steps belonging to another panel. Any application logic hanging
off StepCompleted / FlowCompleted fired for work nobody did.
Every browser-reachable method now re-asks what the interface asked, within the panel and
the visibility the subject actually has.null silently when the panel was not there to
ask. It is captured at mount and locked.javascript: and data:text/html are
refused.onboarding:reset says when it wiped nothing (resetting without --scope in a tenanted
application resets nobody).StartTourAction — a header action for any page or resource that starts one tour in
place ("How does it work?"), visible only when the subject can actually take it.dist.Withdrawn. This release lets any authenticated user complete any onboarding step by calling the component's methods directly, and a condition that throws returns a 500 on every page of the panel. Upgrade to 2.2.0 — its migration is the one that survives the duplicate progress rows this release could write.
Database-driven onboarding for Filament v5.
->manageFlows()).->progressPage(). Includes "start over".StartTourAction — a header action for any page or resource that starts one tour in
place ("How does it work?"), visible only when the subject can actually take that tour.<video> element, the YouTube
IFrame API and the Vimeo SDK — and a video resumes where it stopped.[@livewire](https://github.com/livewire):edit_password_form), which covers
form sections that carry no CSS hook of their own.pt_BR → pt → app fallback).wallacemartinss/filament-icon-picker, detected at runtime.onboarding:reset command.StepCompleted and FlowCompleted events.--fio-theme-* variables, a published
stylesheet, or published views.How can I help you explore Laravel packages today?