dgtlinf / user-onboarding
A lightweight, stateless user onboarding flow manager for Laravel applications.
Installs: 4
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/dgtlinf/user-onboarding
Requires
- php: ^8.2
- illuminate/support: ^10.0|^11.0|^12.0
- spatie/laravel-package-tools: ^1.92
Requires (Dev)
- orchestra/testbench: ^10.6
- pestphp/pest: ^4.1
- pestphp/pest-plugin-laravel: ^4.0
README
A lightweight, stateless user onboarding flow manager for Laravel applications. Define onboarding steps entirely in code or in configuration, use middleware to restrict access, and listen to onboarding events — all without a database.
🚀 Installation
Install the package via Composer:
composer require dgtlinf/user-onboarding
Then publish the config file and installation assets using the included install command:
php artisan user-onboarding:install
This will publish the configuration file:
config/user-onboarding.php
⚙️ Defining Flows
You can define your onboarding steps in two different ways — depending on whether your flow is static (defined in config
) or dynamic (defined programmatically).
1. Config-Based Flows (Recommended)
This is the most common approach. Define your flows in config/user-onboarding.php
:
use Dgtlinf\UserOnboarding\Step; return [ 'flows' => [ 'default' => [ Step::make('profile')->check(fn($user) => filled($user->name)), Step::make('verify_email')->check(fn($user) => $user->hasVerifiedEmail()), ], ], /* |-------------------------------------------------------------------------- | Onboarding Redirects |-------------------------------------------------------------------------- | | Define where users should be redirected when onboarding is incomplete | for a specific flow. The 'default' route is used if none match. | */ 'redirects' => [ 'default' => '/onboarding', ], ];
The middleware and facade will automatically use these definitions:
$flow = UserOnboarding::start($user, 'default');
2. Dynamic Flows (Programmatic)
You can also define steps directly in code — perfect for custom setups, role-based flows, or feature flags.
In a Controller
use Dgtlinf\UserOnboarding\Facades\UserOnboarding; use Dgtlinf\UserOnboarding\Step; public function onboarding() { $user = auth()->user(); $flow = UserOnboarding::for($user) ->addStep(Step::make('fill_profile')->check(fn($u) => filled($u->name))) ->addStep(Step::make('upload_avatar')->check(fn($u) => $u->avatar !== null)); return inertia('Onboarding/Index', [ 'steps' => $flow->steps(), 'currentStep' => $flow->current()?->slug, 'progress' => $flow->progress(), ]); }
In AppServiceProvider (Global Logic)
You can register a reusable macro for dynamic onboarding flows:
use Dgtlinf\UserOnboarding\Facades\UserOnboarding; use Dgtlinf\UserOnboarding\Step; public function boot() { UserOnboarding::macro('dynamicFlow', function ($user) { $flow = UserOnboarding::for($user) ->addStep(Step::make('profile')->check(fn($u) => filled($u->name))) ->addStep(Step::make('verify_email')->check(fn($u) => $u->hasVerifiedEmail())); if ($user->is_team_owner) { $flow->addStep(Step::make('invite_members')->check(fn($u) => $u->team->members->count() > 1)); } return $flow; }); }
Now anywhere in your app, you can call:
UserOnboarding::dynamicFlow($user)->progress();
🧬 Basic Usage
use Dgtlinf\UserOnboarding\Facades\UserOnboarding; use Dgtlinf\UserOnboarding\Step; $user = auth()->user(); $flow = UserOnboarding::for($user) ->addStep(Step::make('profile')->check(fn($u) => $u->profileCompleted())) ->addStep(Step::make('verify_email')->check(fn($u) => $u->hasVerifiedEmail())); if ($flow->isCompleted()) { // continue to dashboard } $flow->progress(); // e.g. 50.0
If you prefer configuration-based flows, use:
$flow = UserOnboarding::start($user, 'default');
🛡️ Middleware Protection
You can easily protect routes to ensure users can only access them after completing onboarding.
Protect Entire Routes
Route::middleware('onboarding.step')->group(function () { Route::get('/dashboard', DashboardController::class); });
If a user has not completed onboarding, they’ll be redirected to /onboarding
(or your configured path).
Require a Specific Step
Route::middleware('onboarding.step:verify_email')->group(function () { Route::get('/projects', ProjectsController::class); });
If the user hasn’t completed the verify_email
step, the middleware denies access.
🤓 Creating a Custom Middleware
By default, this package includes the middleware
Dgtlinf\\UserOnboarding\\Http\\Middleware\\EnsureUserOnboardingStepCompleted
.
You can register it manually or create your own to customize the behavior (for example, JSON vs. redirect responses).
1. Using the built-in middleware
Register it in your app/Http/Kernel.php
if not already auto-discovered:
protected $routeMiddleware = [ 'onboarding.step' => \\Dgtlinf\\UserOnboarding\\Http\\Middleware\\EnsureUserOnboardingStepCompleted::class, ];
Then use it in your routes:
Route::middleware('onboarding.step')->group(function () { Route::get('/dashboard', DashboardController::class); });
2. Creating your own middleware
If you need different behavior (e.g. API response instead of redirect), you can create your own middleware and use the package’s API directly:
php artisan make:middleware EnsureOnboardingForApi
namespace App\\Http\\Middleware; use Closure; use Dgtlinf\\UserOnboarding\\Facades\\UserOnboarding; class EnsureOnboardingForApi { public function handle($request, Closure $next) { $user = $request->user(); // Use a specific flow if needed $flow = UserOnboarding::start($user, 'default'); if (! $flow->isCompleted()) { // For APIs, return a JSON response instead of redirect return response()->json([ 'message' => 'User onboarding not completed', 'next_step' => $flow->current()?->slug, 'progress' => $flow->progress(), ], 403); } return $next($request); } }
Then register and use it:
Route::middleware('onboarding.api')->get('/api/profile', [ProfileController::class, 'show']);
✅ Tip: You can also use this pattern to create per-role or per-guard middleware variants:
UserOnboarding::start($user, $user->isAdmin() ? 'admin' : 'default');
🧬 Example Workflow (Blade or Inertia)
When a user tries to access a protected route, the middleware redirects them to /onboarding
. You can use the flow object to determine which step to render next.
Controller Example
use Dgtlinf\UserOnboarding\Facades\UserOnboarding; public function show() { $user = auth()->user(); $flow = UserOnboarding::start($user, 'default'); return inertia('Onboarding/Index', [ 'currentStep' => $flow->current()?->slug, 'steps' => $flow->steps()->map(fn($s) => [ 'slug' => $s->slug, 'completed' => $flow->isStepCompleted($s->slug), ])->values(), 'progress' => $flow->progress(), ]); }
Inertia/Vue Example
<template> <div class="max-w-lg mx-auto mt-10"> <h2 class="text-2xl font-bold mb-4">Onboarding Progress</h2> <progress :value="progress" max="100" class="w-full mb-4"></progress> <div v-for="step in steps" :key="step.slug" class="mb-2"> <span class="inline-block w-3 h-3 rounded-full mr-2" :class="step.completed ? 'bg-green-500' : 'bg-gray-400'" ></span> {{ step.slug }} </div> <div class="mt-6"> <component :is="getStepComponent(currentStep)" v-if="currentStep" /> </div> </div> </template> <script setup> defineProps({ currentStep: String, steps: Array, progress: Number }) function getStepComponent(slug) { switch (slug) { case 'profile': return 'OnboardingProfileStep' case 'verify_email': return 'OnboardingVerifyEmailStep' default: return 'OnboardingDone' } } </script>
Blade Example
@php $flow = UserOnboarding::start(auth()->user()); $current = $flow->current()?->slug; @endphp @if ($current === 'profile') @include('onboarding.steps.profile') @elseif ($current === 'verify_email') @include('onboarding.steps.verify-email') @else <p>All done! 🎉</p> @endif
When a user completes a step (e.g., submits a form):
UserOnboarding::for($user)->completeStep('profile');
The next time the onboarding view loads, the next step will automatically render.
🧱 Listening to Events
The package dispatches the following events automatically:
Event | Description |
---|---|
OnboardingStarted |
Fired when a flow begins via UserOnboarding::start() |
StepCompleted |
Fired when a step is completed manually or programmatically |
OnboardingCompleted |
Fired when all steps are completed |
Example Listener
use Dgtlinf\UserOnboarding\Events\StepCompleted; Event::listen(StepCompleted::class, function ($event) { Log::info('Step completed', [ 'user' => $event->user->id, 'step' => $event->step->slug, ]); });
📡 Events Overview
OnboardingStarted
→ emitted when a user begins onboardingStepCompleted
→ emitted when a user finishes a stepOnboardingCompleted
→ emitted when a flow is fully done
Each event carries the $user
and $flow
(and $step
when relevant).
🥉 Example Use Cases
- Block certain routes until user setup is finished
- Show onboarding progress bar in the UI
- Trigger reminders via queued listeners
- Log onboarding analytics and completions
- Connect with external CRM or email campaigns
⚙️ Publishing & Customization
You can re-publish configuration anytime:
php artisan vendor:publish --tag="user-onboarding-config"
🧮 Tech Notes
- Stateless: No database persistence — each step is evaluated live.
- Extensible: Add events, listeners, and custom middleware.
- Minimal: Only
spatie/laravel-package-tools
andilluminate/support
are required.
🖦 License
MIT License © Digital Infinity