tarakan/lib/tarakan_web/live/billing_live.ex
Maxfield Luke af6077b9c3
All checks were successful
CI and deploy / Test (push) Successful in 4m52s
CI and deploy / Deploy production (push) Successful in 23s
Initial commit on Forgejo
Fresh repository history for elektrine/tarakan hosted at
https://git.elektrine.com/elektrine/tarakan.
2026-07-29 04:43:40 -04:00

159 lines
5.5 KiB
Elixir

defmodule TarakanWeb.BillingLive do
@moduledoc """
Account billing page: managed-disclosure retainer status, period end, and
the Stripe customer portal exit. No feature gates hang off it.
"""
use TarakanWeb, :live_view
alias Tarakan.Billing
@impl true
def mount(_params, _session, socket) do
account = socket.assigns.current_scope.account
{:ok,
socket
|> assign(:page_title, "Billing")
|> assign(:subscription, Billing.subscription(account))}
end
@impl true
def handle_params(params, _uri, socket) do
socket =
case params["checkout"] do
"success" -> put_flash(socket, :info, "Subscription started - welcome aboard.")
"cancelled" -> put_flash(socket, :info, "Checkout cancelled; nothing was charged.")
_other -> socket
end
{:noreply, socket}
end
@impl true
def handle_event("subscribe", %{"plan" => plan}, socket) do
case Billing.ensure_checkout(socket.assigns.current_scope, plan) do
{:ok, checkout_url} ->
{:noreply, redirect(socket, external: checkout_url)}
{:error, _reason} ->
{:noreply, put_flash(socket, :error, "Checkout could not be started. Try again shortly.")}
end
end
def handle_event("manage", _params, socket) do
case Billing.portal_session(socket.assigns.current_scope.account) do
{:ok, portal_url} ->
{:noreply, redirect(socket, external: portal_url)}
{:error, _reason} ->
{:noreply,
put_flash(socket, :error, "The billing portal is unavailable for this account.")}
end
end
defp status_label(nil), do: "No subscription"
defp status_label(%{status: "active"}), do: "Active"
defp status_label(%{status: "past_due"}), do: "Past due - update your payment method"
defp status_label(%{status: "canceled"}), do: "Canceled"
defp status_label(%{status: "incomplete"}), do: "Incomplete"
defp plan_label(%{plan: "enterprise"}), do: "Managed disclosure"
# Retired tier; kept so historical rows still render.
defp plan_label(%{plan: "team"}), do: "Team (retired)"
defp plan_label(_other), do: "None"
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope} current_path={assigns[:current_path]}>
<Layouts.page width={:focused} class="space-y-8">
<header class="border-b-2 border-strong pb-6">
<h1 class="font-display text-3xl font-medium uppercase leading-none tracking-[0.02em] text-ink">
Billing
</h1>
<p class="mt-3 text-sm leading-6 text-ink-muted">
Managed disclosure retainer. Everything else on Tarakan is free.
</p>
</header>
<section id="billing-current" class="border-2 border-strong p-5">
<dl class="grid gap-4 sm:grid-cols-3">
<div>
<dt class="font-mono text-[11px] uppercase tracking-[0.12em] text-ink-faint">Plan</dt>
<dd id="billing-plan" class="mt-1 font-display text-lg uppercase text-ink">
{plan_label(@subscription)}
</dd>
</div>
<div>
<dt class="font-mono text-[11px] uppercase tracking-[0.12em] text-ink-faint">
Status
</dt>
<dd id="billing-status" class="mt-1 font-display text-lg uppercase text-ink">
{status_label(@subscription)}
</dd>
</div>
<div>
<dt class="font-mono text-[11px] uppercase tracking-[0.12em] text-ink-faint">
Current period ends
</dt>
<dd id="billing-period-end" class="mt-1 font-mono text-sm text-ink">
<%= if @subscription && @subscription.current_period_end do %>
{Calendar.strftime(@subscription.current_period_end, "%Y-%m-%d %H:%M UTC")}
<% else %>
-
<% end %>
</dd>
</div>
</dl>
<div
:if={@subscription && @subscription.status == "past_due"}
class="mt-4 border border-signal p-3 text-sm text-ink-muted"
>
Your last payment failed. Update your payment method from the billing
portal to keep the retainer active.
</div>
<div
:if={@subscription && @subscription.status == "canceled"}
class="mt-4 bg-panel p-3 text-sm text-ink-muted"
>
This retainer is canceled. Your account keeps full access to the record,
alerts, and the API.
</div>
<div class="mt-5 flex flex-wrap gap-3">
<.button
:if={@subscription && @subscription.stripe_customer_id}
id="billing-manage-button"
type="button"
phx-click="manage"
size="sm"
>
Manage subscription
</.button>
<.button
:if={is_nil(@subscription) || @subscription.status in ["canceled", "incomplete"]}
id="billing-subscribe-enterprise"
type="button"
variant="primary"
size="sm"
phx-click="subscribe"
phx-value-plan="enterprise"
>
Start managed disclosure
</.button>
</div>
</section>
<p class="text-sm leading-6 text-ink-faint">
See <.link navigate={~p"/pricing"} class="text-ink-muted underline hover:text-ink">
pricing
</.link>. Payments are processed by Stripe.
</p>
</Layouts.page>
</Layouts.app>
"""
end
end