This lesson got separated as it's a crucial part of the application - sending out an invitation email to an employee and allowing them to register to the system:
In this lesson, we will do the following:
- Create Invitation Model and Database tables
- Modify UserResource Create button action - to invite the Employee
- Email the invitation to the Employee
- Create a custom page that will be signature (Laravel Signer URL) protected
- Create a custom registration form for the Employee
Create Invitation Model and Database tables
Let's create our migration:
Migration
Schema::create('invitations', function (Blueprint $table) { $table->id(); $table->string('email'); $table->timestamps();});
Then, we can fill our Model:
app/Models/Invitation.php
class Invitation extends Model{ protected $fillable = [ 'email', ];}
As you can see from the setup, it's a pretty basic Model. All we care about - is the email address being invited.
Modify UserResource Create Button Action - to Invite the Employee
Next on our list, we need to modify the User Create button. We don't want to create the User right away. We want to invite them via email first. So let's work on that:
app/Filament/Resources/UserResource/Pages/ListUsers.php
use App\Models\Invitation;use Filament\Forms\Components\TextInput;use Illuminate\Support\Facades\Mail;use Filament\Notifications\Notification; // ... protected function getHeaderActions(): array{ return [ Actions\CreateAction::make(), Actions\Action::make('inviteUser') ->form([ TextInput::make('email') ->email() ->required() ]) ->action(function ($data) { $invitation = Invitation::create(['email' => $data['email']]); // @todo Add email sending here Notification::make('invitedSuccess') ->body('User invited successfully!') ->success()->send(); }), ];}
Once this is done, we will see a different button on our UI:
Creating Custom Registration Page
Next, we need to create a custom page where our users will land when they click on the invitation link:
php artisan make:livewire AcceptInvitation
Note: We have changed the whole file, so there are no difference indicators.
app/Livewire/AcceptInvitation.php
use App\Models\Invitation;use App\Models\Role;use App\Models\User;use Filament\Actions\Action;use Filament\Actions\ActionGroup;use Filament\Forms\Components\TextInput;use Filament\Forms\Concerns\InteractsWithForms;use Filament\Forms\Form;use Filament\Pages\Concerns\InteractsWithFormActions;use Filament\Pages\Dashboard;use Filament\Pages\SimplePage;use Illuminate\Validation\Rules\Password; class AcceptInvitation extends SimplePage{ use InteractsWithForms; use InteractsWithFormActions; protected static string $view = 'livewire.accept-invitation'; public int $invitation; private Invitation $invitationModel; public ?array $data = []; public function mount(): void { $this->invitationModel = Invitation::findOrFail($this->invitation); $this->form->fill([ 'email' => $this->invitationModel->email ]); } public function form(Form $form): Form { return $form ->schema([ TextInput::make('name') ->label(__('filament-panels::pages/auth/register.form.name.label')) ->required() ->maxLength(255) ->autofocus(), TextInput::make('email') ->label(__('filament-panels::pages/auth/register.form.email.label')) ->disabled(), TextInput::make('password') ->label(__('filament-panels::pages/auth/register.form.password.label')) ->password() ->required() ->rule(Password::default()) ->same('passwordConfirmation') ->validationAttribute(__('filament-panels::pages/auth/register.form.password.validation_attribute')), TextInput::make('passwordConfirmation') ->label(__('filament-panels::pages/auth/register.form.password_confirmation.label')) ->password() ->required() ->dehydrated(false), ]) ->statePath('data'); } public function create(): void { $this->invitationModel = Invitation::find($this->invitation); $user = User::create([ 'name' => $this->form->getState()['name'], 'password' => $this->form->getState()['password'], 'email' => $this->invitationModel->email, 'role_id' => Role::where('name', 'Employee')->first()->id ]); auth()->login($user); $this->invitationModel->delete(); $this->redirect(Dashboard::getUrl()); } /** * @return array<Action | ActionGroup> */ protected function getFormActions(): array { return [ $this->getRegisterFormAction(), ]; } public function getRegisterFormAction(): Action { return Action::make('register') ->label(__('filament-panels::pages/auth/register.form.actions.register.label')) ->submit('register'); } public function getHeading(): string { return 'Accept Invitation'; } public function hasLogo(): bool { return false; } public function getSubHeading(): string { return 'Create your user to accept an invitation'; }}
Next, we need to modify our View:
resources/views/livewire/accept-invitation.blade.php
<x-filament-panels::page.simple> <x-filament-panels::form wire:submit="create"> {{ $this->form }} <x-filament-panels::form.actions :actions="$this->getCachedFormActions()" :full-width="true" /> </x-filament-panels::form></x-filament-panels::page.simple>
Then, all we have to do is add the route:
routes/web.php
use App\Livewire\AcceptInvitation; // ... Route::middleware('signed') ->get('invitation/{invitation}/accept', AcceptInvitation::class) ->name('invitation.accept');
Note: This uses a signed route middleware and Livewire full-page component.
Creating and Sending the Email
As our last step, we will create the email and send it to the User:
php artisan make:mail TeamInvitationMail
Then we can modify the email:
app/Mail/TeamInvitationMail.php
use App\Models\Invitation; // ... private Invitation $invitation; public function __construct()public function __construct(Invitation $invitation){ $this->invitation = $invitation; } public function envelope(): Envelope{ return new Envelope( subject: 'Team Invitation Mail', subject: 'Invitation to join ' . config('app.name'), );} public function content(): Content{ return new Content( view: 'view.name', markdown: 'emails.team-invitation', with: [ 'acceptUrl' => URL::signedRoute( "invitation.accept", ['invitation' => $this->invitation] ), ] );}
And, of course, let's create the view file:
resources/views/emails/team-invitation.blade.php
<x-mail::message>You have been invited to join {{ config('app.name') }} To accept the invitation - click on the button below and create an account: <x-mail::button :url="$acceptUrl">{{ __('Create Account') }}</x-mail::button> {{ __('If you did not expect to receive an invitation to this team, you may discard this email.') }}</x-mail::message>
The last step before we try everything out is to send it. Remember the @todo
that we left? Let's replace it with our email:
app/Filament/Resources/UserResource/Pages/ListUsers.php
use App\Mail\TeamInvitationMail; // ... protected function getHeaderActions(): array{ return [ Actions\Action::make('inviteUser') ->form([ TextInput::make('email') ->email() ->required() ]) ->action(function ($data) { $invitation = Invitation::create(['email' => $data['email']]); Mail::to($invitation->email)->send(new TeamInvitationMail($invitation)); Notification::make('invitedSuccess') ->body('User invited successfully!') ->success()->send(); }), ];}
That's it! We can now email invites to people:
And once they click the link - they will see the registration page:
That's it! Once they fill out the form - they will be redirected to the dashboard with the Employee role:
i have finish register from invitation email. but when registrating it cannot move to admin/login route and the email_verfied_at and remember_token is null so the invited account cannot used to login
In this tutorial we did not talk about the email verification, so you can add that manually.
remember me token - that does not matter, no? And for the redirect - I'm not sure what you mean
Thanks for you answer sir.
Sir, i have problem when it finish sign up, it show route [login] not defined
Could you expand on this issue? I'm not sure what the problem is
You should sign up in different browser or just use incognito mode.
Nice approach; I've been thinking about this subject for some time now and quite like your take.
Is there a reason you used a Mailable instead of a Notification (which I personally prefer)?
I'll also be looking to implement role selection when creating the invitation and making the invitation expire after a certain period, either by using temporarySignedRoute() or by setting timestamp in a column in the invitations table.
Mailable was used since we don't have an active user (I personally like to separate Notifications (user in system can receive them) and Mailables (users not in system get them)). Other than that - you can change it how you like it :)
As for the role - you can add that to invitations table for sure! That was something we specifically skipped as it depends. Everyone wants the proccess a little bit different :)
Thanks, yes that totally makes sense.
There appears to be a bug in this example; in AcceptInvitation.php you are Hashing the password twice. Once in the Field configuration and another time when you create the new User instance.
Oops! Updating the lesson :)
Thanks, I was struggling to understand why the password didn't match until I discovered what was going on:wink:
I keep getting this error: any ideas? Target [Filament\Forms\Contracts\HasForms] is not instantiable while building [Filament\Forms\Form].
Could you expand on this a bit more? I'm not sure what you were doing to get this error.
ps. Do not cross post on multiple articles ;)
In the file /app/Livewire/AcceptInvitation.php the line all the way on top:
namespace App\Livewire;
is missing. Maybe logical but I would add it for clarity. Great example btw, very useful for my project!
Edit: Wait and one other thing that i noticed, make sure that there is no identation in the blade (resources/views/emails/team-invitation.blade.php) of the email, markdown sees that as formatting. That all has to be removed.
im getting this error when i try to invite a user.
Connection could not be established with host "mailpit:1025": stream_socket_client(): php_network_getaddresses: getaddrinfo for mailpit failed: No such host is known.
This error means that you have misconfigured your .env MAIL settings. Please check them and make sure they are correct
In here:
Is it possible to disable the submit button after submittion? Thanks!
Property [$form] not found on component: [accept-invitation] I'm getting this error.
This usually means that the
accept-invitation
page is incorrectly set up. Double check that everything is thereI have the same :(
Great tutorial! How can I change the primary color, since the form is outside of my Filament panel?
I think I have figured it out:
https://filamentphp.com/docs/3.x/support/colors
Add this to the AppServiceProvider
hello thanks for the great tutorial! but i have some issue on email formatting
I have no idea how to solve this.
in email format Constanly getting this some <table line Do you have some suggestion??
It would be very appreciated! thanks!
I'm not sure what you have in mind here, sorry
use this teplate for resources/views/emails/team-invitation.blade.php
without double line breaks