Tutorial: Frictionless onbaording of an organization

In this scenario, we have an onboarding process that requires:

  • to register a user and an organization at the same time in the most frictionless possible way. That means onboarding should be able to continue with a user name, email address and an organization name.
  • Verification of the e-mail address is done through an activation link.
  • A 30-day free trial
  • A one-time guided tour

Frictionless registration

DjaoApp registration end points, either through the HTML pages or API allows a flexible capture of user and billing information.

  • username
  • password
  • full name
  • email
  • street_address
  • region
  • locaility
  • postal_code
  • phone

When no password is sent along the registration, we are calling the process "frictionless registration". The user account is registered and the user is authenticated but the account is inactive. We will see in the next section what that means and how to activate an account.

By specifying an organization_name in the registration proces, the billing profile is separated from the User into an Organization (The user being granted a manager Role on that organization).

Alternative 1: Customize HTML pages

We thus create a custom theme for the accounts/register.html page.

With a frictionless registration of a user full name, email and an organization name, he minimal accounts/register.html will look like:

<html>
<head>
</head>
<body>
<form method="post" action=".">
<input type="hidden" name="csrfmiddlewaretoken" value="{{csrf_token}}">
<label>Full name</label><input name="full_name" type="text" />
<label>E-mail</label><input name="email" type="text" />
<label>Organization name</label><input name="organization_name" type="text" />
<button name="commit" type="submit" value="Sign up">Sign up</button>
</form>
</body>
</html>

There are a few issues with the previous form, one obviously is that it doesn't give any clues when registration fails (for example when an e-mail is already registered). We thus:

  1. add an HTML node for global error messages
  2. extend our <input> fields with contextual information from the template field variables.

The rewrite of our previous <form> looks like:

...

<form method="post" action=".">
<input type="hidden" name="csrfmiddlewaretoken" value="{{csrf_token}}">
<div>
    <label>Full name</label>
    <input name="full_name" type="text" {{form['full_name']|value_attr}} />
    {% for error in form['full_name'].errors %}
        <p>{{error}}</p>
    {% endfor %}
</div>
<div>
    <label>Email</label>
    <input name="email" type="text" {{form['email']|value_attr}} />
    {% for error in form['email'].errors %}
        <p>{{error}}</p>
    {% endfor %}
</div>
<div>
    <label>Organization name</label>
    <input name="organization_name" type="text" {{form['organization_name']|value_attr}} />
    {% for error in form['organization_name'].errors %}
        <p>{{error}}</p>
    {% endfor %}
</div>
<button name="commit" type="submit" value="Sign up">Sign up</button>
</form>
...

If you are using the bootstrap CSS framework, there is a Jinja2 macro available in the default theme _form_fields.html. The scaffolding is a little more complex but taking advantages of the input field macros can have huge advantages in big source code.

First we rewrite our accounts/register.html template to only deal with the layout of the page in which the form appears and include a accounts/_register_form.html template in.


<html>
  <head>
  </head>
  <body>
    <div id="messages">
    {% if form %}
    {% for message in form|messages %}
      <div>{{message}}</div>
    {% endfor %}
    {% endif %}
    </div>
    {% if form %}
        {% include "account/_register_form.html" %}
    {% endif %}
  </body>
</html>

Then in the accounts/_register_form.html template, we extend from _form.html to get the input_field macro in scope and create our registration form.

{% extends "_form.html" %}
    {% block form_block %}
    <form method="post" action=".">
      <input type="hidden" name="csrfmiddlewaretoken" value="{{csrf_token}}">
      {{ input_field(form['full_name']) }}
      {{ input_field(form['email']) }}
      {{ input_field(form['organization_name']) }}
      <button name="commit" type="submit" value="Sign up">Sign up</button>
    </form>
    {% endblock %}

Alternative 2: Call APIs

Alternatively it is possible to register a user and organization through the /api/auth/register/ API. This API will return a JSON Web Token (JWT) that authenticate the registered user.

We still need the input fields as in the HTML page case but now error handling and feedback must be handled dynamically in Javascript. Assuming we are building an application with viewjs, our page will look something like this:

<html>
<head>
</head>
<body id="app">
    <div id="messages">
        [[messages]]
    </div>
<form method="post" action="." v-on:submit="register()">
<div>
  <label>Full name</label>
  <input name="full_name" type="text" v-model="full_name" />
  <p>[[ errors['full_name'] ]]</p>
</div>
<div>
  <label>E-mail</label>
  <input name="email" type="text" v-model="email" />
  <p>[[ errors['email'] ]]</p>
</div>
<div>
  <label>Organization name</label>
  <input name="organization_name" type="text" v-model="organization_name" />
  <p>[[ errors['organization_name'] ]]</p>
</div>
<button name="commit" type="submit" value="Sign up">Sign up</button>
</form>
<script type="text/javascript" charset="utf-8" src="/static/vendor/jwt-decode.js"></script>
<script type="text/javascript" charset="utf-8" src="/static/vendor/vue.js"></script>
<script type="text/javascript" charset="utf-8" src="/static/vendor/vue-resource.js"></script>
<script type="text/javascript" charset="utf-8">
var app = new Vue({
  el: '#app',
  delimiters: ["[[","]]"],
  data: {
    full_name: null,
    email: null,
    organization_name: null,
    token: null,
    messages: "",
    errors: {}
  },
  methods: {
    register: function() {
        event.preventDefault();
        var scope = this;
        scope.$http({
            url: "/api/auth/register",
            method: "POST",
            body: JSON.stringify({
                full_name: scope.username,
                email: scope.email,
                organization_name: scope.organization_name}),
            datatype: "json",
            contentType: "application/json; charset=utf-8"
        }).then(
        function(resp) { // success
            scope.token = resp.data.token;
            // ... forwards user to an onboarding page ...
        }, function(resp) { // error
            scope.messages = resp.data.detail;
            scope.errors = resp.data;
        });
    },
  },
});
</script>
</body>
</html>

Activate user account

When no password is sent along the registration, we are calling the process "frictionless registration". The user account is registered and the user is authenticated but the account is inactive.

Any URL on the application backend logic that requires one of the following forwarding rule will require activation of the user account.

  • Active
  • Direct role for organization
  • Provider or direct role for organization
  • Provider role for organization
  • Self or role associated to user

When one of this rule is triggered and the user acount is inactive, an activation e-mail (templates/notification/verification.eml) is sent to the user, while the user is redirected to the login page where a message is displayed prompting the user to check his/her inbox. The activation e-mail contains a link back to the website in order to verify the e-mail address and activate the account.

If the user account is active, djaoapp forwards the http request once all other appropriate checks pass.

Frictionless registration and the activation process through a role-based access control rule gives us the opportunity to grant access to part of the sitemap to casual visitor while still collecting an e-mail address. It is one step up from the Any rule of public pages.

Stepper to collect an organization information

Let's say we put a Direct manager for :organization rule on the URL /app/:organization/dashboard. The authenticated user was activated the first time he/she hit the URL. Now the HTTP request is forwarded to our application logic with a session looking like:

{
  "username": "xia",
  "roles": {
    "manager": [
      {
        "slug": "cowork",
        "printable_name": "Cowork",
        "created_at": "2020-01-01T00:00:00Z",
        "email": "xia@localhost.localdomain",
        "subscriptions": []
      }
    ]
  }
}

We notice the subscriptions field in the request session is empty For example, in a Django App configured with deployutils, this can be coded as:

def is_subscribed(request, organization_slug):
    for manages in request.session.get('roles', {}).get('manager', []):
        if manages.get('slug', None) ==  organization_slug:
            return bool(manages.get('subscriptions', []))
    return False

This is the sign we are using to prompt the user with a stepper to capture additional information. The stepper page contains a form with standard fields we will pass directly to store in the djaoapp profile (street_address, locality, region, postal_code, country and phone) and some fields specific to our application that we encode as a JSON dictionary in the extra field on an organization profile.

When the user press the submit button, some bits of Javascript is making a few requests to the djaoapp API.

  1. PUT /profile/:organization/ to store the organization information.
  2. POST /cart/ to add the free trial plan to the user cart.
  3. POST /billing/:organization/checkout/ to checkout the cart and subscribe the organization to the free plan.

Since we are using a free trial plan here, we don't need a processor_token. Once we upgrade the organization to a paid plan, we will need to go through an actual visible checkout page where a user can enter a credit card.

After the various API calls have created a subscription for the free trial plan, we just reload the page. Our application logic then sends back the HTML for subscribed organizations instead of the stepper.

Starting a guided tour first time around

Here we want to display a guided tour the first time a user is landing on the application page. For guided tour, we will be using tripjs. After decorating our HTML nodes with appropriate guided tour steps, starting the tour is as easy as calling:

<script type="text/javascript">
jQuery(document).ready(function($) {
    $('body').chardinJs('start');
});
</script>

It would be quite annoying to have the guided tour start every time a user lands on the page. We want the tour to auto-start only the first time around. We need a mechanism to reliably detect that first time a user lands on a page.

The djaoapp rules contain a generic framework for measuring engagement on top of their usual usefulness for role-based access control.

We set an app label in the engagement column of the /app/:organization/dashboard URL.

With that label in place, djaoapp will track when a user engages with /app/:organization/dashboard and notify the application logic through a field in the request session.

{
  "username": "xia",
  "roles": {
    "manager": [
      {
        "slug": "cowork",
        "printable_name": "Cowork",
        "created_at": "2020-01-01T00:00:00Z",
        "email": "xia@localhost.localdomain",
        "subscriptions": [{
            "plan": "free-trial",
            "ends_at": "2020-01-31T00:00:00Z"
        }]
      }
    ]
  },
  "last_visited": null
}

last_visited will either be a datetime at which the user last visited a URL matching a djaoapp rule with an identical engagement label. For example, all rules marked with the app engagement label are equivalent in terms of engagement so the first time either is visited by a user, an engagement record is created. When a user visits any other URL rules by the same engagement label, last_visited is updated.

In our case, we are looking to start the guided tour the first time around, that is to say when last_visited is null.

With Jinja2 or Django templates, passing request in the context, we update our HTML template as such:


{% if not request.session.last_visited %}
<script type="text/javascript">
jQuery(document).ready(function($) {
    $('body').chardinJs('start');
});
</script>
{% endif %}

The guided tour now starts once, the first time the user engages with a set of URL on our application logic.