# Using Bandera with Phoenix LiveView
This is a short, practical guide to gating UI and behavior in a Phoenix
LiveView app with Bandera. It assumes Bandera is already installed and
configured; see the [README](README.md) for that.
LiveViews are just processes, so calling Bandera from one is nothing special:
`Bandera.enabled?/2` works in `mount/3`, `handle_event/3`, `handle_info/2`, and
inside `~H` templates. What's worth spelling out is wiring the current user in as
the actor, and testing. Both are covered below.
## 1. Identify the current user
Most flags are evaluated "for" a user. Implement the `Bandera.Actor` protocol so
Bandera can derive a stable id for your user struct, and `Bandera.Group` if you
use group gates (e.g. roles):
```elixir
defimpl Bandera.Actor, for: MyApp.Accounts.User do
def id(user), do: "user:#{user.id}"
end
defimpl Bandera.Group, for: MyApp.Accounts.User do
def in?(user, group_name), do: group_name in user.roles
end
```
The simplest way to have `current_user` available in every LiveView is the
`on_mount` hook generated by `mix phx.gen.auth`:
```elixir
# lib/my_app_web/router.ex
live_session :default, on_mount: [{MyAppWeb.UserAuth, :mount_current_user}] do
live "/dashboard", DashboardLive
end
```
That puts `socket.assigns.current_user` in place before your `mount/3` runs.
## 2. Check a flag in a LiveView
Read the flag in `mount/3`, assign the result, and branch in the template with
`:if`:
```elixir
defmodule MyAppWeb.DashboardLive do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
user = socket.assigns.current_user
{:ok, assign(socket, :new_dashboard?, Bandera.enabled?(:new_dashboard, for: user))}
end
@impl true
def render(assigns) do
~H"""
<.new_dashboard :if={@new_dashboard?} />
<.classic_dashboard :if={!@new_dashboard?} />
"""
end
end
```
Assigning once in `mount/3` (rather than calling `Bandera.enabled?/2` directly in
the template) keeps the check out of the render path and makes the value easy to
override in tests.
A reusable helper keeps controllers and LiveViews consistent:
```elixir
# lib/my_app_web/feature_flags.ex
defmodule MyAppWeb.FeatureFlags do
def assign_flag(socket_or_conn, key, flag, actor) do
Phoenix.Component.assign(socket_or_conn, key, Bandera.enabled?(flag, for: actor))
end
end
```
## 3. Gate events and actions
Don't rely on the UI alone. Re-check the flag where the action actually happens,
so a hidden control can't be triggered by a crafted event:
```elixir
@impl true
def handle_event("publish", _params, socket) do
if Bandera.enabled?(:publishing, for: socket.assigns.current_user) do
{:noreply, do_publish(socket)}
else
{:noreply, put_flash(socket, :error, "That feature isn't available yet.")}
end
end
```
## 4. (Optional) React to flag changes live
`mount/3` reads the flag once. If you want a running LiveView to pick up a flag
change without a reload, re-read the flag on some trigger, for example a
periodic tick, or a `Phoenix.PubSub` broadcast you send when toggling flags:
```elixir
def mount(_params, _session, socket) do
if connected?(socket), do: Phoenix.PubSub.subscribe(MyApp.PubSub, "feature_flags")
{:ok, assign_new_dashboard(socket)}
end
def handle_info(:flags_changed, socket), do: {:noreply, assign_new_dashboard(socket)}
defp assign_new_dashboard(socket) do
assign(socket, :new_dashboard?, Bandera.enabled?(:new_dashboard, for: socket.assigns.current_user))
end
```
For most apps the mount-time read is enough; reach for this only when live
toggling matters.
## 5. Outside LiveView
The same calls work everywhere: controllers, function components, and plugs:
```elixir
# controller
if Bandera.enabled?(:beta_export, for: conn.assigns.current_user) do
# ...
end
```
```heex
<%!-- a function component receiving the flag as an assign --%>
<.banner :if={@promo_enabled?} />
```
## 6. Testing LiveViews
The test layer scopes overrides to the **test process and its descendants**. A
LiveView started by `Phoenix.LiveViewTest` is a descendant (Phoenix propagates
`$callers`), so flags you set in the test are visible inside the LiveView. Tests
stay `async: true`, never touch the database, and clean up automatically.
One-time setup (see the README for details):
```elixir
# config/test.exs
config :bandera, store: Bandera.Store.ProcessScoped
# test/test_helper.exs
Bandera.Test.start()
```
Then in a LiveView test, declare flags with the `@tag feature_flags:` tag or set
them imperatively:
```elixir
defmodule MyAppWeb.DashboardLiveTest do
use MyAppWeb.ConnCase, async: true
use Bandera.Test
import Phoenix.LiveViewTest
setup :register_and_log_in_user
@tag feature_flags: [new_dashboard: true]
test "renders the new dashboard when the flag is on", %{conn: conn} do
{:ok, _live, html} = live(conn, ~p"/dashboard")
assert html =~ "New dashboard"
end
test "falls back to the classic dashboard when off", %{conn: conn} do
{:ok, _live, html} = live(conn, ~p"/dashboard")
assert html =~ "Classic dashboard"
end
test "enables a flag for one actor only", %{conn: conn, user: user} do
enable_flag(:new_dashboard, user)
{:ok, _live, html} = live(conn, ~p"/dashboard")
assert html =~ "New dashboard"
end
end
```
Notes:
- `@tag feature_flags: [new_dashboard: true]` sets a global boolean override, so
it's honored even when the LiveView checks `enabled?(:new_dashboard, for: user)`.
- Use `enable_flag(flag, actor)` / `disable_flag(flag, actor)` to override for a
specific user, useful for testing per-actor or per-group gates.
- `register_and_log_in_user` is the `mix phx.gen.auth` helper; it puts the user
in the session so `current_user` is assigned.