1.0.11 • Published 1 year ago

@sproutedigital/advanced-ussd-builder v1.0.11

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

NPM npm

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 user
  • authenticated: true | false, whether authentication is successful or not
  • require_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.

OptionDefaultData TypeRequiredDescription
state{}objectfalseLocal state for menu handlers
paginatedtruebooleanfalsePaginate main menu
require_pinfalsebooleanfalsePrompt for pin on every new session
provider-'nalo', 'arkesel', 'mtn',trueSupported USSD providers for AUMB
next_page_label'#'stringfalseLabel for next page indicator
prev_page_label'0'stringfalseLabel for next page indicator
service_code-stringtrueService code. Do not include ending '#': example *625*1
redis_connection_url-stringtrueRedis connection url
welcome_message-stringtrueDefault welcome message for main menu
menu[]arrayfalseUSSD 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

OptionsData TypeRequiredDescription
titlestringtrueTitle for the menu options
paginatedbooleanfalsePaginate options for rendered menu. Defaults to true
renderobjectfalseDefine action for the selected menu item. Render is required if no handler is provided
'render.success_message'stringtrueTitle for the menu when selected
'render.error_message'stringfalseIf you need to override handler error message
'render.hide_options'booleanfalseHide menu options when selected and prompted for user feedback
'render.menu'arrayfalseNested 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 functionfalseCalled 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
1.0.11

1 year ago

1.0.10

1 year ago

1.0.9

1 year ago

1.0.8

1 year ago

1.0.7

1 year ago

1.0.5

1 year ago

1.0.4

1 year ago

1.0.3

1 year ago

1.0.2

1 year ago

1.0.1

1 year ago

1.0.0

1 year ago