My team at Spatie is currently building Mailcoach, a solution to self host your e-mail newsletter. Mailcoach can be used as stand alone software or as a Laravel package. Subscribe now at Mailcoach to get a notification as soon as we release it.

Sending a welcome notification to new users of a Laravel app

Original – by Freek Van der Herten – 7 minute read

My team and I currently building Mailcoach, a solution to self-host newsletters and email campaigns. In Mailcoach you can create new users to use the app.

How should these new users be onboarded? The easy way out would be to send these new users a default password reset notification to those users, but that isn't a good first experience. The default auth scaffold by Laravel doesn't help us here: it only contains functionality to log in and to let users register themselves.

To onboard new users created by other users, I've created a package called laravel-welcome-notification which can send a welcome notification to new users that allows them to set an initial password.

In this blogpost I'd like to explain how you can use the package).

Sending a welcome notification

After installing the package, you can send a welcome notification by calling the sendWelcomeNotification on a newly created user.

$expiresAt = now()->addDays(3);

$user->sendWelcomeNotification($expiresAt);

By default, this method will send a mail to the users with a link to a welcome screen where the user can set an initial passwords. The sendWelcomeNotification accepts a Carbon instance that determines when that welcome link will be expire.

The link that will be mailed is a signed url. This will make sure that only urls generated by your app can be used to display and use the welcome screen.

Customising the mail sent

By experience I know that a lot of times there will be a need to customise the mail sent to a user.

To customise the mail you should extend Spatie\WelcomeNotification\WelcomeNotification provided by the package and override the buildWelcomeNotificationMessage method.

class MyCustomWelcomeNotification extends WelcomeNotification
{
    public function buildWelcomeNotificationMessage(): Illuminate\Notifications\Messages\MailMessage
    {
        return (new MailMessage)
            ->subject('Welcome to my app')
            ->action(Lang::get('Set initial password'), $this->showWelcomeFormUrl)
    }
}

Next, you must add a method called sendWelcomeNotification to your User model.

public function sendWelcomeNotification(\Carbon\Carbon $validUntil)
{
    $this->notify(new MyCustomWelcomeNotification($validUntil));
}

And with that in place you have total freedom of how the notification should look like.

Customising the welcome form

When the user click the welcome link in the sent mail, a welcome form will be displayed. The user can use this form to set an initial password.

To style and customise the form you should publish it

php artisan vendor:publish --provider="Spatie\WelcomeNotification\WelcomeNotificationServiceProvider" --tag="views"

In resources/vendor/welcome-notification/welcome.blade.php you now have full access to the html of the form.

Customising the behaviour

The installation instructions of the package explain how you should set up WelcomeController of your own.

This WelcomeController allows you to customise what should happen after a user has set an initial password.

After the a user has successfully set a new password the sendPasswordSavedResponse of the WelcomeController will get called.

class MyWelcomeController extends BaseWelcomeController
{
    public function sendPasswordSavedResponse()
    {
        // maybe also set a flash message here
    
        return redirect()->route('home');
    }
}

If you added extra fields to the welcome form you can add a rules function to validate them.

Here's an example where we want to validate an extra field named job_title.

class MyWelcomeController extends BaseWelcomeController
{
    public function rules()
    {
        return [
            'password' => 'required|confirmed|min:6',
            'job_title' => 'required',
        ];
    }
}

How it works behind the scenes

Welcome links should only be allowed to use once. Otherwise those links, which probably have a long expiration period, can be used after the user sets an initial password.

In a first implementation of the package, we piggy backed on Laravel's password reset tokens. The benefit by doing that was that when a token is used, it will be deleted by Laravel, so the welcome link wouldn't work twice. The big draw back of this approach was that the expiration time of a welcome link would be tied to the expiration time set for password resets.

Password reset links should expire fast. When a user initiates a password reset, he or she will fairly quickly check if the reset mail arrived. An expiration time of an hour or so is enough for most cases. But when new users are created by other users, those new users aren't checking their inboxes. Probably users will click welcome links after more than an hour. In most cases

After a gentle nudge by Joseph Silber we decided to swap out token based URLs for signed URLs. The problem we had to solve with signed URLs is that they can't be invalidated.

Luckily the solution is simple. In addition on only relying on the expiration of a signed URL. The package will save the expiration time in the users table.

protected function initializeNotificationProperties(User $user)
{
    $this->user = $user;

    $this->user->welcome_valid_until = $this->validUntil;
    $this->user->save();

    $this->showWelcomeFormUrl = URL::temporarySignedRoute(
        'welcome', $this->validUntil, ['user' => $user->id]
    );
}

Whenever a password is set using the welcome form, the welcome_valid_until field is set to null.

public function savePassword(Request $request, User $user)
{
    $request->validate($this->rules());

    $user->password = bcrypt($request->password);
    $user->welcome_valid_until = null;
    $user->save();

    auth()->login($user);

    return $this->sendPasswordSavedResponse();
}

The middleware that protects the welcome routes checks if the signed URL is valid and if the welcome_valid_until field contains a date that is not in past.

public function handle($request, Closure $next)
{
  if (! $request->hasValidSignature()) {
	abort(Response::HTTP_FORBIDDEN, 'The welcome link does not have a valid signature or is expired.');
  }

  if (! $request->user) {
	abort(Response::HTTP_FORBIDDEN, 'Could not find a user to be welcomed.');
  }

  if (is_null($request->user->welcome_valid_until)) {
	return abort(Response::HTTP_FORBIDDEN, 'The welcome link has already been used.');
  }

  if (Carbon::create($request->user->welcome_valid_until)->isPast()) {
	return abort(Response::HTTP_FORBIDDEN, 'The welcome link has expired.');
  }

  return $next($request);
}

With all of this in place our two goals are achieved:

  • welcome links can have an expire time separate from password resets
  • welcome links can only be used once

In closing

Personally I've coded up this logic a couple of times in separate projects (here's an old blogpost about it), so it made sense to package it up. This isn't the first package that my team has created. Check out this big list of packages we've created previously.

This package was created to be used in upcoming Mailcoach application and package. Mailcoach will allow you to self host your newsletters and email campaigns. To get updates on the project and get notified when it gets released, sign up to the email list at mailcoach.app

Stay up to date with all things Laravel, PHP, and JavaScript.

Follow me on Twitter. I regularly tweet out programming tips, and what I myself have learned in ongoing projects.

Every two weeks I send out a newsletter containing lots of interesting stuff for the modern PHP developer.

Expect quick tips & tricks, interesting tutorials, opinions and packages. Because I work with Laravel every day there is an emphasis on that framework.

Rest assured that I will only use your email address to send you the newsletter and will not use it for any other purposes.

Comments

You can comment on this post by replying to this tweet.
Mattias Geniar liked on 19th November 2019
Mark Myers liked on 19th November 2019
Josh Salway liked on 19th November 2019
Thibault Lavoisey liked on 19th November 2019
Thibault Lavoisey liked on 19th November 2019
Rajesh Dewle liked on 19th November 2019
Felipe ? liked on 19th November 2019
Ryan Colson liked on 16th November 2019
Ryan Colson retweeted on 16th November 2019
Nick retweeted on 16th November 2019
Miguel Ángel Sánchez liked on 16th November 2019
Alex Pierre liked on 16th November 2019
Matt Kingshott ? liked on 16th November 2019
Luca Degasperi liked on 16th November 2019
Tiagosimoes liked on 15th November 2019
Felipe Podestá liked on 15th November 2019
Flamur Mavraj liked on 15th November 2019
Clevon Noel ?? liked on 15th November 2019
Ruud van Zuidam liked on 15th November 2019
Neil Keena liked on 15th November 2019
Neil Keena retweeted on 15th November 2019
Wyatt liked on 15th November 2019
Nipuna Fonseka retweeted on 15th November 2019
Feras Shaer ?? liked on 15th November 2019
Nipuna Fonseka liked on 15th November 2019
Zayn Buksh liked on 15th November 2019
Swapnil Bhavsar liked on 15th November 2019
Grant Williams liked on 15th November 2019
Israel Araujo liked on 15th November 2019
pxgamer liked on 15th November 2019
Wyatt liked on 15th November 2019
wahlemedia | Philipp liked on 15th November 2019
wahlemedia | Philipp retweeted on 15th November 2019
Baha2r liked on 15th November 2019
/dev/clipboard liked on 15th November 2019
«mauro» liked on 15th November 2019
Casper Sørensen liked on 15th November 2019
Someone's husband liked on 15th November 2019
Marcel Pociot ? retweeted on 15th November 2019
Joseph Silber replied on 15th November 2019
"A gentle nudge" ?
Martin Medina liked on 15th November 2019
Freek Van der Herten replied on 15th November 2019
Thanks!
Manojkiran liked on 15th November 2019
Neil Carlo Faisan Sucuangco liked on 15th November 2019
oluwajubelo loves VueJS ? retweeted on 15th November 2019
Sami Mansour liked on 15th November 2019
oluwajubelo loves VueJS ? liked on 15th November 2019
Gaurav Makhecha replied on 15th November 2019
And it's your 1500th post!?!
Spatie retweeted on 15th November 2019
bahd liked on 15th November 2019
Manojkiran retweeted on 15th November 2019
Freek Van der Herten replied on 15th November 2019
Yes! That's more or less mentioned in the footer :-)
im.mister.j liked on 15th November 2019
Guus liked on 15th November 2019
Monney Arthur liked on 15th November 2019
Jesse Kramer replied on 15th November 2019
Do you use mailcoach for sending the mailcoach newsletter? ?
Parthasarathi G K liked on 15th November 2019
Ahmad Ripaldi liked on 15th November 2019
Anthony liked on 15th November 2019
Andrew Broberg liked on 15th November 2019
Miguel Piedrafita ? liked on 15th November 2019
Cyril de Wit liked on 15th November 2019
Noe liked on 15th November 2019
Jimmy Lipham liked on 15th November 2019
Serge Byishimo liked on 15th November 2019
abhishek retweeted on 15th November 2019
Jess Archer liked on 15th November 2019
Serge Byishimo replied on 15th November 2019
i was coding this by myself and i didn't know that signed URLs remain valid until they are expired, so that was a flaw in my code but now thanks there is a package for this! cant wait for Mailcoach!
Reem AlTurki retweeted on 15th November 2019
José Cage liked on 15th November 2019
José Cage liked on 15th November 2019
Kalle Palokankare liked on 15th November 2019
\Zloi\Freelancer retweeted on 15th November 2019
\Zloi\Freelancer liked on 15th November 2019
Dries Vints retweeted on 15th November 2019
Mithicher Baro liked on 15th November 2019
gus liked on 15th November 2019
abdo shaban liked on 15th November 2019
Faruk liked on 15th November 2019
Zubair Mohsin liked on 15th November 2019
Mithicher Baro retweeted on 15th November 2019
Richard van Baarsen liked on 15th November 2019