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
- 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:
- add an HTML node for global error messages
- 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
<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 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.
- PUT /profile/:organization/ to store the organization information.
- POST /cart/ to add the free trial plan to the user cart.
- 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.