DEV Community

Freek Van der Herten
Freek Van der Herten

Posted on • Originally published at freek.dev on

★ Sending a welcome notification to new users of a Laravel app

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

Top comments (0)