Filament: User Registration by Email Invitations

Once you create a Filament User resource - you will get a typical user creation form, which will instantly create a new user. What if you want to send an invitation first and allow the User to join from an email? You can do that, too, but there is some custom work needed.

Let's build an invitation System for our Filament!


Setup

Our project setup includes the following:

  • Fresh Laravel project
  • Filament installation
  • Filament setup

Next, we will work on the default User model and resource.


Creating User Resource

We have to create our User resource to manage our Users. Let's do this:

php artisan make:filament-resource User --generate

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 User

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\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,
]);
 
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 a fresh account:


This example is taken from our own CRM Course

avatar

Did some small changes in my invite, made a check first if the user allready exist, and change the create to updateOrCreate, so I don't end up with the same email multiple times in the database

	$userExists = User::where('email', $data['email'])->exists();

	if ($userExists) {
		Notification::make('userExists')
			->body('User with this email already exists!')
			->danger()->send();

		return;
	}

	$invitation = Invitation::updateOrCreate([
		'email' => $data['email'],
	], [
		'email' => $data['email'],
		'role_id' => $data['role_id'],
		'inviter_id' => auth()->check() ? auth()->id() : null,
	]);

	Mail::to($invitation->email)->send(new TeamInvitationMail($invitation));

Altso added roles, so in App/Livewire/AcceptInvitation.php I changed the line on create to

'role_id' => $this->invitationModel->role_id, // Role::where('name', 'Employee')->first()->id,
👍 3
🥳 1
👀 1
avatar

Thank you for this! For the unique check I just use this one liner:

->unique(User::class, 'email')

in the Add E-Mail Form

->form([
		TextInput::make('email')
				->email()
				->required()
				->unique(User::class, 'email')
])
avatar
You can use Markdown
avatar

In one of your classes you created a step two login. My question is can that be done with filament as well? And if so; can you create a tutorial showing us how to do it?

avatar

Can you expand on this and point to the lesson you are referring?

By the way, Filament login is just a basic login page out of the box. You can go in and customise it however you need!

avatar
You can use Markdown
avatar

Ive upgraded Laravel 9 to Laravel 10 and installed Filament, set up User Resource etc and started to follow your tutorial with swapping the User Resource button to Invite.

Adding the routes/web.php code is throwing the following:

https://flareapp.io/share/v5pxA0E5

avatar

Did you import the route? Also, maybe the upgrade path made something different - I'm not sure.

avatar

The top of my routes/web.php:

use App\Livewire\AcceptInvitation;
avatar

Hmm, not sure what could be wrong here. Are you on livewire 3?

avatar

Livewire 3.2.6 and Filament 3.1.15

Just doing some googling now and will get back to you if I figure it out

avatar

Found the problem: My Model had a typo in the name, so it was throwing a type error

avatar
You can use Markdown
avatar

Question can this be done using a Role? Say you are hiring someone and want them to fill out a different Registration form that has different questions related to the job that they are being hired for.

And if so can you give an example for us?

avatar

To achieve this, you simply add the role to your invitation table. From there, it's as simple as displaying different form fields for each of the roles.

Not sure if this is worth covering as it is very simple..

avatar
You can use Markdown
avatar
You can use Markdown

Recent New Courses