Tricks

Adding a Wizard to Filament Breezy's Registration Page & Adding Sections to the Profile Page

Nov 23, 2022
Andrew Wallo
Admin panel, Form builder, Integration

For this trick I will show you how to add a Wizard to Filament Breezy's Registration Flow and how to easily add new sections to the "My Profile" Page.

Scroll to the bottom if you want to see the end result.

These steps assume you already have Filament, Filament Forms, and Filament Breezy already installed and configured.

Before you begin make sure you publish the breezy views.

php artisan vendor:publish --tag="filament-breezy-views"

Steps:

1. Make a Livewire/Filament Form called "Register"

php artisan make:livewire Register

For the purpose of this tutorial/trick I will be adding more fields to my User Model and User Database File.

2. Go to your user database migration file and add the following fields or whatever you desire for your project.

<?php
 
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
 
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
// Start -- New Fields
$table->string('company_name', 100);
$table->string('website');
$table->string('address');
$table->string('logo')->nullable();
// End -- New Fields
$table->rememberToken();
$table->timestamps();
});
}
 
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
};

3. Go to your User Model and add the fields you chose.

<?php
 
namespace App\Models;
 
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use JeffGreco13\FilamentBreezy\Traits\TwoFactorAuthenticatable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
 
class User extends Authenticatable
{
use HasApiTokens;
use HasFactory;
use Notifiable;
use TwoFactorAuthenticatable;
 
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
// Start -- New Fields
'company_name',
'website',
'address',
'logo',
// End -- New Fields
];
 
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
 
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
 
}

4. Go to App/Http/Livewire/Register and add the following or similar.

<?php
 
namespace App\Http\Livewire;
 
use App\Models\User;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Facades\Hash;
use Filament\Forms\Components\Wizard;
use Filament\Forms\Components\FileUpload;
use Livewire\Component;
use Illuminate\Support\HtmlString;
use Illuminate\Contracts\View\View;
use Filament\Facades\Filament;
 
 
class Register extends Component implements HasForms
{
use InteractsWithForms;
 
public User $user;
 
public $name = '';
public $email = '';
public $password = '';
public $passwordConfirmation = '';
public $company_name = '';
public $website = '';
public $address = '';
public $logo = '';
 
public function mount(): void
{
$this->form->fill();
}
 
protected function getFormSchema(): array
{
return [
Wizard::make([
Wizard\Step::make('Personal Information')
->schema([
TextInput::make('name')
->label('Name')
->required()
->maxLength(50),
TextInput::make('email')
->label('Email Address')
->email()
->required()
->maxLength(50)
->unique(User::class),
TextInput::make('password')
->label('Password')
->password()
->required()
->maxLength(50)
->minLength(8)
->same('passwordConfirmation')
->dehydrateStateUsing(fn ($state) => Hash::make($state)),
TextInput::make('passwordConfirmation')
->label('Confirm Password')
->password()
->required()
->maxLength(50)
->minLength(8)
->dehydrated(false),
])
->columns([
'sm' => 2,
])
->columnSpan([
'sm' => 2,
]),
Wizard\Step::make('Company Information')
->schema([
TextInput::make('company_name')->label('Company Name')->required()->maxLength(50)->autofocus(),
TextInput::make('website')->label('Company Website')->prefix('https://')->maxLength(50),
TextInput::make('address')->label('Company Address')->maxLength(100),
FileUpload::make('logo')->label('Company Logo')->image()->directory('logos'),
])
])
->columns([
'sm' => 1,
])
->columnSpan([
'sm' => 1,
])
->submitAction(new HtmlString(html: '<button type="submit">Register</button>'))
 
];
}
 
public function register()
{
$user = User::create($this->form->getState());
Filament::auth()->login(user: $user, remember:true);
return redirect()->intended(Filament::getUrl('filament.pages.dashboard'));
}
 
public function render(): View
{
return view('livewire.register');
}
}

5. Go to config/filament-breezy and make sure to edit the following to this, keep everything else the same.

"enable_profile_page" => false,
 
"registration_component_path" => \App\Http\Livewire\Register::class,

6. Make a Filament page called "MyProfile".

php artisan make:filament-page MyProfile

7. Go to the page at App/Filament/Pages/MyProfile and add the following or similar.

<?php
 
namespace App\Filament\Pages;
 
use Filament\Facades\Filament;
use Filament\Forms;
use Filament\Pages\Page;
use Illuminate\Support\Facades\Hash;
use JeffGreco13\FilamentBreezy\FilamentBreezy;
use JeffGreco13\FilamentBreezy\Traits\HasBreezyTwoFactor;
 
class MyProfile extends Page
{
use HasBreezyTwoFactor;
 
protected static string $view = "filament.pages.my-profile";
 
public $user;
public $userData;
// Password
public $new_password;
public $new_password_confirmation;
// Sanctum tokens
public $token_name;
public $abilities = [];
public $plain_text_token;
protected $loginColumn;
 
public function boot()
{
// user column
$this->loginColumn = config('filament-breezy.fallback_login_field');
}
 
public function mount()
{
$this->user = Filament::auth()->user();
$this->updateProfileForm->fill($this->user->toArray());
}
 
protected function getForms(): array
{
return array_merge(parent::getForms(), [
"updateProfileForm" => $this->makeForm()
->model(config('filament-breezy.user_model'))
->schema($this->getUpdateProfileFormSchema())
->statePath('userData'),
 
// Start -- New Section for MyProfile Account Page
"updateCompanyProfileForm" => $this->makeForm()
->model(config('filament-breezy.user_model'))
->schema($this->getUpdateCompanyProfileFormSchema())
->statePath('userData'),
// End -- New Section for MyProfile Account Page
 
"updatePasswordForm" => $this->makeForm()->schema(
$this->getUpdatePasswordFormSchema()
),
"createApiTokenForm" => $this->makeForm()->schema(
$this->getCreateApiTokenFormSchema()
),
"confirmTwoFactorForm" => $this->makeForm()->schema(
$this->getConfirmTwoFactorFormSchema()
),
]);
}
 
protected function getUpdateProfileFormSchema(): array
{
return [
Forms\Components\TextInput::make('name')
->required()
->label(__('filament-breezy::default.fields.name')),
Forms\Components\TextInput::make($this->loginColumn)
->required()
->email(fn () => $this->loginColumn === 'email')
->unique(config('filament-breezy.user_model'), ignorable: $this->user)
->label(__('filament-breezy::default.fields.email')),
];
}
 
public function updateProfile()
{
$this->user->update($this->updateProfileForm->getState());
$this->notify("success", __('filament-breezy::default.profile.personal_info.notify'));
}
 
 
// Start -- New Section for "Updating" MyProfile Account Page
protected function getUpdateCompanyProfileFormSchema(): array
{
return [
Forms\Components\TextInput::make('company_name')->label('Company Name')->required()->maxLength(100)->autofocus(),
Forms\Components\TextInput::make('website')->prefix('https://')->maxLength(250)->required()->label('Website'),
Forms\Components\TextInput::make('address')->maxLength(100)->label('Address'),
Forms\Components\FileUpload::make('logo')->image()->directory('logos')->label('Company Logo'),
];
}
 
 
public function updateCompanyProfile()
{
$this->user->update($this->updateCompanyProfileForm->getState());
$this->notify("success", __('Company Information Updated'));
}
// End -- New Section for "Updating" MyProfile Account Page
 
protected function getUpdatePasswordFormSchema(): array
{
return [
Forms\Components\TextInput::make("new_password")
->label(__('filament-breezy::default.fields.new_password'))
->password()
->rules(app(FilamentBreezy::class)->getPasswordRules())
->required(),
Forms\Components\TextInput::make("new_password_confirmation")
->label(__('filament-breezy::default.fields.new_password_confirmation'))
->password()
->same("new_password")
->required(),
];
}
 
public function updatePassword()
{
$state = $this->updatePasswordForm->getState();
$this->user->update([
"password" => Hash::make($state["new_password"]),
]);
session()->forget("password_hash_web");
Filament::auth()->login($this->user);
$this->notify("success", __('filament-breezy::default.profile.password.notify'));
$this->reset(["new_password", "new_password_confirmation"]);
}
 
protected function getCreateApiTokenFormSchema(): array
{
return [
Forms\Components\TextInput::make('token_name')->label(__('filament-breezy::default.fields.token_name'))->required(),
Forms\Components\CheckboxList::make('abilities')
->label(__('filament-breezy::default.fields.abilities'))
->options(config('filament-breezy.sanctum_permissions'))
->columns(2)
->required(),
];
}
 
public function createApiToken()
{
$state = $this->createApiTokenForm->getState();
$indexes = $state['abilities'];
$abilities = config("filament-breezy.sanctum_permissions");
$selected = collect($abilities)->filter(function ($item, $key) use (
$indexes
) {
return in_array($key, $indexes);
})->toArray();
$this->plain_text_token = Filament::auth()->user()->createToken($state['token_name'], array_values($selected))->plainTextToken;
$this->notify("success", __('filament-breezy::default.profile.sanctum.create.notify'));
$this->emit('tokenCreated');
$this->reset(['token_name']);
}
 
protected function getBreadcrumbs(): array
{
return [
url()->current() => __('filament-breezy::default.profile.profile'),
];
}
 
protected static function getNavigationIcon(): string
{
return config('filament-breezy.profile_page_icon', 'heroicon-o-document-text');
}
 
protected static function getNavigationGroup(): ?string
{
return __('filament-breezy::default.profile.account');
}
 
public static function getNavigationLabel(): string
{
return __('filament-breezy::default.profile.profile');
}
 
protected function getTitle(): string
{
return __('filament-breezy::default.profile.my_profile');
}
 
protected static function shouldRegisterNavigation(): bool
{
return config('filament-breezy.show_profile_page_in_navbar');
}
}

8. Go to App/Providers/AppServiceProvider and add the following or similar.

<?php
 
namespace App\Providers;
 
use Illuminate\Support\ServiceProvider;
use Filament\Facades\Filament;
use Filament\Navigation\UserMenuItem;
 
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
 
}
 
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Filament::serving(function () {
Filament::registerUserMenuItems([
'account' => UserMenuItem::make()->url(route('filament.pages.my-profile')),
]);
});
}
}

Now we will edit the blade files for the register form we made earlier and the myprofile page.

9. Go to resources/views/livewire/register.blade.php and add the following or similar (This is using breezy's register blade file layout).

<x-filament-breezy::auth-card action="register">
<div class="w-full flex justify-center">
<x-filament::brand />
</div>
<div>
<h2 class="font-bold tracking-tight text-center text-2xl">
{{ __('filament-breezy::default.registration.heading') }}
</h2>
<p class="mt-2 text-sm text-center">
{{ __('filament-breezy::default.or') }}
<a class="text-primary-600" href="{{route('filament.auth.login')}}">
{{ strtolower(__('filament::login.heading')) }}
</a>
</p>
</div>
<form wire:submit.prevent="register" class="space-y-4 md:space-y-6">
{{ $this->form }}
</form>
</x-filament-breezy::auth-card>

10. Go to resources/views/filament/pages/my-profile.blade.php and add the following or similar (This is using breezy's myprofile blade file layout).

<x-filament::page>
 
<x-filament-breezy::grid-section class="mt-8">
 
<x-slot name="title">
{{ __('filament-breezy::default.profile.personal_info.heading') }}
</x-slot>
 
<x-slot name="description">
{{ __('filament-breezy::default.profile.personal_info.subheading') }}
</x-slot>
 
<form wire:submit.prevent="updateProfile" class="col-span-2 sm:col-span-1 mt-5 md:mt-0">
<x-filament::card>
 
{{ $this->updateProfileForm }}
 
<x-slot name="footer">
<div class="text-right">
<x-filament::button type="submit">
{{ __('filament-breezy::default.profile.personal_info.submit.label') }}
</x-filament::button>
</div>
</x-slot>
</x-filament::card>
</form>
 
</x-filament-breezy::grid-section>
 
<-- Start New Section for MyProfile Account Page -->
<x-filament-breezy::grid-section class="mt-8">
 
<x-slot name="title">
{{ __('Company Information') }}
</x-slot>
 
<x-slot name="description">
{{ __('Manage your company information') }}
</x-slot>
 
<form wire:submit.prevent="updateCompanyProfile" class="col-span-2 sm:col-span-1 mt-5 md:mt-0">
<x-filament::card>
 
{{ $this->updateCompanyProfileForm }}
 
<x-slot name="footer">
<div class="text-right">
<x-filament::button type="submit">
{{ __('Update') }}
</x-filament::button>
</div>
</x-slot>
</x-filament::card>
</form>
 
</x-filament-breezy::grid-section>
<-- End New Section for MyProfile Account Page -->
 
<x-filament::hr />
 
<x-filament-breezy::grid-section>
 
<x-slot name="title">
{{ __('filament-breezy::default.profile.password.heading') }}
</x-slot>
 
<x-slot name="description">
{{ __('filament-breezy::default.profile.password.subheading') }}
</x-slot>
 
<form wire:submit.prevent="updatePassword" class="col-span-2 sm:col-span-1 mt-5 md:mt-0">
<x-filament::card>
 
{{ $this->updatePasswordForm }}
 
<x-slot name="footer">
<div class="text-right">
<x-filament::button type="submit">
{{ __('filament-breezy::default.profile.password.submit.label') }}
</x-filament::button>
</div>
</x-slot>
</x-filament::card>
</form>
 
</x-filament-breezy::grid-section>
 
@if(config('filament-breezy.enable_2fa'))
<x-filament::hr />
 
<x-filament-breezy::grid-section class="mt-8">
 
<x-slot name="title">
{{ __('filament-breezy::default.profile.2fa.title') }}
</x-slot>
 
<x-slot name="description">
{{ __('filament-breezy::default.profile.2fa.description') }}
</x-slot>
 
 
<x-filament::card class="col-span-2 sm:col-span-1 mt-5 md:mt-0">
@if($this->user->has_enabled_two_factor)
 
@if ($this->user->has_confirmed_two_factor)
<p class="text-lg font-medium text-gray-900 dark:text-white">{{ __('filament-breezy::default.profile.2fa.enabled.title') }}</p>
{{ __('filament-breezy::default.profile.2fa.enabled.description') ?? __('filament-breezy::default.profile.2fa.enabled.store_codes') }}
@else
<p class="text-lg font-medium text-gray-900 dark:text-white">{{ __('filament-breezy::default.profile.2fa.finish_enabling.title') }}</p>
{{ __('filament-breezy::default.profile.2fa.finish_enabling.description') }}
@endif
 
<div class="mt-2">
{!! $this->twoFactorQrCode() !!}
<p>{{ __('filament-breezy::default.profile.2fa.setup_key') }} {{ decrypt($this->user->two_factor_secret) }}</p>
</div>
 
@if ($this->showing_two_factor_recovery_codes)
<hr class="my-3"/>
<p>{{ __('filament-breezy::default.profile.2fa.enabled.store_codes') }}</p>
<div class="space-y-2">
@foreach (json_decode(decrypt($this->user->two_factor_recovery_codes), true) as $code)
<span class="inline-flex items-center p-2 rounded-full text-xs font-medium bg-gray-100 text-gray-800">{{ $code }}</span>
@endforeach
</div>
{{$this->getCachedAction('regenerate2fa')}}
 
@endif
 
@else
 
<p class="text-lg font-medium text-gray-900 dark:text-white">{{ __('filament-breezy::default.profile.2fa.not_enabled.title') }}</p>
{{ __('filament-breezy::default.profile.2fa.not_enabled.description') }}
 
@endif
<x-slot name="footer">
@if($this->user->has_enabled_two_factor && $this->user->has_confirmed_two_factor)
<div class="flex items-center justify-between">
<x-filament::button color="secondary" wire:click="toggleRecoveryCodes">
{{$this->showing_two_factor_recovery_codes ? __('filament-breezy::default.profile.2fa.enabled.hide_codes') :__('filament-breezy::default.profile.2fa.enabled.show_codes')}}
</x-filament::button>
{{$this->getCachedAction('disable2fa')}}
</div>
@elseif($this->user->has_enabled_two_factor)
<form wire:submit.prevent="confirmTwoFactor">
<div class="flex items-center justify-between">
<div>{{$this->confirmTwoFactorForm}}</div>
<div class="mt-5">
<x-filament::button type="submit">
{{ __('filament-breezy::default.profile.2fa.actions.confirm_finish') }}
</x-filament::button>
 
<x-filament::button color="secondary" wire:click="disableTwoFactor">
{{ __('filament-breezy::default.profile.2fa.actions.cancel_setup') }}
</x-filament::button>
</div>
</div>
</form>
@else
<div class="text-right">
{{$this->getCachedAction('enable2fa')}}
</div>
@endif
</x-slot>
</x-filament::card>
 
</x-filament-breezy::grid-section>
@endif
 
@if(config('filament-breezy.enable_sanctum'))
<x-filament::hr />
 
<x-filament-breezy::grid-section class="mt-8">
 
<x-slot name="title">
{{ __('filament-breezy::default.profile.sanctum.title') }}
</x-slot>
 
<x-slot name="description">
{{ __('filament-breezy::default.profile.sanctum.description') }}
</x-slot>
 
<div class="space-y-3">
 
<form wire:submit.prevent="createApiToken" class="col-span-2 sm:col-span-1 mt-5 md:mt-0">
 
<x-filament::card>
@if($plain_text_token)
<input type="text" disabled @class(['w-full py-1 px-3 rounded-lg bg-gray-100 border-gray-200',' dark:bg-gray-900 dark:border-gray-700'=>config('filament.dark_mode')]) name="plain_text_token" value="{{$plain_text_token}}" />
@endif
 
{{$this->createApiTokenForm}}
 
<div class="text-right">
<x-filament::button type="submit">
{{ __('filament-breezy::default.profile.sanctum.create.submit.label') }}
</x-filament::button>
</div>
</x-filament::card>
</form>
 
<x-filament::hr />
 
@livewire(\JeffGreco13\FilamentBreezy\Http\Livewire\BreezySanctumTokens::class)
 
</div>
</x-filament-breezy::grid-section>
@endif
 
</x-filament::page>

All finished! Hopefully I didn't leave anything out but if you followed everything you should have this as the end result. Also If you think the Registration page layout is too cluttered you can adjust the width of the form in breezy's config File. Screenshot 2022-11-23 061130 Screenshot 2022-11-23 061053 Filament My Profile Page

avatar

Great trick, thanks!! But I suggest updating the 4th step's render method, it may cause a $wire not defined error depending on your layouts:

public function render()
{
/* -- optional: this line changes the default width for this view only -- */
config(['filament-breezy.auth_card_max_w' => '4xl']);
/* ---- */
$view = view('livewire.register');
$view->layout('filament::components.layouts.base', [
'title' => __('Register'),
]);
return $view;
}