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

t()

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

menu() :: t()

ExUssd menu

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 menu
  • menus :: list(menu()) :: The menu to add to the parent menu list.
  • opts :: keyword_args() :: Options to pass to ExUssd.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

  • :data Set data to pass through to next menu. N/B - ExUssd menu are stateful unless using ExUssd.new/2 with :name and :resolve as 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))
  • :delimiter Set's menu style delimiter. Default- :

  • :default_error Default error shown on invalid input

  • :name Sets the name of the menu

  • :nav Its used to set a new ExUssd Nav menu, see ExUssd.Nav.new/1

  • :orientation Sets the menu orientation. Available option;

    • :horizontal - Left to right. Blog/articles style menu
    • vertical - Top to bottom(default)
  • :resolve set the resolve function/module

  • :should_close Indicate whether to USSD session should end or continue

  • :show_navigation Set show navigation menu. Default - true

  • :split Set menu batch size. Default - 7

  • :title Set 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}}
Link to this function

to_string(menu, atom, opts)

View Source

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"
Link to this function

to_string!(menu, atom, opts)

View Source

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

Link to this callback

ussd_after_callback(menu, payload, metadata)

View Source (optional)

Specs

ussd_after_callback(
  menu :: menu(),
  payload :: payload(),
  metadata :: metadata()
) :: any()

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"
Link to this callback

ussd_callback(menu, payload, metadata)

View Source (optional)

Specs

ussd_callback(menu :: menu(), payload :: payload(), metadata :: metadata()) ::
  menu()

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"
Link to this callback

ussd_init(menu, payload)

View Source

Specs

ussd_init(menu :: menu(), payload :: payload()) :: menu()

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"