Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
159 lines
5.5 KiB
Elixir
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
|