ExUssd lets you create simple, flexible, and customizable USSD interface. Under the hood ExUssd uses Elixir Registry to create and route individual USSD session.
ex_ussd.mp4
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.8"}
]
end
Checkout The example folder.
The ExUssd field can be set using Use ExUssd.set/2
Following are the setable fields
@allowed_fields [
:title, # -> USSD menu title
:next, # -> %{name: "MORE", next: "98", delimiter: ":"}
:previous, # -> %{name: "BACK", previous: "0", delimiter: ":"}
:home, # -> %{name: "HOME", previous: "00", delimiter: ":"}
:should_close, # -> Indicate Menu state, default `false`
:split, # -> Set split menu list by, default 7
:delimiter, # -> Set delimiter style, ":"
:error, # -> Custom error message, Used in the `callback/2`
:default_error, # -> Set default error message, "Invalid Choice\n", Used in the `init/2`
:show_navigation # Show navigation, default `true`
:data
]
Set ExUssd Global Default config.exs
config :ex_ussd,
default: [
next: %{name: "MORE", next: "98", delimiter: ":"},
previous: %{name: "BACK", previous: "0", delimiter: ":"},
split: 7,
default_error: "You have selected invalid option try again\n",
delimiter: ":"
]
Implement ExUssd init/2
or init/3
callback.
defmodule MyHomeHandler do
use ExUssd.Handler
def init(menu, _api_parameters) do
menu
|> ExUssd.set(title: "Welcome")
end
end
menu = ExUssd.new(name: "Home", handler: MyHomeHandler)
api_parameters = %{"service_code" => "*544#", "session_id" => "session_01", "text" => ""}
ExUssd.goto(menu: menu, api_parameters: api_parameters)
{:ok, %{menu_string: "Welcome", should_close: false}}
defmodule MyHomeHandler do
use ExUssd.Handler
def init(menu, _api_parameters, _metadata) do
menu
|> ExUssd.set(title: "Welcome")
end
end
menu = ExUssd.new(name: "Home", handler: MyHomeHandler)
api_parameters = %{"service_code" => "*544#", "session_id" => "session_01", "text" => ""}
ExUssd.goto(menu: menu, api_parameters: api_parameters)
{:ok, %{menu_string: "Welcome", should_close: false}}
Manually close USSD session, Use ExUssd.end_session/1
it takes the session_id
as params
defmodule MyHomeHandler do
use ExUssd.Handler
def init(menu, _api_parameters) do
menu
|> ExUssd.set(title: "Welcome")
|> ExUssd.set(should_close: true)
end
end
menu = ExUssd.new(name: "Home", handler: MyHomeHandler)
api_parameters = %{"service_code" => "*544#", "session_id" => "session_01", "text" => ""}
ExUssd.goto(menu: menu, api_parameters: api_parameters)
|> case do
{:ok, %{menu_string: menu_string, should_close: false}} ->
"CON " <> menu_string
{:ok, %{menu_string: menu_string, should_close: true}} ->
# End Session
ExUssd.end_session(session_id: "session_01")
"END " <> menu_string
end
Use ExUssd.add/2
to add to USSD menu list.
The USSD menu list is []
by default.
defmodule ProductAHandler do
use ExUssd.Handler
def init(menu, _api_parameters) do
menu |> ExUssd.set(title: "selected product a")
end
end
defmodule ProductBHandler do
use ExUssd.Handler
def init(menu, _api_parameters) do
menu |> ExUssd.set(title: "selected product b")
end
end
defmodule ProductCHandler do
use ExUssd.Handler
def init(menu, _api_parameters) do
menu
|> ExUssd.set(title: "selected product c")
end
end
defmodule MyHomeHandler do
use ExUssd.Handler
def init(menu, _api_parameters) do
menu
|> ExUssd.set(title: "Welcome")
|> ExUssd.add(ExUssd.new(name: "Product A", handler: ProductAHandler))
|> ExUssd.add(ExUssd.new(name: "Product B", handler: ProductBHandler))
|> ExUssd.add(ExUssd.new(name: "Product C", handler: ProductCHandler))
end
end
menu = ExUssd.new(name: "Home", handler: MyHomeHandler)
Use ExUssd.add/2
to add to USSD menu list on Individual USSD menu.
defmodule ProductCHandler do
use ExUssd.Handler
def init(menu, _api_parameters) do
menu
|> ExUssd.set(title: "selected product c")
end
end
defmodule ProductBHandler do
use ExUssd.Handler
def init(menu, _api_parameters) do
menu
|> ExUssd.set(title: "selected product b")
|> ExUssd.add(ExUssd.new(name: "Product C", handler: ProductCHandler))
end
end
defmodule ProductAHandler do
use ExUssd.Handler
def init(menu, _api_parameters) do
menu
|> ExUssd.set(title: "selected product a")
|> ExUssd.add(ExUssd.new(name: "Product B", handler: ProductBHandler))
end
end
defmodule MyHomeHandler do
use ExUssd.Handler
def init(menu, _api_parameters) do
menu
|> ExUssd.set(title: "Welcome")
|> ExUssd.add(ExUssd.new(name: "Product A", handler: ProductAHandler))
end
end
menu = ExUssd.new(name: "Home", handler: MyHomeHandler)
Implement after_route/1
function on your USSD handler module.
after_route/1
callback returns navigation status.
User passes in invalid
- response
%{
state: :error,
menu: menu,
payload: %{
api_parameters: api_parameters,
metadata: metadata
}
}
User passes in valid input, name navigated to next menu
- response
%{
state: :ok,
payload: %{
api_parameters: api_parameters,
metadata: metadata
}
}
# ...
defmodule MyHomeHandler do
use ExUssd.Handler
def init(menu, _api_parameters) do
menu
|> ExUssd.set(title: "Welcome")
|> ExUssd.add(ExUssd.new(name: "Product A", handler: ProductAHandler))
|> ExUssd.add(ExUssd.new(name: "Product B", handler: ProductBHandler))
|> ExUssd.add(ExUssd.new(name: "Product C", handler: ProductCHandler))
end
def after_route(%{state: :ok, payload: payload}) do
IO.inspect payload
end
def after_route(%{state: :error, menu: menu, payload: payload}) do
IO.inspect payload
end
end
menu = ExUssd.new(name: "Home", handler: MyHomeHandler)
Implement ExUssd callback/2
or callback/3
in the event you need to validate the Users input
# ...
defmodule MyHomeHandler do
use ExUssd.Handler
def init(menu, _api_parameters) do
menu
|> ExUssd.set(title: "Welcome")
|> ExUssd.add(ExUssd.new(name: "Product A", handler: ProductAHandler))
|> ExUssd.add(ExUssd.new(name: "Product B", handler: ProductBHandler))
|> ExUssd.add(ExUssd.new(name: "Product C", handler: ProductCHandler))
end
def callback(menu, api_parameters) do
case api_parameters.text == "5555" do
true ->
menu
|> ExUssd.set(title: "You have Entered the Secret Number, 5555")
|> ExUssd.set(should_close: true)
_ ->
menu
end
end
end
menu = ExUssd.new(name: "Home", handler: MyHomeHandler)
api_parameters = %{"service_code" => "*544#", "session_id" => "session_01", "text" => "5555"}
ExUssd.goto(menu: menu, api_parameters: api_parameters)
{:ok, %{menu_string: "You have Entered the Secret Number, 5555", should_close: false}}
# ...
defmodule MyHomeHandler do
use ExUssd.Handler
def init(%{data: data} = menu, _api_parameters) do
IO.inspect data
menu
|> ExUssd.set(title: "Welcome")
|> ExUssd.add(ExUssd.new(name: "Product A", handler: ProductAHandler))
|> ExUssd.add(ExUssd.new(name: "Product B", handler: ProductBHandler))
|> ExUssd.add(ExUssd.new(name: "Product C", handler: ProductCHandler))
end
def callback(menu, api_parameters) do
case api_parameters.text == "5555" do
true ->
menu
|> ExUssd.set(title: "You have Entered the Secret Number, 5555")
|> ExUssd.set(should_close: true)
_ ->
menu
end
end
end
defmodule PinHandler do
use ExUssd.Handler
def init(menu, _api_parameters) do
menu
|> ExUssd.set(title: "Enter your pin number")
|> ExUssd.set(show_navigation: false)
end
def callback(menu, api_parameters) do
case api_parameters.text == "4321" do
true ->
menu
# |> ExUssd.navigate(data: %{name: "John"}, handler: MyHomeHandler)
|> ExUssd.navigate(handler: MyHomeHandler)
_ ->
menu
|> ExUssd.set(error: "Wrong pin number\n")
end
end
def after_route(%{payload: %{metadata: %{attempts: attempts}}} = attrs) do
if(Map.get(attrs, :menu) && attempts == 3) do
attrs.menu
|> ExUssd.set(title: "Max retry reached, account locked")
|> ExUssd.set(error: "")
|> ExUssd.set(should_close: true)
end
end
end
menu = ExUssd.new(name: "Check PIN", handler: PinHandler)
# ...
# ...
defmodule SubCountyHandler do
use ExUssd.Handler
def init(%{data: %{name: name}} = menu, api_parameters) do
# TODO: Fetch county sub locations by county_code
# Make dynamic location menus for the county
# Split by 6 / 7
menu
|> ExUssd.set(title: "#{name} County")
end
end
defmodule CountyHandler do
use ExUssd.Handler
def init(menu, _api_parameters) do
menus =
fetch_api()
|> Enum.map(fn %{name: name} = data ->
ExUssd.new(name: name, data: data)
end)
menu
|> ExUssd.set(title: "List of Counties")
|> ExUssd.dynamic(
menus: menus,
handler: App.Dymanic.Vertical.SubCountyHandler,
orientation: :vertical
)
end
def fetch_api do
[
%{county_code: 47, name: "Nairobi"},
%{county_code: 01, name: "Mombasa"},
%{county_code: 42, name: "Kisumu"}
]
end
end
defmodule MyHomeHandler do
use ExUssd.Handler
def init(menu, _api_parameters) do
menu
|> ExUssd.set(title: "Welcome")
|> ExUssd.add(ExUssd.new(name: "Counties List", handler: CountyHandler))
end
end
menu = ExUssd.new(name: "Home", handler: MyHomeHandler)
# ...
{:ok, %{
menu_string: "List of Counties\n1:Nairobi\n2:Mombasa\n3:Kisumu\n0:BACK",
should_close: false
}}
Note: The name value is Truncated after 140 characters
defmodule NewsHandler do
use ExUssd.Handler
def init(menu, _api_parameters) do
menus = fetch_api() |> Enum.map(fn %{"title"=> title, "body"=> body} ->
ExUssd.new(name: title <> "\n" <> body)
end)
menu
|> ExUssd.set(title: "World News")
|> ExUssd.dynamic(menus: menus, orientation: :horizontal)
end
def fetch_api do
[
%{
"userId" => 1,
"id" => 1,
"title" => "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body" =>
"quia et suscipit suscipit recusandae consequuntur expedita et cum reprehenderit molestiae ut ut quas totam nostrum rerum est autem sunt rem eveniet architecto"
},
%{
"userId" => 1,
"id" => 2,
"title" => "qui est esse",
"body" =>
"est rerum tempore vitae sequi sint nihil reprehenderit dolor beatae ea dolores neque fugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis qui aperiam non debitis possimus qui neque nisi nulla"
},
%{
"userId" => 1,
"id" => 3,
"title" => "ea molestias quasi exercitationem repellat qui ipsa sit aut",
"body" =>
"et iusto sed quo iure voluptatem occaecati omnis eligendi aut ad voluptatem doloribus vel accusantium quis pariatur molestiae porro eius odio et labore et velit aut"
}
]
end
end
defmodule MyHomeHandler do
use ExUssd.Handler
def init(menu, _api_parameters) do
menu
|> ExUssd.set(title: "BBC News")
|> ExUssd.add(ExUssd.new(name: "News", handler: NewsHandler))
# |> ExUssd.add(ExUssd.new(name: "WorkLife", handler: WorkLifeHandler))
# |> ExUssd.add(ExUssd.new(name: "Sports", handler: SportsHandler))
end
end
menu = ExUssd.new(name: "Home", handler: MyHomeHandler)
# ...
{:ok, %{
menu_string: "1/3\nsunt aut facere repellat provident occaecati excepturi optio reprehenderit\nquia et suscipit suscipit recusandae consequuntur expedita et ...\n0:BACK 98:MORE",
should_close: false
}}
Update your router's configuration to forward requests to ExUssd Simulator, it takes menu
and phone_numbers
list
# lib/my_app_web/router.ex
use MyAppWeb, :router
import ExUssd
...
if Mix.env() == :dev do
scope "/" do
pipe_through :browser
simulate "/simulator",
menu: ExUssd.new(name: "Home", handler: MyHomeHandler),
phone_numbers: ["254700100100", "254700200200", "254700300300"]
end
end