Invite Users to Laravel Jetstream Team

Kevin Kirchner
6 min readJan 15, 2021

--

UPDATE 2: We still didn’t like the default userflow from Jetstream, so we made our own composer package. Check it out here for the easy way to set this up: https://github.com/truefrontier/jetstream-team-invites

UPDATE 1: As of the writing of this, Laravel Jetstream doesn’t allow you to invite users who are not already registered. (It will when v2 is released! See GitHib issue #228 and #345 for more details.)

Here’s how I did it.

  1. First, create all the files needed for an Invitation model.
    $ php artisan make:model -a Invitation

The Invitation model will be attached to a User (the one who is sending the invite) and a Team. It will also use Jetstream’s built-in role feature.

2. Next, edit migration file: create_invitations_table.php

<?phpuse Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateInvitationsTable extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up() {
Schema::create('invitations', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id');
$table->foreignId('team_id');
$table->string('role')->nullable();
$table->string('email');
$table->string('code');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down() {
Schema::dropIfExists('invitations');
}
}

3. Next, create the Notification that will send the email invitation:
$ php artisan make:notification InviteTeamMember

4. Edit the notification, app/Notifications/InviteTeamMember.php
This sets up the email invitation being sent. FYI:$notifiable will be an instance of the Invitation model.

<?phpnamespace App\Notifications;use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class InviteTeamMember extends Notification {
use Queueable;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct() {
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable) {
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable) {
$userName = $notifiable->user->name;
$teamName = $notifiable->team->name;
return (new MailMessage)
->subject("$userName invites you to join $teamName")
->line("$userName wants you to join their team: $teamName")
->action('Create Your Account', route('register', ['email' => $notifiable->email, 'invite' => $notifiable->code]))
->line('Looking forward to having you on the team!');
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable) {
return [
//
];
}
}

5. Create a trait to enable the Invitation model with the above Notification. I created the file in app/Traits/InvitesTeamMembers.php (By the way, if you see a way to improve this, let me know!)

<?phpnamespace App\Traits;use App\Notifications\InviteTeamMember;trait InvitesTeamMembers {/**
* Send the email verification notification.
*
* @return void
*/
public function sendEmailInviteNotification() {
$this->notify(new InviteTeamMember);
}
}

6. Update app/Models/Invitation.php

<?phpnamespace App\Models;use App\Traits\InvitesTeamMembers;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Str;
class Invitation extends Model {
use HasFactory;
use Notifiable;
use InvitesTeamMembers;
protected $fillable = [
'user_id',
'team_id',
'role',
'email',
'code',
];
protected static function booted() {
static::creating(function ($invitation) {
$invitation->code = $invitation->code ?: Str::random(40);
return $invitation;
});
}
public function user() {
return $this->belongsTo('App\Models\User');
}
public function team() {
return $this->belongsTo('App\Models\Team');
}
}

7. Update relationships with app/Models/Team.php and app/Models/User.php by adding the following:

    public function invitations() {
return $this->hasMany('App\Models\Invitation');
}

8. Update Jetstream’s AddTeamMember action: app/Actions/Jetstream/AddTeamMember.php NOTE: Edits are wrapped with ## BEGIN EDIT ## style comments (Also, don’t forget the additional use declarations used here.)

<?phpnamespace App\Actions\Jetstream;use App\Models\Invitation;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator;
use Laravel\Jetstream\Contracts\AddsTeamMembers;
use Laravel\Jetstream\Events\TeamMemberAdded;
use Laravel\Jetstream\Jetstream;
use Laravel\Jetstream\Rules\Role;
class AddTeamMember implements AddsTeamMembers {
/**
* Add a new team member to the given team.
*
* @param mixed $user
* @param mixed $team
* @param string $email
* @param string|null $role
* @return void
*/
public function add($user, $team, string $email, string $role = null) {
Gate::forUser($user)->authorize('addTeamMember', $team);
$this->validate($team, $email, $role);## BEGIN EDIT - Send invite if user is not in the system ##
$newTeamMember = User::where('email', $email)->first();
if ($newTeamMember) {
## END EDIT ##
$team->users()->attach(
$newTeamMember = Jetstream::findUserByEmailOrFail($email),
['role' => $role]
);
TeamMemberAdded::dispatch($team, $newTeamMember);
## BEGIN EDIT - Send invite if user is not in the system ##
} else {
$invitation = Invitation::create([
'user_id' => $user->id,
'team_id' => $team->id,
'role' => $role,
'email' => $email,
]);
$invitation->sendEmailInviteNotification();
}
## END EDIT ##
}
/**
* Validate the add member operation.
*
* @param mixed $team
* @param string $email
* @param string|null $role
* @return void
*/
protected function validate($team, string $email, ? string $role) {
Validator::make([
'email' => $email,
'role' => $role,
], $this->rules(), [
'email.exists' => __('We were unable to find a registered user with this email address.'),
])->after(
$this->ensureUserIsNotAlreadyOnTeam($team, $email)
)->validateWithBag('addTeamMember');
}
/**
* Get the validation rules for adding a team member.
*
* @return array
*/
protected function rules() {
## BEGIN EDIT - comment out exists:users check ##
return array_filter([
'email' => [
'required',
'email',
// 'exists:users',
],
'role' => Jetstream::hasRoles()
? ['required', 'string', new Role]
: null,
]);
## END EDIT ##
}
/**
* Ensure that the user is not already on the team.
*
* @param mixed $team
* @param string $email
* @return \Closure
*/
protected function ensureUserIsNotAlreadyOnTeam($team, string $email) {
return function ($validator) use ($team, $email) {
$validator->errors()->addIf(
$team->hasUserWithEmail($email),
'email',
__('This user already belongs to the team.')
);
};
}
}

9. Update the registration form resources/views/auth/register.blade.php. The Notification we created has a link that adds the invitation email and code to the URL as query params (e.g. /register?email=name@example.com&invite=[40 random characters]). These changes will use those params for the registration form so when they register, we know what team they were invited to.

...            <div class="mt-4">
<x-jet-label for="email" value="{{ __('Email') }}" />
@if (app('request')->input('email'))
<x-jet-input id="email" class="block mt-1 w-full" type="email" name="email" value="{{ app('request')->input('email') }}" required />
@else
<x-jet-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required />
@endif
</div>
@if (app('request')->input('invite'))
<x-jet-input id="invite" class="hidden" type="hidden" name="invite" value="{{ app('request')->input('invite') }}" required />
@endif
...

10. Next, we need to use those params on the form submission. We can edit app/Actions/Fortify/CreateNewUser.php to do this. NOTE: the changes are wrapped in ## BEGIN EDIT ## comments, but also include the use ($input) on line 35 and the other use declarations at the top of the file.

<?phpnamespace App\Actions\Fortify;use App\Models\Invitation;
use App\Models\Team;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\CreatesNewUsers;
use Laravel\Jetstream\Events\TeamMemberAdded;
class CreateNewUser implements CreatesNewUsers {
use PasswordValidationRules;
/**
* Create a newly registered user.
*
* @param array $input
* @return \App\Models\User
*/
public function create(array $input) {
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => $this->passwordRules(),
])->validate();
return DB::transaction(function () use ($input) {
return tap(User::create([
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
]), function (User $user) use ($input) {
$this->createTeam($user);
## BEGIN EDIT - if there's an invite, attach them accordingly ##
if (isset($input['invite'])) {
if ($invitation = Invitation::where('code', $input['invite'])->first()) {
if ($team = $invitation->team) {
$team->users()->attach(
$user,
['role' => $invitation->role]
);
$user->current_team_id = $team->id;
$user->save();
TeamMemberAdded::dispatch($team, $user);$invitation->delete();
}
}
}
## END EDIT ##
});
});
}
/**
* Create a personal team for the user.
*
* @param \App\Models\User $user
* @return void
*/
protected function createTeam(User $user) {
$user->ownedTeams()->save(Team::forceCreate([
'user_id' => $user->id,
'name' => explode(' ', $user->name, 2)[0] . "'s Team",
'personal_team' => true,
]));
}
}

11. OPTIONAL: Update the message when you submit the invite form in resources/js/Pages/Teams/TeamMemberManager.vue on line 94. I changed Added to Invited.

12. 🎉 Done! Let me know if I missed anything: kevin@truefrontierapps.com

--

--

Kevin Kirchner
Kevin Kirchner

Written by Kevin Kirchner

Creating new revenue streams in 30 days or less with custom web apps at truefrontierapps.com

No responses yet