Packages

Build Ussd (Unstructured Supplementary Service Data) applications in Elixir without breaking a sweat.

Current section

Files

Jump to
ussd README.md
Raw

README.md

# Ussd
![Hex.pm](https://img.shields.io/hexpm/v/ussd) ![Hex.pm](https://img.shields.io/hexpm/dt/ussd)
Build USSD (Unstructured Supplementary Service Data) applications in Elixir without
breaking a sweat.
This README covers installation and a quick example. For every feature in depth - Record,
Decisions, Pagination & Truncation, Resuming Sessions, Configurators, Localization,
Encrypted Records, Gateway Responses, the `mix ussd.graph`/`ussd.simulate`/`ussd.lint`
tasks, Testing, and more - see the **[full guide](GUIDE.md)**.
## Installation
Add `ussd` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:ussd, "~> 0.2.0"}
]
end
```
## Features
- **Menus as modules** — define screens as `Ussd.State` modules with a fluent `Ussd.Menu`
builder, and route between them declaratively with `transition/2`.
- **Built-in decisions** — match input with `Equal`, `Between`, `In`, `Regex`, `IsNumeric`
and more out of the box; scaffold custom ones with `mix ussd.gen.decision`.
- **Conditional branching**`Ussd.Action` modules decide the next state at runtime (e.g.
after an HTTP call), for flows that can't be expressed as static transitions.
- **Back navigation**`back/2` with an automatic per-session history stack, no manual
bookkeeping.
- **Automatic pagination**`use Ussd.Pagination` plus `paginate/1` page long listings
without manual bookkeeping.
- **Response truncation**`truncate/1` caps how many characters a screen returns, so
dynamic content can't blow past your gateway's character limit.
- **Resumable sessions**`use_continuing_state/4` lets a redial pick back up where a
timed-out session left off, silently or after confirming with the user.
- **Configurators** — group and share repeated setup (response format, exception handling,
etc.) across entry points.
- **Localized menus** — build menu content from a Gettext backend, with locale persisted
across a session.
- **Session records with real expiry** — a `Ussd.Record` API (`get`/`set`/`increment`/
`decrement`/...) for persisting data during a session, backed by a pluggable `Ussd.Cache`
with actual TTL, swept periodically instead of leaking forever.
- **Encrypted session data** — store sensitive values (PINs, account numbers) encrypted at
rest via `set_encrypted`/`get_encrypted`.
- **Built-in gateway responses** — ships `Ussd.Response` formatters for Speso,
Africa's Talking, Nsano, Nalo, Moolre and Arkesel; scaffold your own with
`mix ussd.gen.response`.
- **Exception handling** — implement `Ussd.ExceptionHandler` to turn an unhandled
exception into a message the caller sees, instead of a dead session.
- **Flow visualization**`mix ussd.graph` renders a Mermaid state diagram of a flow from
its `transition/2`/`back/2`/`terminate/0` declarations.
- **Interactive simulation**`mix ussd.simulate` lets you walk a flow in the terminal
like a real handset, no gateway or phone required.
- **Flow linting**`mix ussd.lint` catches dead ends, broken transitions, duplicate
matches and unreachable states before they ship.
- **Testing utilities** — a fluent `Ussd.Test` API for asserting screens, context and
session state across multi-step conversations.
- **Session events**`:telemetry` events for state entry and session termination, for
logging or analytics without touching core modules.
- **Mix generators** — scaffold states, actions, responses, decisions, configurators and
exception handlers with `mix ussd.gen.*`.
## Usage
### Creating states
```
mix ussd.gen.state Welcome
```
generates `lib/my_app/ussd/states/welcome.ex`:
```elixir
defmodule MyApp.Ussd.States.Welcome do
use Ussd.State
alias Ussd.Decisions.Fallback
transition Fallback.new(), to: __MODULE__
terminate()
@impl Ussd.State
def render(_context) do
Ussd.Menu.build()
|> Ussd.Menu.line("Welcome")
end
end
```
### Building a menu and routing between states
```elixir
defmodule MyApp.Ussd.States.Welcome do
use Ussd.State, initial: true
alias Ussd.Decisions.Equal
transition Equal.new("1"), to: MyApp.Ussd.States.Airtime
transition Equal.new("2"), to: MyApp.Ussd.States.DataBundle
@impl Ussd.State
def render(_context) do
Ussd.Menu.build()
|> Ussd.Menu.line("Welcome")
|> Ussd.Menu.line("Select an option")
|> Ussd.Menu.listing(["Airtime Topup", "Data Bundle", "TV Subscription", "ECG/GWCL"])
|> Ussd.Menu.line("")
|> Ussd.Menu.text("Powered by Speso")
end
end
```
### Running a request
```elixir
defmodule MyAppWeb.UssdController do
use MyAppWeb, :controller
def index(conn, params) do
context = Ussd.Context.new(params["session_id"], params["phone"], params["input"] || "")
result =
context
|> Ussd.build()
|> Ussd.use_initial_state(MyApp.Ussd.States.Welcome)
|> Ussd.use_response(&Ussd.Responses.AfricasTalking.respond/3)
|> Ussd.run()
text(conn, result)
end
end
```
Nothing here is Phoenix-specific — `Ussd.run/1` returns whatever your `Ussd.Response`
formatter produces, so it works the same from a Plug, a `mix run` script, or a test.
### Configuration
```elixir
# config/config.exs
config :ussd,
namespace: "MyApp.Ussd", # used by mix ussd.gen.* and mix ussd.lint's auto-discovery
cache: Ussd.Cache.ETS, # or your own Ussd.Cache implementation
cache_sweep_interval: :timer.minutes(1),
session_ttl: 300, # seconds a mid-flow session survives with no further input
encryption_key: System.fetch_env!("USSD_ENCRYPTION_KEY"), # required for set_encrypted/get_encrypted
gettext_backend: MyApp.Gettext # required for Ussd.Menu.trans/4
```
### Testing
```elixir
import Ussd.Test
test "buying airtime" do
build(MyApp.Ussd.States.Welcome)
|> start()
|> assert_see("Welcome")
|> input("1")
|> assert_see("Enter amount")
|> input("5")
|> assert_terminated()
end
```
## License
MIT. Please see the [license file](LICENSE) for more information.