Skip to content

Instantly share code, notes, and snippets.

@kevprice83
Last active March 18, 2024 21:09
Show Gist options
  • Save kevprice83/5c673c074fde190771e06967bf8ae232 to your computer and use it in GitHub Desktop.
Save kevprice83/5c673c074fde190771e06967bf8ae232 to your computer and use it in GitHub Desktop.
3scale Developer portal custom signup flows

Dynamic 3scale Developer Portal signup templates

There are 4 custom signup flows included in the parent homepage. These flows are included into the homepage using Liquid tags such as {% include 'partial name' %} because the flows are separated out into individual partials. The partials can be included in your 3scale portal individually or all together depending on which flows you want to enable in your portal and for ease of switching between flows as and when needed.

NOTE: If you prefer you can create a separate page to render the subscription forms to the different Services. This would be particularly useful if you want to allow multiple applications per account for the Custom Field & Group Membership Flows.

How does the 3scale signup work?

The Basics

A developer can sign up to subscribe to an API service in various ways and how they do that is determined by the configuration of all the settings on the 3scale Admin Portal. Although this is flexible and varying in terms of the process and mechanics working in the background, the end user will not see a great deal of difference from their persepctive and this is key to delivering a good user experience.

Typically the developer would be able to discover any API service and the differing plans (contracts) as a public user on the homepage of any Developer Portal, when choosing the service and plan to subscribe to, the necessary parameters to create the contract between the Account and the API service will be passed to the signup form. There will be an Application created in the background as part of the signup process as this is the object that contains the API keys for consuming the contracted services.

Objects created during signup

  • Account
  • User
  • Application
  • Key(s)

The Account can have many Users but will only have 1 upon initial creation. If the Account has subscribed to multiple Services in the signup then it will be associated to those Services via what's known as a Service subscription and for each Service subscribed an Application will be created along with a set of Keys. Although it is important to understand the 3scale data object model and the relationship each object has with one another we won't go into too much detail on that here.

Subscriptions

There are 2 different subscriptions made when the signup is completed. The user commonly only really sees and cares about one of these though. The most important subscription is the Application Subscription and the less important of the two is the Service Subscription.

Application Subscription

The Application object has 1:1 relationship to a Service via an Application Plan in 3scale. This is the contract that determines the level of access, rate limits & pricing tier (if it's a monetized API) to consume a single Service.

Service Subscription

The Account object can be subscribed to many Services in 3scale and is able to access those Services via the Service Plan -- we use this only to manage the APIs the Account has access to -- as an admin user you will see these subscriptions in the Admin portal as a list of Service Subscriptions.

The Signup Flows

Single Application Flow

This is the simplest signup flow that only allows a signup to a single Service and Application Plan upon account creation. No features need to be enabled in the 3scale Developer portal to use this flow. Just include the single app partial from your homepage with: {% include '<PARTIAL_NAME>' %}.

Multiple Application Flow

This is a fairly simple signup flow that allows any public users to signup directly to multiple services and the associated Application Plans in just one sign up form. The user does not need an account prior to the signup as we want the Services to be publicly available. The Multiple Applications feature -- Developer portal > Feature visibility -- needs to be enabled on the Developer portal to use this flow. Use the multiple apps partial and call it from the homepage as so: {% include '<PARTIAL_NAME>' %}.

Custom Field Flow

This flow is useful when you want to control the Services a user can see or subscribe to. Imagine a simple scenario where there are 3 categories of APIs that are being managed through 3scale; Public, Private & Internal. The user could be limited to only have the ability to request to subscribe to a Service through some Admin portal settings. However, that then leaves some manual work on the API Provider side to be done every time the user wants to change to another Service or subscribe to a new Service. This isn't easy to maintain if many Services are being published on a frequent basis and with a growing Developer community the Devloper portal needs to remain scalable & manageable.

To use this flow a custom field on the Account object will need to be defined with some predefined options. For example; public, private, internal. These values will then be used to perform a substring match using some Liquid logic to render the allowed services and plans for the logged in user. As we want to control the access the user has, the signup form itself will only create an Account and User object. The custom field -- Settings > Field definitions -- should be assigned the appropriate value by an admin user once the Account has been created. This part can also be automated using a bit of fancy JavaScript which checks the email domain in the signup form and passes the appropriate custom field parameter value in the signup.

NOTE: If you do automate that part it's recommended to enable the "Account approval required" checkbox for added security measures. This can be done at Settings > General > Signup.

The final step is to include the custom fields partial in the 3scale CMS and call it with the Liquid tag: {% include '<PARTIAL_NAME>' %} from the homepage and also update the applications/form partial in the 3scale CMS with the form partial.

Group Membership Flow

This flow is most useful when like in the Custom Fields Flow you want to control the access to Services and in addition you want to create sections of content that can only be accessed by those users with the correct permissions. This flow also requires a user to have signed up to create an account previously. None of the Services or Plans are exposed publicly. The account will be approved by an admin of the API provider team and subsequently assigned the appropriate Group Membership. This will in turn expose the relevant APIs and their plans on the homepage for subscription. To enable this flow a series of steps must be followed.

  • The groups should be created under Developer portal > Groups the "Allowed sections" can be assigned to their appropriate groups later once they have been created.
  • Create a Feature in each of the Services' default Service Plan that denotes the "category" of the API, public, private, internal were the examples we used earlier.
  • Create a new Section for each Group under Developer portal > Content and select "New section" from the dropdown menu. Set the partial path to something that would also be contained as a substring of the Feature system_name created previously. It is the string values of these two attributes that will be compared to control the subscription forms the user is presented with.

Finally include the group membership partial in the 3scale CMS and as with the previous flows call it from the homepage.

Additional points

Demos & Workshops

These templates can be used in conjunction with one another if you're demoing the 3scale Developer Portal and the potential it has. Having the 4 different flows extracted into separate partials makes it very easy to manage and once the test Account & User is set up for demo purposes this should result in a very easy to execute demo.

JavaScript functions

There are some custom snippets of JavaScript in a few of the templates also. The following function subscribeApp passes the necessary parameters to the application new form so that the Application can be created directly after the Plan is selected. The default behaviour otherwise would be then to redirect the user to select the Service under which they want to create the Application.

function subscribeApp(serviceId, planId) {
      window.location.href = '/admin/applications/new?service_id=' + serviceId + '&plan_id=' + planId;
}

redirectUser function is required to redirect from the applications index page to the page defined in the function on the layout template. When a User first logs in the portal redirects them to the applications index view but if they haven't created any Application(s) yet this will feel a bit broken or strange and this custom redirect improves the UX a little.

function redirectUser() {
    // This redirect can be set to any page you wish. The default is the homepage.
    window.location.href="/"
};

I hope this gist is useful and please leave any comments or suggestions if you have any issues or improvements.

{% unless current_account.applications.size > 0 %}
<script type="text/javascript">
redirectUser();
</script>
{% else %}
<section class="about section">
<div class="container">
<div class="row">
<div class="col-md-9">
<table class="table panel panel-default" id="applications">
<thead class="panel-heading">
<tr>
<th>Name</th>
{% if provider.multiple_services_allowed? %}
<th>Service</th>
{% endif %}
<th>Credentials</th>
<th>State</th>
<th>
</th>
</tr>
</thead>
<tbody class="panel-body">
{% for service in current_account.subscribed_services %}
{% for application in service.applications %}
<tr class="{% cycle 'applications': 'odd', 'even' %}" id="application_{{ application.id }}">
<td>
{{ application.name | link_to: application.url }}
</td>
{% if provider.multiple_services_allowed? %}
<td>{{ service.name }}</td>
{% endif %}
<td>{{ application.key }}</td>
<td>{{ application.state }}</td>
<td>
{% if application.can.be_updated? %}
<a href="{{ application.edit_url }}">
<i class="icon-pencil"></i>
</a>
{% endif %}
</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
<tfoot class="panel-footer">
<tr>
<td colspan="3">
</td>
<td>
{% if current_user.can.create_application? %}
<a href="{{ urls.new_application }}" title="Create new application" class="btn btn-default btn-xs">Create new application</a>
{% endif %}
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</section>
{% endunless %}
<section class="plans" id="plans">
<div class="container">
<h1>Pick your plan</h1>
<br/>
{% for field in current_account.extra_fields %}
{% if field.label contains 'access' %}
{% assign access = field.value %}
{% endif %}
{% endfor %}
<h1>{{ access }}</h1>
<div class="row">
{% for service in provider.services %}
{% if service.system_name contains access %}
{% for plan in service.application_plans %}
<div class="col-md-6">
<article class="panel panel-default">
<div class="panel-heading">
<strong>{{ plan.name }}</strong>
</div>
<div class="panel-body">
<div class="row">
{% if plan.features == present %}
<div class="col-md-6">
<h5>Features</h5>
<ul class="features list-unstyled">
{% for feature in plan.features %}
<li>
<i class="fa fa-check"></i> {{ feature.name }}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="col-md-6">
<h5>Limits{{ plan.trial_period_days.size }}</h5>
<ul class="limits list-unstyled">
{% if plan.usage_limits == present %} {% for limit in plan.usage_limits %}
<li>
<i class="fa fa-signal"></i> {{ limit.metric.name }} &ndash; {{ limit.value }} {{ limit.metric.unit }}s per {{ limit.period }}
</li>
{% endfor %} {% else %}
<li>
<i class="fa fa-signal"></i> No limits
</li>
{% endif %}
</ul>
</div>
</div>
</div>
<div class="panel-footer">
<div class="row">
<div class="col-md-12">
{% if service.subscription %}
<button type="button" class="btn btn-success pull-right" onclick="subscribeApp('{{plan.service.system_name}}', '{{plan.id}}')">Signup to plan {{ plan.name }}</button>
{% else %}
<a href="/admin/service_contracts/new?service_id={{service.id}}" class="btn btn-success pull-right">Subscribe to the {{ plan.name }} service</a>
{% endif %}
</div>
</div>
</div>
</article>
</div>
{% endfor %}
{% else %}
{% comment %}
request to be assigned some membership
{% endcomment %}
{% endif %}
{% endfor %}
</div>
</div>
</section>
<script>
function subscribeApp(serviceId, planId) {
window.location.href = '/admin/applications/new?service_id=' + serviceId + '&plan_id=' + planId;
}
</script>
<section class="plans" id="plans">
<div class="container">
<h1>Pick your group plan</h1>
<br/>
<div class="row">
{% assign sections = current_user.sections %}
{% for service in provider.services %}
{% for section in sections %}
{% assign cleanedsection = section | remove_first: "/" %}
{% for feature in service.features %}
{% if feature.system_name contains cleanedsection %}
{% for plan in service.application_plans %}
<div class="col-md-6">
<article class="panel panel-default">
<div class="panel-heading">
<strong>{{ plan.name }}</strong>
</div>
<div class="panel-body">
<div class="row">
{% if plan.features == present %}
<div class="col-md-6">
<h5>Features</h5>
<ul class="features list-unstyled">
{% for feature in plan.features %}
<li>
<i class="fa fa-check"></i> {{ feature.name }}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="col-md-6">
<h5>Limits{{ plan.trial_period_days.size }}</h5>
<ul class="limits list-unstyled">
{% if plan.usage_limits == present %} {% for limit in plan.usage_limits %}
<li>
<i class="fa fa-signal"></i> {{ limit.metric.name }} &ndash; {{ limit.value }} {{ limit.metric.unit }}s per {{ limit.period }}
</li>
{% endfor %} {% else %}
<li>
<i class="fa fa-signal"></i> No limits
</li>
{% endif %}
</ul>
</div>
</div>
</div>
<div class="panel-footer">
<div class="row">
<div class="col-md-12">
{% if service.subscription %}
<button type="button" class="btn btn-success pull-right" onclick="subscribeApp('{{plan.service.system_name}}', '{{plan.id}}')">Signup to plan {{ plan.name }}</button>
{% else %}
<a href="/admin/service_contracts/new?service_id={{service.id}}" class="btn btn-success pull-right">Subscribe to the {{ service.name }} service</a>
{% endif %}
</div>
</div>
</div>
</article>
</div>
{% endfor %}
{% else %}
{% comment %}
Maybe render a signup or login button
{% endcomment %}
{% endif %}
{% endfor %}
{% endfor %}
{% endfor %}
</div>
</div>
</section>
<script>
function subscribeApp(serviceId, planId) {
window.location.href = '/admin/applications/new?service_id=' + serviceId + '&plan_id=' + planId;
}
</script>
<form action="{{ urls.signup }}" method="get">
<div class="container">
<h1>Pick your plan</h1>
<br/>
{% for service in provider.services %}
<h2> {{ service.name }} </h2>
<div class="row">
{% for plan in service.application_plans%}
<div class="col-md-4">
<article class="panel panel-default">
<div class="panel-heading">
<strong>{{ plan.name }}</strong>
</div>
<div class="panel-body">
<div class="row">
{% if plan.features == present %}
<div class="col-md-6">
<h5>Features</h5>
<ul class="features list-unstyled">
{% for feature in plan.features %}
<li>
<i class="fa fa-check"></i> {{ feature.name }}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="col-md-6">
<h5>Limits</h5>
<ul class="limits list-unstyled">
{% if plan.usage_limits == present %} {% for limit in plan.usage_limits %}
<li>
<i class="fa fa-signal"></i> {{ limit.metric.name }} &ndash; {{ limit.value }} {{ limit.metric.unit }}s per {{ limit.period }}
</li>
{% endfor %} {% else %}
<li>
<i class="fa fa-signal"></i> No limits
</li>
{% endif %}
</ul>
</div>
</div>
</div>
<div class="panel-footer">
<div class="row">
<div class="col-md-12">
<div class="row">
<div class="col-md-12">
<input type="checkbox" name="plan_ids[]" value="{{ plan.id }}">Signup to {{ plan.name }}</input>
<input type="hidden" name="plan_ids[]" value="{{ service.service_plans.first.id }}"></input>
</div>
</div>
</div>
</div>
</div>
</article>
</div>
{% endfor %}
</div>
{% endfor %}
<div class="container text-center">
<button type="submit" class="btn btn-cta-primary">Signup</a>
</div>
</div>
</form>
<div class="container">
<h1>Pick your plan</h1>
<br/>
{% for service in provider.services %}
<h2> {{ service.name }} </h2>
<div class="row">
{% for plan in service.application_plans%}
<div class="col-md-4">
<article class="panel panel-default">
<div class="panel-heading">
<strong>{{ plan.name }}</strong>
</div>
<div class="panel-body">
<div class="row">
{% if plan.features == present %}
<div class="col-md-6">
<h5>Features</h5>
<ul class="features list-unstyled">
{% for feature in plan.features %}
<li>
<i class="fa fa-check"></i> {{ feature.name }}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="col-md-6">
<h5>Limits</h5>
<ul class="limits list-unstyled">
{% if plan.usage_limits == present %} {% for limit in plan.usage_limits %}
<li>
<i class="fa fa-signal"></i> {{ limit.metric.name }} &ndash; {{ limit.value }} {{ limit.metric.unit }}s per {{ limit.period }}
</li>
{% endfor %} {% else %}
<li>
<i class="fa fa-signal"></i> No limits
</li>
{% endif %}
</ul>
</div>
</div>
</div>
<div class="panel-footer">
<div class="row">
<div class="col-md-12">
<a class="btn btn-cta-secondary pull-right" href="{{ urls.signup }}?{{ plan | to_param }}&{{ service.service_plans.first | to_param }}">Signup to plan {{ plan.name }}</a>
</div>
</div>
</div>
</article>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% assign params = request.request_uri | split: 'plan_id=' %}
{% assign planId = params[1] %}
<input type="hidden" id="cinstance_plan_id" name="application[plan_id]" value="{{ planId }}" />
<fieldset>
<div class="form-group" id="selected-plan" data-plan-id="{{ planId }}">
<label class="control-label col-md-4">Plan</label>
<div class="col-md-6">
<p class="form-control-static">
<strong>{{application.plan.name}}</strong>
{% if application.can_change_plan? %}
<span>› </span>
<a href="#choose-plan-{{ application.id }}" id="choose-plan-{{application.id}}">
Review/Change
</a>
{% plan_widget application, wizard: true %}
{{ '/css/plans_widget_overrides.css' | stylesheet_link_tag }}
{% endif %}
</p>
</div>
</div>
</fieldset>
<fieldset>
{% for field in application.fields %}
{% include 'field' with field %}
{% endfor %}
</fieldset>
{% include 'new_application_licence' %}
<header class="jumbotron page-header">
<div class="container">
<div class="row">
<div class="col-md-12">
<h1> &nbsp; </h1>
</div>
</div>
</div>
</header>
{% if current_user and current_account.applications.size == 0 %}
{% if current_user.sections.size > 0 %}
{% include 'group_membership_plans' %}
{% else %}
{% include 'custom_field_plans' %}
{% endif %}
{% elsif current_user %}
<section class="start">
<div class="container">
<div class="row">
<div class="col-md-6">
<h1>Lets get started</h1>
<p class="lead">
I will obey your orders. I will serve this ship as First Officer. And in an attack against the Enterprise, I will die with this crew. But I will not break my oath of loyalty to Starfleet. and attack the Romulans. But the probability of making a six is no greater than that of rolling a seven. I'll alert the crew.
</p>
</div>
<div class="col-md-6" style="padding-top:2em;">
<div class="panel panel-default" id="access-details">
<div class="panel-heading">
<i class="fa fa-key"></i>
Credentials
{% if current_account.applications.size > 0 %}
<a class="pull-right" href="{{ urls.applications }}" title="Applications">
<i class="fa fa-chevron-right"></i>
</a>
{% endif %}
</div>
<div class="panel-body panel-footer">
{% if current_account.applications.size == 1 %}
{% assign app = current_account.applications.first %}
{% if app.user_key_mode? %}
<dl class="dl-horizontal">
<dt>App name</dt>
<dd><a href="{{ app.url }}">{{ app.name }}</a></dd>
<dt>Key</dt>
<dd>
<code>{{ app.user_key }}</code>
<p>Add this as a <code>user_key</code> parameter to your API calls to authenticate.</p>
</dd>
</dl>
{% elsif app.app_id_mode? %}
<dl class="dl-horizontal">
<dt>App name</dt>
<dd><a href="{{ app.url }}">{{ app.name }}</a></dd>
<dt>App ID</dt>
<dd><code>{{ app.application_id }}</code></dd>
<dt>Key</dt>
<dd><code>{{ app.keys.first }}</code></dd>
</dl>
{% elsif app.oauth_mode? %}
<dl class="dl-horizontal">
<dt>App name</dt>
<dd><a href="{{ app.url }}">{{ app.name }}</a></dd>
<dt>Client ID</dt>
<dd><code>{{ app.application_id }}</code></dd>
<dt>Client Secret</dt>
<dd><code>{{ app.keys.first }}</code></dd>
</dl>
{% endif %}
{% if app.trial? %}
<div class="alert alert-warning">
<h3>Trial period</h3>
<p>{{ app.remaining_trial_period_days }} days remaining.</p>
</div>
{% endif %}
{% elsif current_account.applications.size > 1 %}
<a href="{{ urls.applications }}" class="btn btn-primary">See your Applications & their credentials</a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
{% else %}
<section class="sell">
<div class="container">
<div class="row">
<div class="col-md-4">
<h3>Register</h3>
<p>
<i class="fa fa-sign-in fa-3x pull-left"></i> Register to the developer portal to use the Echo API
</p>
</div>
<div class="col-md-4">
<h3>Get your API key</h3>
<p>
<i class="fa fa-key fa-3x pull-left"></i> Use your API key to authenticate and report the calls you make
</p>
</div>
<div class="col-md-4">
<h3>Create your app</h3>
<p>
<i class="fa fa-code fa-3x pull-left"></i> Start coding and create awesome applications with the Echo API
</p>
</div>
</div>
</div>
</section>
{% assign multiple_apps = provider.multiple_applications_allowed? %}
<section class="plans" id="plans">
{% if multiple_apps %}
{% include 'multiple_app_signup_form' %}
{% else %}
{% include 'single_app_signup_form' %}
{% endif %}
</section>
<!-- lets remove this from here... -->
{% endif %}
<section class="invert">
<div class="container">
<h1>Run your requests</h1>
<div class="row">
<div class="col-md-12">
<h3><code style="display:block">$ curl -v https://echo-api.3scale.net</code></h3>
<br/>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
Request
</div>
<div class="panel-body panel-footer">
<pre>
&gt; GET / HTTP/1.1
&gt; User-Agent: curl/7.27.0
&gt; Host: https://echo-api.3scale.net/echo
&gt; Accept: */*
&gt;
</pre>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
Response
</div>
<div class="panel-body panel-footer">
<pre>
&lt; HTTP/1.1 200 OK
&lt; Content-Type: text/plain; charset=utf-8
&lt; Connection: close
echo
</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{provider.name}} API</title>
<link rel="stylesheet" href="/css/custom.css">
<!-- STYLESHEETS -->
{% csrf %}
{{ content_of.stylesheets | html_safe }}
<!--<link rel="stylesheet" type="text/css" href="/css/custom.css">-->
<link rel="stylesheet" type="text/css" href="/css/bootstrap.css">
<!-- fonts -->
{{ 'https://fonts.googleapis.com/css?family=Lato:300,400,300italic,400italic' | stylesheet_link_tag }}
{{ 'https://fonts.googleapis.com/css?family=Montserrat:400,700' | stylesheet_link_tag }}
{{ 'https://fonts.googleapis.com/css?family=Comfortaa:400,700' | stylesheet_link_tag }}
<!-- font-awesome css -->
{{ 'https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css' | stylesheet_link_tag }}
<!-- JS (PRELOADED) -->
<!-- jQuery -->
{{ 'https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js' | javascript_include_tag }}
<!-- 3scale application file -->
{{ '/javascripts/3scale.js' | javascript_include_tag }}
{{ '/javascripts/excanvas.compiled.js' | javascript_include_tag }}
<!-- devaid -->
{{ '/javascripts/devaid.js' | javascript_include_tag }}
{{ content_of.javascripts | html_safe }}
</head>
<body>
<!-- NAVBAR AND HEADER SECTION -->
<header>
{% logo %}
</header>
<script type="text/javascript">
function redirectUser() {
// This redirect can be set to any page you wish. The default is the homepage.
window.location.href="/"
};
</script>
<header id="header" class="header">
<div class="container">
<h1 class="logo pull-left">
<a href="/">
<span class="logo-title">{{ provider.name }}</span>
</a>
</h1>
<nav class="main-nav navbar-right" role="navigation">
<div class="navbar-header">
<button class="navbar-toggle" type="button" data-toggle="collapse" data-target="#navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<!-- DYNAMIC NAVBAR ELEMENTS -->
{% include 'submenu1' %}
</nav>
</div>
</header>
<main role="main">
{% content %}
</main>
<!-- STATIC FOOTER -->
<footer class="footer">
<div class="container">
<div class="row">
<p class="text-center">Copyright &copy; {{ today.year }} {{ provider.name }}<a href="http://3scale.net/" class="powered-by" target="_blank">Powered by 3scale</a></p>
</div>
</div>
</footer>
</body>
</html>
@wrenkredhat
Copy link

Thank you for sharing !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment