@sproutedigital/advanced-ussd-builder v1.0.11
Advanced USSD Menu Builder for Node.js makes it easy to build USSD Menu in your Node.js applications.
Advanced USSD Menu Builder for Node.js
Advanced USSD Menu Builder makes it easy to build ussd menu in your Node.js API/Backend systems. This package supports the following out of the box
- Welcome Message
- PIN Authentication
- Middleware Support
- Menu Customizations
- Automated Pagination
- Automated Navigation
- Menu Option Customizations
- State management with Redis which makes this powerful for having multiple instances of your backend server share state.
The following USSD Service providers are supported
Getting Started
First things first, get yourself a Node.js project. There are lots of ways to do this, but I'm gonna go with a classic:
npm init
Once you have that sweet, sweet package.json, let's add our newest favorite package to it:
npm install @sproute/advanced-ussd-builder
At this stage we need to get Redis installed. Click here and follow the instructions to install Redis.
After installation test your redis installation
redis-cli ping
PONG
⚠️ Notice
This assumes you have installed Redis on a local instance.
In a production environment you are likely to have redis installed on a remote instance therefore make sure the conection url is supplied to the redis-cli to test the connection.
Once you see PONG
as the response, it means your redis connection is successfully established.
Excellent. Setup done. Let's write some code!
Initializing AUMB
The AUMB uses redis state to create a USSD menu. A state is created for each session. A state is terminated automatically once user exists or session gets terminated. To initialize a menu reference the code below
const ussd_menu = new UssdMenu(opts);
and to return API response, depending on your Node.js framework and the provider, you have to build a response wrapper example
const cb = (response: iNaloUssdCallback) => {
return res.status(200).json(response);
}
const ussd_args = req.body
return await ussd_menu.pipe(ussd_args, cb)
The args
which is the request body should match the provider defined in the options. We'll look at the options for the AUMB below.
Sample Options For AUMB
const opts = {
state: {},
paginated: true,
require_pin: true,
provider: 'nalo',
next_page_label: '#',
prev_page_label: '0',
service_code: '*920*41',
redis_connection_url: '<REDIS_CONNECTION_URL>',
welcome_message: 'Welcome To Sproute Digital',
authentication_handler: async(args) => {
// authentication handler
},
middleware: async (args) => {
// middleware runs before all methods.
},
menu: [
// USSD menu tree defined here
{
title: 'Register',
handler: async (builder, ctx, user_input) => {}
},
{
title: 'Contact Us',
render: {
success_message: 'Contact us on 0546891427',
},
},
...etc
]
}
const ussd_args = req.body
const ussd_menu = new UssdMenu(opts);
return await ussd_menu.pipe(ussd_args, cb)
When the above code is executed the following USSD Message will be displayed on the receiving end.
Welcome To Sproute Digital
1). Register
2). Contact Us
Lets read more into details about the options.
Local State
Local state allows you to pass additional information to the menu handlers
. The menu structure supports automated and custom menu handlers. Thus depending on your need you can control the menu's logic or you can leave it to AUMB to handle it. This will make more sense once we look at the Menu Definition.
An example use case would be to pass a user_id so that you can access the user id in the custom menu handlers.
Example
const opts = {
state: {
user_id: '643028bd93798b23c79e7459'
},
menu: [
{
title: 'Show Balance',
handler: async (builder, ctx, user_input) => {
// get local state data
const user_id = builder.opts.state.user_id
// do some db operations
const user = db.users.findOne({ _id: user_id })
// rest is history
}
}
]
...etc
}
Main Menu Pagination
The paginated
option attribute is used to determine if the main menu should be paginated or not. If paginated: true
and menu items exceeds 5
, the menu will be paginated showing #) Next
and 0) Back
automatically.
Example output
Welcome To Sproute Digital
1). Register
2). Contact Us
... other menu items
#) Next
0) Back
Require PIN
The require_pin
option attribute is used to require login or authentication before rending menu. If you set require_pin:true
you need to provide authentication_handler
Details about authentication handler is provided below.
Example Pin output
Welcome To Sproute Digital
Please enter PIN
Authentication Handler
If you indicated require_pin
you need to provide your own logic for authentication. The authentication handler needs to return an object with the following attributes
return {
message: '<Message here if any>',
authenticated: boolean,
require_feedback: boolean
}
Example Authentication Handler
const opts = {
authentication_handler: async(args) => {
const correct_pin = "4321";
const input = args.user_input;
if (input === correct_pin){
return {
message: '',
authenticated: true,
require_feedback: false,
}
}
if (input !== correct_pin && args.is_new_request){
return {
message: 'Welcome to Sproute Digital\n Please enter your PIN',
authenticated: false,
require_feedback: true
}
} else {
return {
message: 'Invalid PIN entered. Try again. Attempts left: 3 attempts remaining',
authenticated: false,
require_feedback: true
}
}
},
...etc
}
Authentication Handler Response Options
message
: string, message to display as feedback to userauthenticated
: true | false, whether authentication is successful or notrequire_feedback
: true | false, whether to require user feedback. Usually when pin is incorrect, you'd want user to attempt again.
Middleware
Middleware runs before all other logics. You can middleware handle receives the request payload thus the payload from the indicated provider.
Middleware can be used to run extra validations before invoking the menu handlers. You can use middleware to determine if payload is valid
Example middleware
const opts = {
middleware: async (args) => {
if (args.user_id !== NALO_USSD_USERID){
throw new Error('Identity verification failed.')
}
},
...etc
}
Menu Options
Find comprehensive reference to the menu options.
Option | Default | Data Type | Required | Description |
---|---|---|---|---|
state | {} | object | false | Local state for menu handlers |
paginated | true | boolean | false | Paginate main menu |
require_pin | false | boolean | false | Prompt for pin on every new session |
provider | - | 'nalo', 'arkesel', 'mtn', | true | Supported USSD providers for AUMB |
next_page_label | '#' | string | false | Label for next page indicator |
prev_page_label | '0' | string | false | Label for next page indicator |
service_code | - | string | true | Service code. Do not include ending '#': example *625*1 |
redis_connection_url | - | string | true | Redis connection url |
welcome_message | - | string | true | Default welcome message for main menu |
menu | [] | array | false | USSD Menu config |
Menu Definition
The menu can be configured in a variety of ways depending on needs. You can configure menu with placeholders in message to be populated by the handler. Detailed examples have been defined below.
A typical menu will have the following structure
{
title: 'My Wallet',
paginated: true,
render: {
success_message: 'My Wallet',
error_message: 'Error showing wallet',
hide_options: false,
menu: [
// nested menu
// menu options here
// has same interface as parent.
// can be nested as much as you want.
{
title: 'Check Balance',
render: {
terminate_if_error: true,
success_message: 'Your balance is GHS %1',
error_message: 'You don\'t have any balance ATM'
},
handler: async (builder, ctx, user_input) => {
return ['10,000']
}
},
{
title: 'Allow Cash Out',
handler: async (builder, ctx, user_input) => {
return 'Cash out allowed'
}
},
...etc
]
}
handler: async (builder, ctx, user_input) => {
}
},
Menu Structure Options
Options | Data Type | Required | Description |
---|---|---|---|
title | string | true | Title for the menu options |
paginated | boolean | false | Paginate options for rendered menu. Defaults to true |
render | object | false | Define action for the selected menu item. Render is required if no handler is provided |
'render.success_message' | string | true | Title for the menu when selected |
'render.error_message' | string | false | If you need to override handler error message |
'render.hide_options' | boolean | false | Hide menu options when selected and prompted for user feedback |
'render.menu' | array | false | Nested menu options for when menu is selected. This inherits all attributes of the parent menu and can have unlimited nested menu items |
handler / 'render.handler' | async function | false | Called when menu item is selected. A handler can return string, array of strings and a menu item. More details below |
Handler return types
A menu handler can return string
, string[]
, { message: string; require_feedback: boolean }
, provider specific response
and void
The type of return option depends on how the menu is defined or your specific use cases.
Return type string
If the handler returns a string, that overrides render.success_message
if defined. So you can omit render.success_message
if your handler returns a string.
Return type string[]
If the handler returns an array of string, then the handler will update all the placeholders in render.success_message
with values in the array. Remember the options will update the render_message based on the index of the array. Unlike string
only return type, string[]
does not override render_message but only updates the placeholders. Find an example placeholder below.
Return type { message: string; require_feedback: boolean }
This is used to return self menu and it the situations where user wants control over the rendering and processing of the menu.
If the handler returns an object with message and require_feedback, it returns this message to the user. require_feedback
determines if user's feedback is required.
⚠️ Note
If a menu is defined with
hide_options:true
or no render, then the handler expected to return a self menu.
Menu with placeholders
const opts = {
...,
menu: [
// USSD menu tree defined here
{
title: 'Check Balance',
render: {
success_message: 'Your current balance is GHS % and available balance is GHS %'
}
handler: async (builder, ctx, user_input) => {
return [
'1,500.00',
'28,500.74'
]
}
},
...etc
]
}
When above menu item is selected the display message will be
Your current balance is GHS 1,500.00 and available balance is GHS 28,500.74
Overriding handler error messages
You may sometimes want to override error messages defined in handlers as thats displayed to the user or could be unhandled exceptions etc.
const opts = {
...,
menu: [
// USSD menu tree defined here
{
title: 'Check Balance',
render: {
success_message: 'Your current balance is GHS % and available balance is GHS %',
error_message: 'Sorry, system down check back later.'
}
handler: async (builder, ctx, user_input) => {
return [
'1,500.00',
'28,500.74'
]
}
},
...etc
]
}
Menu with self handlers
There are situations when you'd want to write your own menu / session management. It this case its referred to as self handlers. Checkout the example below.
In such cases, you have to omit the render attribute.
const opts = {
...,
menu: [
// USSD menu tree defined here
{
title: 'My Approvals',
handler: async (builder, ctx, user_input) => {
// perform some db operations and return menu.
// the builder expose set_session endpoint which can be used to store custom session metadata
const last_menu = builder.session.custom_menu['my_approvals'];
if (!last_menu){
// first time selected
// update session
await builder.update_custom_session({
$set: { 'custom_menu.my_approvals': 'step_1' }
});
// display menu
let response_message = 'Select pending transaction to approve'
response_message += "1) GHS 1,000 From CrownPay"
response_message += "2) GHS 500 From Exxtra Cash"
response_message += "3) GHS 20 From Coocoobay"
return {
message: response_message,
require_feedback: true
}
} else {
// continuous message
switch (user_input){
case: '1':
// here approve transaction 1
break;
case: '2':
// here approve transaction 1
break;
...etc
default:
return {
message: 'Invalid input, kindly try again',
require_feedback: false // you have a choice here to continue session or not!
}
}
}
}
},
...etc
]
}
In the above code, if its the first time we display menu options and require user to input feedback. When user responds, we check which option they selected and act accordingly or display invalid input message.
With self menu, the developer is responsible for managing their own state so you can tell what the last activity of the user was and therefore to know what to do with their current input or feedback like demonstrated above.
Full example
The following is a minimal example of a logic in node express route/controller;
const cb = (response: iNaloUssdCallback) => {
return res.status(200).json(response);
}
const opts = {
state: {
user_id: '643028bd93798b23c79e7459',
user_email: 'johndoe@gmail.com',
user_name: 'john_doe'
},
paginated: true,
require_pin: true,
provider: 'nalo',
next_page_label: '#',
prev_page_label: '0',
service_code: '*622*55',
redis_connection_url: REDIS_CONNECTION_URL,
welcome_message: 'Welcome To Sproute Digital Advance',
authentication_handler: async(args) => {
const correct_input = "4321";
const input = args.user_input;
if (input === correct_input){
return {
message: '',
authenticated: true,
require_feedback: false,
}
}
if (input !== correct_input && args.is_new_request){
return {
message: 'Welcome to Sproute Digital Advance\n Please enter your PIN',
authenticated: false,
require_feedback: true
}
} else {
return {
message: 'Invalid PIN entered. Try again. Attempts left: 3 attempts remaining',
authenticated: false,
require_feedback: true
}
}
},
menu: [
{
title: 'Contact Us',
render: {
success_message: 'Contact us on 0208150416',
},
},
{
title: 'Terms and Conditions',
render: {
success_message: 'When you accept the TandCs, you consent to receiving SMSs and sharing your personal information with external providers.',
menu: [
{
title: 'Next',
render: {
success_message: 'We will share your personal information and usage with authorized parties for regulatory and commercial purposes.',
menu: [
{
title: 'Next',
render: {
success_message: 'If you apply for our products, you will have to accept the TandCs.'
}
},
]
}
}
]
},
}
],
}
const ussd_args = req.body
const ussd_menu = new UssdMenu(opts);
return await ussd_menu.pipe(ussd_args, cb)
What Next ?
Support for the following providers in development. d
Future milestones
- More features
- Different state solutions
- Support individual menu authentication
- More demos