# `Accrue.Live.Entitlements`
[🔗](https://github.com/szTheory/accrue/blob/accrue-v1.5.1/lib/accrue/live/entitlements.ex#L23)

`on_mount/4` entitlement guard for host LiveViews (ENT-07).

Gate a LiveView (or a whole `live_session`) on a feature or plan by adding
this hook to the `on_mount:` list:

    live_session :paid, on_mount: [
      {MyAppWeb.UserAuth, :ensure_authenticated},
      {Accrue.Live.Entitlements, {:require_feature, :reports}}
    ] do
      live "/reports", ReportsLive
    end

Or gate on an active plan:

    {Accrue.Live.Entitlements, {:require_plan, :pro}}

On allow it returns `{:cont, socket}` (the resolved billable is stashed once
on the socket under `:accrue_billable`). On deny it returns `{:halt, socket}`
where the socket has been redirected (and, for the `:forbidden` path, given a
generic flash error).

All decision logic — billable resolution, the fail-closed gate call, and the
tiered `on_deny` resolution — lives in `Accrue.Entitlements.Guard`. This
module only surface-translates the Guard's deny form into LiveView terms.

## Ordering (REQUIRED — read this)

The host's authentication `on_mount` hook MUST run BEFORE this guard in the
`on_mount:` list (D-20). This guard gates *entitlement only*, never
authentication: it resolves the billable from server-side socket assigns
(the default scope/user probe, or your configured `billable` fn). If it runs
before auth has populated those assigns, the billable resolves to `nil` and
EVERY user is denied (fail-closed, but spuriously). List your auth hook
first.

## Deny destination (avoid redirect loops)

The deny destination — the per-guard/`config` `on_deny` `{:redirect, path}`
target, or the `config :accrue, :entitlements, deny_path:` fallback (default
`"/"`) — MUST live OUTSIDE the gated `live_session` (D-13). A deny target
that is itself behind this guard produces an infinite redirect / a LiveView
that never mounts.

## Deny surface-translation (D-21)

  * `{:redirect, path}` → `redirect(socket, to: path)`.
  * `:forbidden` → `put_flash(:error, …)` + `redirect(to: deny_path())`.
  * `{status, body}` → degrades to the `:forbidden` path. A raw HTTP status +
    body is meaningless on a socket, so the one irreducible plug-vs-socket
    asymmetry collapses to the flash+redirect deny. Hosts never see this
    plumbing.

The deny flash is intentionally generic ("You don't have access to this
page.") and NEVER names the required feature or plan (D-10).

## Conditional compilation

This module is wrapped in `Code.ensure_loaded?(Phoenix.LiveView)` and is the
ONLY always-shipped core file allowed to reference the LiveView socket
runtime (D-03/D-04). Because `:phoenix_live_view` is a hard core dep the
branch is never actually elided, but the wrapper keeps the surface refs
confined to one auditable location for the Plan 06 merge gate.

# `on_mount`

```elixir
@spec on_mount(
  {:require_feature, atom()} | {:require_plan, atom() | String.t()},
  map(),
  map(),
  Phoenix.LiveView.Socket.t()
) :: {:cont, Phoenix.LiveView.Socket.t()} | {:halt, Phoenix.LiveView.Socket.t()}
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
