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
endOr 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:forbiddenpath. 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.
Summary
Functions
@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()}