ExUssd behaviour (ExUssd v1.1.0) View Source
Link to this section Summary
Types
ExUssd menu
It's a map of metadata about the session The map contains the following keys
The Gateway payload value
ExUssd menu structure
Functions
Add menu to ExUssd menu list.
Add menus to ExUssd menu list.
Teminates session.
ExUssd.goto/1 is called when the gateway provider calls the callback URL.
ExUssd.new/1 - Creates a new ExUssd menu.
ExUssd.new/2 - Creates a new ExUssd menu.
ExUssd.set/2 - Sets the menu field.
Use ExUssd.to_string/2 to get menu string representation and should close value which indicates if the session.
ExUssd.to_string/2 takes a menu and opts.
ExUssd.to_string/3 is similar to ExUssd.to_string/2
The only difference is that it takes a menu, atom and opts.
Its used to test the menu life cycle.
ExUssd.to_string!/2 gets the menu string text from ExUssd.to_string/2
See ExUssd.to_string/2 for more details.
ExUssd.to_string!/3 gets the menu string text from ExUssd.to_string/3
See ExUssd.to_string/3 for more details.
Example
Callbacks
ussd_after_callback/3 is a the third life cycle method.
This callback is invoked every time before ussd menu is rendered. It's invoke after menu_list is resolved.
You can think of ussd_after_callback/3 as a optional clean up callback.
ussd_callback/3 is a the second life cycle method.
This callback is invoked every time the user enters a value into the current menu.
You can think of ussd_callback/3 as a optional validation callback.
ExUssd provides different life cycle methods for your menu.
Link to this section Types
Specs
metadata() :: map()
It's a map of metadata about the session The map contains the following keys:
- attempt: The number of attempts the user has made to enter the menu
- invoked_at: The time the menu was invoked
- route: The route that was invoked
- text: This is the text that was entered by the user. We receive this from the gateway payload.
Example:
%{attempt: %{count: 2, input: ["wrong2", "wrong1"]}, invoked_at: ~U[2024-09-25 09:10:15Z], route: "*555*1#", text: "1"}
Specs
payload() :: map()
The Gateway payload value
Typically you will Register a callback URL with your gateway provider that they will call whenever they get a request from a client.
Example: You would have a simple endpoint that you receive POST requests from your gateway provider.
# Africa talking callback URL
post "v1/ussd" do
payload = conn.params
# With the payload you can call `ExUssd.goto/1`
menu = ExUssd.new(name: "HOME", resolve: AppWeb.HomeResolver)
case ExUssd.goto(menu: menu, payload: payload) 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_id)
"END " <> menu_string
end
end
Specs
t() :: %ExUssd{
data: any(),
default_error: String.t(),
delimiter: String.t(),
error: String.t(),
is_zero_based: boolean(),
menu_list: [t()],
name: String.t(),
nav: [ExUssd.Nav.t()],
navigate: (... -> any()),
orientation: atom(),
parent: t(),
resolve: (... -> any()) | mfa(),
should_close: boolean(),
show_navigation: boolean(),
split: term(),
title: String.t()
}
ExUssd menu structure
Link to this section Functions
Add menu to ExUssd menu list.
Arguments:
- menu :: menu() :: The parent menu
- menu :: menu() :: The menu to add to the parent menu list.
Example
iex> resolve = fn menu, _payload ->
...> menu =
...> menu
...> |> ExUssd.set(title: "Menu title")
...> |> ExUssd.add(ExUssd.new(name: "option 1", resolve: &({:ok, ExUssd.set(&1, title: "option 1")})))
...> |> ExUssd.add(ExUssd.new(name: "option 2", resolve: &({:ok, ExUssd.set(&1, title: "option 2")})))
...> {:ok, menu}
...> end
iex> menu = ExUssd.new(name: "HOME", resolve: resolve)
iex> ExUssd.to_string!(menu, [])
"Menu title\n1:option 1\n2:option 2"
Add menus to ExUssd menu list.
Arguments:
menu:: menu() :: The parent menumenus:: list(menu()) :: The menu to add to the parent menu list.opts:: keyword_args() :: Options to pass toExUssd.add/3
Example:
iex> defmodule AppWeb.LocationResolver do
...> use ExUssd
...> def ussd_init(menu, _) do
...> # Get locations from database
...> locations = Enum.map(1..5, &Integer.to_string/1)
...> # convert locations to menus
...> menus = Enum.map(locations, fn location ->
...> ExUssd.new(name: "Location " <> location, data: %{name: location})
...> end)
...> menu =
...> menu
...> |> ExUssd.set(title: "Select Location")
...> |> ExUssd.add(menus, resolve: &(ExUssd.set(&1, title: "Location " <> &1.data.name)))
...> {:ok, menu}
...> end
...> end
iex> menu = ExUssd.new(name: "HOME", resolve: AppWeb.LocationResolver)
iex> ExUssd.to_string!(menu, [])
"Select Location\n1:Location 1\n2:Location 2\n3:Location 3\n4:Location 4\n5:Location 5"
Teminates session.
ExUssd.end_session(session_id: "sn1")
ExUssd.goto/1 is called when the gateway provider calls the callback URL.
Keyword Arguments:
payload: The payload from the gateway provider.menu: The menu to be rendered.
Example:
case ExUssd.goto(menu: menu, payload: payload) 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_id)
"END " <> menu_string
end
ExUssd.new/1 - Creates a new ExUssd menu.
Keyword Arguments:
name:: The name of the menu.resolve:: The resolve function to be called when the menu is selected.orientation:: The orientation of the menu.is_zero_based:: indicates whether the menu list is zero based.
Example:
iex> resolve = fn menu, _payload ->
...> menu =
...> menu
...> |> ExUssd.set(title: "Menu title")
...> |> ExUssd.add(ExUssd.new(name: "option 1", resolve: &({:ok, ExUssd.set(&1, title: "option 1")})))
...> |> ExUssd.add(ExUssd.new(name: "option 2", resolve: &({:ok, ExUssd.set(&1, title: "option 2")})))
...> {:ok, menu}
...> end
iex> menu = ExUssd.new(name: "HOME", resolve: resolve)
iex> ExUssd.to_string!(menu, [])
"Menu title\n1:option 1\n2:option 2"
iex> # Change menu orientation
iex> menu = ExUssd.new(name: "HOME", resolve: resolve, orientation: :horizontal)
iex> ExUssd.to_string!(menu, [])
"1:2\noption 1\nMORE:98"zero based
Used when the menu list is zero based.
Example:
iex> resolve = fn menu, _payload ->
...> menu =
...> menu
...> |> ExUssd.set(title: "Menu title")
...> |> ExUssd.add(ExUssd.new(name: "offers", resolve: fn menu, _ -> {:ok, ExUssd.set(menu, title: "offers")} end))
...> |> ExUssd.add(ExUssd.new(name: "option 1", resolve: &({:ok, ExUssd.set(&1, title: "option 1")})))
...> |> ExUssd.add(ExUssd.new(name: "option 2", resolve: &({:ok, ExUssd.set(&1, title: "option 2")})))
...> {:ok, menu}
...> end
iex> menu = ExUssd.new(name: "HOME", is_zero_based: true, resolve: resolve)
iex> ExUssd.to_string!(menu, [])
"Menu title\n0:offers\n1:option 1\n2:option 2"
iex> ExUssd.to_string!(menu, [simulate: true, payload: %{text: "0"}])
"offers"NOTE:
ExUssd.new/1 can be used to create a menu with a callback function.
Use the anonymous function syntax to create menu if you want to perform some action before the menu is rendered.
Remember to use ExUssd.set to set the menu name and the resolve function/module.
Example:
iex> defmodule User do
...> def get_user(phone_number), do: %{name: "John", phone_number: phone_number, type: :personal}
...> end
iex> defmodule HomeResolver do
...> def home(%ExUssd{data: %{name: name}} = menu, _) do
...> menu =
...> menu
...> |> ExUssd.set(title: "Welcome " <> name)
...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2))
...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2))
...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2))
...> {:ok, menu}
...> end
...> def product_a(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product a")}
...> def product_b(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product b")}
...> def product_c(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product c")}
...> end
iex> menu = ExUssd.new(fn menu, %{phone: phone} = _payload ->
...> user = User.get_user(phone)
...> menu =
...> menu
...> |> ExUssd.set(name: "Home")
...> |> ExUssd.set(data: user)
...> |> ExUssd.set(resolve: &HomeResolver.home/2)
...> {:ok, menu}
...> end)
iex> ExUssd.to_string!(menu, [payload: %{text: "*544#", phone: "072000000"}])
"Welcome John\n1:Product A\n2:Product B\n3:Product C"You can also use the anonymous function syntax to create menu if you want to create dymamic menu name.
Example:
iex> defmodule User do
...> def get_user(phone_number), do: %{name: "John", phone_number: phone_number, type: :personal}
...> end
iex> defmodule HomeResolver do
...> def home(menu, %{phone: phone} = _payload) do
...> user = User.get_user(phone)
...> menu =
...> menu
...> |> ExUssd.set(title: "Welcome "<> user.name)
...> |> ExUssd.set(data: user)
...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2))
...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2))
...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2))
...> |> ExUssd.add(ExUssd.new(&account/2))
...> {:ok, menu}
...> end
...> def product_a(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product a")}
...> def product_b(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product b")}
...> def product_c(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product c")}
...> def account(%{data: %{type: :personal, name: name}} = menu, _payload) do
...> # Should be stateless, don't put call functions with side effect (Insert to DB, fetch)
...> # Because it will be called every time the menu is rendered because the menu `:name` is dynamic
...> # See `ExUssd.new/2` for more details where `:name` is static.
...> menu =
...> menu
...> |> ExUssd.set(name: "Personal account")
...> |> ExUssd.set(resolve: &({:ok, ExUssd.set(&1, title: "Personal account")}))
...> {:ok, menu}
...> end
...> def account(%{data: %{type: :business, name: name}} = menu, _payload) do
...> # Should be stateless, don't put call functions with side effect (Insert to DB, fetch)
...> # Because it will be called every time the menu is rendered because the menu `:name` is dynamic
...> # See `ExUssd.new/2` for more details where `:name` is static.
...> menu =
...> menu
...> |> ExUssd.set(name: "Business account")
...> |> ExUssd.set(resolve: &({:ok, ExUssd.set(&1, title: "Business account")}))
...> {:ok, menu}
...> end
...> end
iex> menu = ExUssd.new(name: "HOME", resolve: &HomeResolver.home/2)
iex> ExUssd.to_string!(menu, [payload: %{text: "*544#", phone: "072000000"}])
"Welcome John\n1:Product A\n2:Product B\n3:Product C\n4:Personal account"
ExUssd.new/2 - Creates a new ExUssd menu.
Arguments: name: The name of the menu. resolve: The resolve function/module.
It similiar to ExUssd.new/1 that takes callback function.
The only difference is that it takes a static name argument.
Example:
iex> defmodule User do
...> def get_user(phone_number), do: %{name: "John", phone_number: phone_number, type: :personal}
...> end
iex> defmodule HomeResolver do
...> def home(menu, %{phone: phone} = _payload) do
...> user = User.get_user(phone)
...> menu =
...> menu
...> |> ExUssd.set(title: "Welcome "<> user.name)
...> |> ExUssd.set(data: user)
...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2))
...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2))
...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2))
...> |> ExUssd.add(ExUssd.new("Account", &account/2))
...> {:ok, menu}
...> end
...> def product_a(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product a")}
...> def product_b(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product b")}
...> def product_c(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product c")}
...> def account(%{data: %{type: :personal}} = menu, _payload) do
...> # Get Personal account details, then set as data
...> {:ok, ExUssd.set(menu, resolve: &({:ok, ExUssd.set(&1, title: "Personal account")}))}
...> end
...> def account(%{data: %{type: :business, name: name}} = menu, _payload) do
...> # Get Business account details, then set as data
...> {:ok, ExUssd.set(menu, resolve: &({:ok, ExUssd.set(&1, title: "Business account")}))}
...> end
...> end
iex> menu = ExUssd.new(name: "HOME", resolve: &HomeResolver.home/2)
iex> ExUssd.to_string!(menu, [payload: %{text: "*544#", phone: "072000000"}])
"Welcome John\n1:Product A\n2:Product B\n3:Product C\n4:Account"
ExUssd.set/2 - Sets the menu field.
Arguments: menu: The menu to set. field: The field to set.
It sets the field of the menu.
Settable Fields
:dataSet data to pass through to next menu. N/B - ExUssd menu are stateful unless usingExUssd.new/2with:nameand:resolveas arguments;data = %{name: "John Doe"} # stateful menu |> ExUssd.set(data: data) |> ExUssd.add(ExUssd.new(&check_balance/2)) menu |> ExUssd.set(data: data) |> ExUssd.add(ExUssd.new("Check Balance", &check_balance/2)) # stateless menu |> ExUssd.add(ExUssd.new(data: data, name: "Check Balance", resolve: &check_balance/2)):delimiterSet's menu style delimiter. Default-::default_errorDefault error shown on invalid input:nameSets the name of the menu:navIts used to set a new ExUssd Nav menu, seeExUssd.Nav.new/1:orientationSets the menu orientation. Available option;:horizontal- Left to right. Blog/articles style menuvertical- Top to bottom(default)
:resolveset the resolve function/module:should_closeIndicate whether to USSD session should end or continue:show_navigationSet show navigation menu. Default -true:splitSet menu batch size. Default - 7:titleSet menu title
Use ExUssd.to_string/2 to get menu string representation and should close value which indicates if the session.
ExUssd.to_string/2 takes a menu and opts.
Example:
iex> defmodule AppWeb.ProductResolver do
...> use ExUssd
...> def ussd_init(menu, _) do
...> menu =
...> menu
...> |> ExUssd.set(title: "Product List")
...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2))
...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2))
...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2))
...> {:ok, menu}
...> end
...> def product_a(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product a")}
...> def product_b(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product b")}
...> def product_c(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product c")}
...> end
iex> menu = ExUssd.new(name: "HOME", resolve: AppWeb.ProductResolver)
iex> # Simulate the first time user enters the menu
iex> ExUssd.to_string(menu, [])
{:ok, %{menu_string: "Product List\n1:Product A\n2:Product B\n3:Product C", should_close: false}}
iex> # To simulate a user selecting option "1"
iex> ExUssd.to_string(menu, [simulate: true, payload: %{text: "1"}])
{:ok, %{menu_string: "selected product a", should_close: false}}NOTE:
If your ussd_init/2 callback expects data field to have values, Use init_data.
Example:
iex> defmodule AppWeb.ProductResolver do
...> use ExUssd
...> def ussd_init(%ExUssd{data: %{user_name: user_name}} = menu, _) do
...> menu =
...> menu
...> |> ExUssd.set(title: "Welcome " <> user_name <> ", Select Product")
...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2))
...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2))
...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2))
...> {:ok, menu}
...> end
...> def product_a(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product a")}
...> def product_b(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product b")}
...> def product_c(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product c")}
...> end
iex> menu = ExUssd.new(name: "HOME", resolve: AppWeb.ProductResolver)
iex> # Simulate the first time user enters the menu
iex> ExUssd.to_string(menu, init_data: %{user_name: "John"})
{:ok, %{menu_string: "Welcome John, Select Product\n1:Product A\n2:Product B\n3:Product C", should_close: false}}
ExUssd.to_string/3 is similar to ExUssd.to_string/2
The only difference is that it takes a menu, atom and opts.
Its used to test the menu life cycle.
Example:
iex> defmodule AppWeb.PinResolver do
...> use ExUssd
...> def ussd_init(menu, _) do
...> {:ok, ExUssd.set(menu, title: "Enter your PIN")}
...> end
...> def ussd_callback(menu, payload, _) do
...> if payload.text == "5555" do
...> {:ok, ExUssd.set(menu, resolve: &success_menu/2)}
...> end
...> end
...> def success_menu(menu, _) do
...> menu =
...> menu
...> |> ExUssd.set(title: "You have Entered the Secret Number, 5555")
...> |> ExUssd.set(should_close: true)
...> {:ok, menu}
...> end
...> end
iex> menu = ExUssd.new(name: "PIN", resolve: AppWeb.PinResolver)
iex> # Get `ussd_init/2` menu string representation
iex> ExUssd.to_string(menu, :ussd_init, [])
{:ok, %{menu_string: "Enter your PIN", should_close: false}}
iex> # Get `ussd_callback/2` menu string representation
iex> ExUssd.to_string(menu, :ussd_callback, payload: %{text: "5555"})
{:ok, %{menu_string: "You have Entered the Secret Number, 5555", should_close: true}}
ExUssd.to_string!/2 gets the menu string text from ExUssd.to_string/2
See ExUssd.to_string/2 for more details.
Example:
iex> defmodule AppWeb.ProductResolver do
...> def products(menu, _) do
...> menu =
...> menu
...> |> ExUssd.set(title: "Product List")
...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2))
...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2))
...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2))
...> {:ok, menu}
...> end
...> def product_a(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product a")}
...> def product_b(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product b")}
...> def product_c(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product c")}
...> end
iex> menu = ExUssd.new(name: "HOME", resolve: &AppWeb.ProductResolver.products/2)
iex> # Simulate the first time user enters the menu
iex> ExUssd.to_string!(menu, [])
"Product List\n1:Product A\n2:Product B\n3:Product C"
ExUssd.to_string!/3 gets the menu string text from ExUssd.to_string/3
See ExUssd.to_string/3 for more details.
Example:
iex> defmodule AppWeb.PinResolver do
...> use ExUssd
...> def ussd_init(menu, _) do
...> {:ok, ExUssd.set(menu, title: "Enter your PIN")}
...> end
...> def ussd_callback(menu, payload, _) do
...> if payload.text == "5555" do
...> {:ok, ExUssd.set(menu, resolve: &success_menu/2)}
...> end
...> end
...> def success_menu(menu, _) do
...> menu =
...> menu
...> |> ExUssd.set(title: "You have Entered the Secret Number, 5555")
...> |> ExUssd.set(should_close: true)
...> {:ok, menu}
...> end
...> end
iex> menu = ExUssd.new(name: "PIN", resolve: AppWeb.PinResolver)
iex> # Get `ussd_init/2` menu string representation
iex> ExUssd.to_string!(menu, :ussd_init, [])
"Enter your PIN"
iex> # Get `ussd_callback/2` menu string representation
iex> ExUssd.to_string!(menu, :ussd_callback, payload: %{text: "5555"})
"You have Entered the Secret Number, 5555"
Link to this section Callbacks
Specs
ussd_after_callback/3 is a the third life cycle method.
This callback is invoked every time before ussd menu is rendered. It's invoke after menu_list is resolved.
You can think of ussd_after_callback/3 as a optional clean up callback.
Example:
iex> defmodule AppWeb.ProductResolver do
...> use ExUssd
...> def ussd_init(menu, _) do
...> menu =
...> menu
...> |> ExUssd.set(title: "Product List")
...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2))
...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2))
...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2))
...> {:ok, menu}
...> end
...> def ussd_after_callback(%{error: true} = _menu, _payload, _metadata) do
...> # Use the gateway payload and metadata to capture user metrics on error
...> end
...> def ussd_after_callback(_menu, _payload, _metadata) do
...> # Use the gateway payload and metadata to capture user metrics before navigating to next menu
...> end
...> def product_a(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product a")}
...> def product_b(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product b")}
...> def product_c(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product c")}
...> end
iex> menu = ExUssd.new(name: "HOME", resolve: AppWeb.ProductResolver)
iex> # To simulate a user selecting option "1"
iex> ExUssd.to_string!(menu, simulate: true, payload: %{text: "1"})
"selected product a"
iex> # To simulate a user selecting invalid option "42"
iex> ExUssd.to_string!(menu, simulate: true, payload: %{text: "42"})
"Invalid Choice\nProduct List\n1:Product A\n2:Product B\n3:Product C" # Note:
ussd_after_callback/3 can to used to render menu if set conditions are met.
For example, you can use ussd_after_callback/3 to render a custom menu if the user has not entered the correct PIN.
Example:
iex> defmodule AppWeb.HomeResolver do
...> use ExUssd
...> def ussd_init(menu, _) do
...> {:ok, ExUssd.set(menu, title: "Enter your PIN")}
...> end
...> def ussd_callback(menu, payload, _) do
...> if payload.text == "5555" do
...> {:ok, ExUssd.set(menu, resolve: &success_menu/2)}
...> else
...> {:error, "Wrong PIN"}
...> end
...> end
...> def ussd_after_callback(%{error: true} = menu, _payload, %{attempt: %{count: 3}}) do
...> menu =
...> menu
...> |> ExUssd.set(title: "Account is locked, you have entered the wrong PIN 3 times")
...> |> ExUssd.set(should_close: true)
...> {:ok, menu}
...> end
...> def success_menu(menu, _) do
...> menu =
...> menu
...> |> ExUssd.set(title: "You have Entered the Secret Number, 5555")
...> |> ExUssd.set(should_close: true)
...> {:ok, menu}
...> end
...> end
iex> # To simulate a user entering wrong PIN 3 times.
iex> menu = ExUssd.new(name: "PIN", resolve: AppWeb.HomeResolver)
iex> ExUssd.to_string!(menu, :ussd_after_callback, payload: %{text: "5556", attempt: %{count: 3}})
"Account is locked, you have entered the wrong PIN 3 times"
Specs
ussd_callback/3 is a the second life cycle method.
This callback is invoked every time the user enters a value into the current menu.
You can think of ussd_callback/3 as a optional validation callback.
ussd_callback/3 is called with the following arguments:
- menu: The menu that was invoked
- payload: The payload that was received from the gateway provider
- metadata: The metadata about the session
Example:
iex> defmodule AppWeb.PinResolver do
...> use ExUssd
...> def ussd_init(menu, _) do
...> {:ok, ExUssd.set(menu, title: "Enter your PIN")}
...> end
...> def ussd_callback(menu, payload, _) do
...> if payload.text == "5555" do
...> {:ok, ExUssd.set(menu, resolve: &success_menu/2)}
...> else
...> {:error, "Wrong PIN"}
...> end
...> end
...> def success_menu(menu, _) do
...> menu =
...> menu
...> |> ExUssd.set(title: "You have Entered the Secret Number, 5555")
...> |> ExUssd.set(should_close: true)
...> {:ok, menu}
...> end
...> end
iex> # To simulate a user entering correct PIN, you can use the `ExUssd.to_string/3` method.
iex> menu = ExUssd.new(name: "HOME", resolve: AppWeb.PinResolver)
iex> ExUssd.to_string!(menu, :ussd_callback, [payload: %{text: "5555"}])
"You have Entered the Secret Number, 5555"
iex> # To simulate a user entering wrong PIN, you can use the `ExUssd.to_string/3` method.
iex> menu = ExUssd.new(name: "HOME", resolve: AppWeb.PinResolver)
iex> ExUssd.to_string!(menu, :ussd_callback, payload: %{text: "5556"})
"Wrong PIN\nEnter your PIN"Note:
Use default_error
ussd_callback/3 will use the default error message if the callback returns false.
Example:
iex> defmodule AppWeb.PinResolver do
...> use ExUssd
...> def ussd_init(menu, _) do
...> {:ok, ExUssd.set(menu, title: "Enter your PIN")}
...> end
...> def ussd_callback(menu, payload, _) do
...> if payload.text == "5555" do
...> {:ok, ExUssd.set(menu, resolve: &success_menu/2)}
...> end
...> end
...> def success_menu(menu, _) do
...> menu =
...> menu
...> |> ExUssd.set(title: "You have Entered the Secret Number, 5555")
...> |> ExUssd.set(should_close: true)
...> {:ok, menu}
...> end
...> end
iex> # To simulate a user entering wrong PIN.
iex> menu = ExUssd.new(name: "PIN", resolve: AppWeb.PinResolver)
iex> ExUssd.to_string!(menu, :ussd_callback, payload: %{text: "5556"})
"Invalid Choice\nEnter your PIN"Life cycle
ussd_callback/3 is called before ussd menu list is resolved.
If the callback returns false or it's not implemented, ExUssd will proccess to resolve the user input section from the menu list.
Example:
iex> defmodule AppWeb.ProductResolver do
...> use ExUssd
...> def ussd_init(menu, _) do
...> menu =
...> menu
...> |> ExUssd.set(title: "Product List, Enter 5555 for Offers")
...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2))
...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2))
...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2))
...> {:ok, menu}
...> end
...> def ussd_callback(menu, payload, _) do
...> if payload.text == "5555" do
...> {:ok, ExUssd.set(menu, resolve: &product_offer/2)}
...> end
...> end
...> def product_a(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product a")}
...> def product_b(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product b")}
...> def product_c(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product c")}
...> def product_offer(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product offer")}
...> end
iex> menu = ExUssd.new(name: "HOME", resolve: AppWeb.ProductResolver)
iex> # To simulate a user entering "5555"
iex> ExUssd.to_string!(menu, simulate: true, payload: %{text: "5555"})
"selected product offer"
iex> # To simulate a user selecting option "1"
iex> ExUssd.to_string!(menu, simulate: true, payload: %{text: "1"})
"selected product a"
Specs
ExUssd provides different life cycle methods for your menu.
ussd_init/2
This callback must be implemented in your module as it is called when the menu is first invoked.
This callback is invoked once
ussd_init/2 is called with the following arguments:
- menu: The menu that was invoked
- payload: The payload that was received from the gateway provider
Example:
iex> defmodule AppWeb.HomeResolver do
...> use ExUssd
...> def ussd_init(menu, _) do
...> {:ok, ExUssd.set(menu, title: "Enter your PIN")}
...> end
...> end
iex> # To simulate a user entering a PIN, you can use the `ExUssd.to_string/2` method.
iex> menu = ExUssd.new(name: "HOME", resolve: AppWeb.HomeResolver)
iex> ExUssd.to_string!(menu, :ussd_init, [])
"Enter your PIN"