ExUssd

Actions Status Hex.pm Hex.pm Coverage Status

Introduction

ExUssd lets you create simple, flexible, and customizable USSD interface. Under the hood ExUssd uses Elixir Registry to create and route individual USSD session.

Sections

Installation

If available in Hex, the package can be installed by adding ex_ussd to your list of dependencies in mix.exs:

def deps do
  [
    {:ex_ussd, "~> 0.1.2"}
  ]
end

Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/ex_ussd.

Providers

ExUssd currently supports

Africastalking API

Infobip API

Configuration

To Use One of the above gateway providers for your project Create a copy of config/dev.exs or config/prod.exs from config/dev.sample.exs Use the gateway key to set the ussd vendor.

AfricasTalking

Add below config to dev.exs / prod.exs files

config :ex_ussd, :gateway, AfricasTalking

Infobip

Add below config to dev.exs / prod.exs files

config :ex_ussd, :gateway, Infobip

Documentation

ExUssd supports Ussd customizations through Menu struct via the render function

  • name: (Public) This is the value display when Menu is rendered as menu_list. check more on menu_list,
  • handler: (Public) A callback that modifies the current menu struct. Implemented via ExUssd.Handler
  • callback: (Internal) A callback function that takes the handler callback. This function is triggered when the client is at that menu position.
  • title: (Public) Outputs the ussd's title,
  • menu_list: (Public) Takes a list of Ussd Menu struct,
  • error: (Public) A custom validation error message for validation_menu,
  • show_navigation: (Public) set to false to hide navigation menu,
  • next: (Public) navigate's the next menu chunk, default %{name: "MORE", input_match: "98", display_style: ":"},
  • previous: (Public) navigate's the previous menu chunk or the previous menu struct default %{name: "BACK", input_match: "0", display_style: ":"},
  • split: (Public) This is used to set the chunk size value when rendering menu_list. default value size 7,
  • should_close: (Public) This triggers ExUssd to end the current registry session,
  • display_style: (Public) This is used to change default's display style, default ":"
  • parent: (Internal) saves the previous menu struct to the current menu in order to facilitate navigation,
  • validation_menu: (Public) Its a special Menu struct that enables the developer to validate the client input,
  • data: (Public) takes data as Props that will be attached to the children menu struct,
  • default_error_message:(Public) This the default error message shown on invalid input. default "Invalid Choice\n"

ussd title only

defmodule MyHomeHandler do
   @behaviour ExUssd.Handler
  def handle_menu(menu, _api_parameters) do
     menu |> Map.put(:title, "Welcome")
   end
end
menu = ExUssd.Menu.render(name: "Home", handler: MyHomeHandler)
ExUssd.goto(
  internal_routing: %{text: "", session_id: "session_01", service_code: "*544#"},
  menu: menu,
  api_parameters: %{"sessionId" => "session_01", "phoneNumber" => "254722000000", "networkCode" => "Safaricom", "serviceCode" => "*544#", "text" => "" }
  )
{:ok, "CON Welcome"}

ussd menu_list

defmodule ProductAHandler do
  @behaviour ExUssd.Handler
  def handle_menu(menu, _api_parameters) do
    menu |> Map.put(:title, "selected product a")
  end
end

defmodule ProductBHandler do
  @behaviour ExUssd.Handler
  def handle_menu(menu, _api_parameters) do
    menu |> Map.put(:title, "selected product b")
  end
end

defmodule ProductCHandler do
  @behaviour ExUssd.Handler
  def handle_menu(menu, _api_parameters) do
    menu |> Map.put(:title, "selected product c")
  end
end

defmodule MyHomeHandler do
  @behaviour ExUssd.Handler
  def handle_menu(menu, _api_parameters) do
    menu
    |> Map.put(:title, "Welcome")
    |> Map.put(
      :menu_list,
      [
        ExUssd.Menu.render(name: "Product A", handler: ProductAHandler),
        ExUssd.Menu.render(name: "Product B", handler: ProductBHandler),
        ExUssd.Menu.render(name: "Product C", handler: ProductCHandler)
      ]
    )
  end
end
menu = ExUssd.Menu.render(name: "Home", handler: MyHomeHandler)
ExUssd.goto(
  internal_routing: %{text: "", session_id: "session_01", service_code: "*544#"},
  menu: menu,
  api_parameters: %{"sessionId" => "session_01", "phoneNumber" => "254722000000", "networkCode" => "Safaricom", "serviceCode" => "*544#", "text" => "" }
)

{:ok, "CON Welcome\n1:Product A\n2:Product B"}
# simulate 1
menu = ExUssd.Menu.render(name: "Home", handler: MyHomeHandler)
ExUssd.goto(
  internal_routing: %{text: "1", session_id: "session_01", service_code: "*544#"},
  menu: menu,
  api_parameters: %{"sessionId" => "session_01", "phoneNumber" => "254722000000", "networkCode" => "Safaricom", "serviceCode" => "*544#", "text" => "1" }
  )
{:ok, "CON selected product a\n0:BACK"}

ussd validation menu

defmodule PinValidateHandler do
  @behaviour ExUssd.Handler
  def handle_menu(menu, api_parameters) do
    case api_parameters.text == "5555" do
      true ->
        menu
        |> Map.put(:title, "success, thank you.")
        |> Map.put(:should_close, true)

      _ ->
        menu |> Map.put(:error, "Wrong pin number\n")
    end
  end
end

defmodule MyHomeHandler do
  @behaviour ExUssd.Handler
  def handle_menu(menu, _api_parameters) do
    menu
    |> Map.put(:title, "Enter your pin number")
    |> Map.put(:validation_menu, ExUssd.Menu.render(name: "", handler: PinValidateHandler))
  end
end

menu = ExUssd.Menu.render(name: "Home", handler: MyHomeHandler)

ExUssd.goto(
  internal_routing: %{text: "", session_id: "session_01", service_code: "*544#"},
  menu: menu,
  api_parameters: %{"sessionId" => "session_01", "phoneNumber" => "254722000000", "networkCode" => "Safaricom", "serviceCode" => "*544#", "text" => "" }
)
{:ok, "CON Enter your pin number"}

# simulate wrong pin
menu = ExUssd.Menu.render(name: "Home", handler: MyHomeHandler)
ExUssd.goto(
  internal_routing: %{text: "3339", session_id: "session_01", service_code: "*544#"},
  menu: menu,
  api_parameters: %{"sessionId" => "session_01", "phoneNumber" => "254722000000", "networkCode" => "Safaricom", "serviceCode" => "*544#", "text" => "3339" }
)
{:ok, "CON Wrong pin number\nEnter your pin number"}

# simulate correct pin
menu = ExUssd.Menu.render(name: "Home", handler: MyHomeHandler)
ExUssd.goto(
  internal_routing: %{text: "5555", session_id: "session_01", service_code: "*544#"},
  menu: menu,
  api_parameters: %{"sessionId" => "session_01", "phoneNumber" => "254722000000", "networkCode" => "Safaricom", "serviceCode" => "*544#", "text" => "5555" }
)
{:ok, "END success, thank you."}

receive data as props

defmodule MyHomeHandler do
  @behaviour ExUssd.Handler
 def handle_menu(menu, api_parameters) do
  %{language: language} = menu.data
  case language do
     "Swahili" -> menu |> Map.put(:title, "Karibu")
     _-> menu |> Map.put(:title, "Welcome")
    end
   end
end

data = %{language: "Swahili"}
ExUssd.Menu.render(name: "Home", data: data, handler: MyHomeHandler)

Testing

To test your USSD menu, ExUssd provides a simulate function that helps you test menu rendering and logic implemented by mimicking USSD gateway callback.

  iex> defmodule MyHomeHandler do
        @behaviour ExUssd.Handler
        def handle_menu(menu, _api_parameters) do
          menu |> Map.put(:title, "Welcome")
        end
      end
  iex> menu = ExUssd.Menu.render(name: "Home", handler: MyHomeHandler)
  iex> ExUssd.simulate(menu: menu, text: "")

  {:ok, %{menu_string: "Welcome", should_close: false}}

Examples

ussd examples using ExUssd can be found here https://github.com/lenileiro/ussd_examples

Contribution

If you'd like to contribute, start by searching through the issues and pull requests to see whether someone else has raised a similar idea or question. If you don't see your idea listed, Open an issue.

Check the Contribution guide on how to contribute.

Contributors

Auto-populated from: contributors-img

<img src="https://contributors-img.firebaseapp.com/image?repo=beamkenya/ex_ussd" />

Licence

ExPesa is released under MIT License

license