# ExUssd
[![Actions Status](https://github.com/beamkenya/ex_ussd/workflows/Elixir%20CI/badge.svg)](https://github.com/beamkenya/ex_ussd/actions) ![Hex.pm](https://img.shields.io/hexpm/v/ex_ussd) ![Hex.pm](https://img.shields.io/hexpm/dt/ex_ussd)
[![Coverage Status](https://coveralls.io/repos/github/beamkenya/ex_ussd/badge.svg?branch=develop)](https://coveralls.io/github/beamkenya/ex_ussd?branch=develop)
## 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](#Installation)
- [Gateway Providers](#providers)
- [Configuration](#Configuration)
- [Documentation](#Documentation)
- [Examples](#examples)
- [Contribution](#contribution)
- [Contributors](#contributors)
- [Licence](#licence)
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `ex_ussd` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:ex_ussd, "~> 0.1.2"}
]
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/ex_ussd](https://hexdocs.pm/ex_ussd).
## Providers
ExUssd currently supports
[Africastalking API](https://africastalking.com)
[Infobip API](https://www.infobip.com/)
## 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
```elixir
config :ex_ussd, :gateway, AfricasTalking
```
### Infobip
Add below config to dev.exs / prod.exs files
```elixir
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
```elixir
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
```elixir
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
```elixir
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
```elixir
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.
```elixir
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](https://github.com/lenileiro/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](https://github.com/beamkenya/ex_ussd/issues) and [pull requests](https://github.com/beamkenya/ex_ussd/pulls) to see whether someone else has raised a similar idea or question.
If you don't see your idea listed, [Open an issue](https://github.com/beamkenya/ex_ussd/issues).
Check the [Contribution guide](contributing.md) on how to contribute.
## Contributors
Auto-populated from:
[contributors-img](https://contributors-img.firebaseapp.com/image?repo=beamkenya/ex_ussd)
<a href="https://github.com/beamkenya/ex_ussd/graphs/contributors">
<img src="https://contributors-img.firebaseapp.com/image?repo=beamkenya/ex_ussd" />
</a>
## Licence
ExPesa is released under [MIT License](https://github.com/appcues/exsentry/blob/master/LICENSE.txt)
[![license](https://img.shields.io/github/license/mashape/apistatus.svg?style=for-the-badge)](#)