lib/openagents_web/live/forum_tips_live.ex

436d37facc96 · 6 KB

defmodule OpenAgentsWeb.ForumTipsLive do
  @moduledoc """
  Your tip destination and what arrived at it.

  The page records where you want tips to go and lists the settlements you can
  check in your own wallet. The forum holds nothing here, so there is no
  balance to withdraw — only payment hashes to verify.
  """
  use OpenAgentsWeb, :live_view

  alias OpenAgents.Forum.Tips

  def mount(_params, _session, socket) do
    user = socket.assigns.current_user

    {:ok,
     socket
     |> assign(:current_scope, socket.assigns[:current_scope])
     |> assign(:destination, Tips.active_destination(user.id))
     |> assign(:export, Tips.withdrawal_export(user.id))
     |> assign(:form, destination_form())}
  end

  def handle_event("save_destination", %{"destination" => params}, socket) do
    user = socket.assigns.current_user

    attrs = %{
      user_id: user.id,
      kind: params["kind"],
      destination: params["destination"],
      label: params["label"]
    }

    case Tips.register_destination(attrs) do
      {:ok, destination} ->
        {:noreply,
         socket
         |> assign(:destination, destination)
         |> assign(:export, Tips.withdrawal_export(user.id))
         |> assign(:form, destination_form())
         |> put_flash(:info, "Destination saved. Tips go straight to your wallet.")}

      {:error, changeset} ->
        {:noreply, assign(socket, :form, to_form(changeset, as: :destination))}
    end
  end

  def handle_event("toggle_accepting", _params, socket) do
    case socket.assigns.destination do
      nil ->
        {:noreply, put_flash(socket, :error, "Add a destination first")}

      destination ->
        {:ok, updated} = Tips.set_accepting_tips(destination, not destination.accepting_tips)

        {:noreply,
         socket
         |> assign(:destination, updated)
         |> put_flash(
           :info,
           if(updated.accepting_tips, do: "Tips are on", else: "Tips are off")
         )}
    end
  end

  def handle_event("retire_destination", _params, socket) do
    case socket.assigns.destination do
      nil ->
        {:noreply, socket}

      destination ->
        {:ok, _retired} = Tips.retire_destination(destination)

        {:noreply,
         socket
         |> assign(:destination, nil)
         |> assign(:export, Tips.withdrawal_export(socket.assigns.current_user.id))
         |> put_flash(:info, "Destination retired")}
    end
  end

  defp destination_form do
    to_form(%{"kind" => "bolt12", "destination" => "", "label" => ""}, as: :destination)
  end

  defp kind_options do
    [
      {"Bolt 12 offer", "bolt12"},
      {"Lightning address or LNURL", "lnurl"},
      {"On-chain address", "onchain"}
    ]
  end

  def render(assigns) do
    ~H"""
    <Layouts.app
      flash={@flash}
      sidebar_sections={assigns[:sidebar_sections]}
      current_scope={@current_scope}
    >
      <div class="flex items-center gap-2 mb-4">
        <.link navigate={~p"/forum"} class="text-sm text-muted-foreground hover:text-foreground">
          Forum
        </.link>
        <span class="text-muted-foreground">/</span>
        <h1 class="text-2xl font-bold">Tips</h1>
      </div>

      <section class="card !mx-0 !mt-0 mb-6">
        <header class="mb-2">
          <h2 class="text-lg font-semibold">Your destination</h2>
          <p class="text-sm text-muted-foreground">
            Tips are paid to a wallet you control. OpenAgents stores where to send sats,
            never a key or a seed, so it can neither hold nor spend them.
          </p>
        </header>

        <%= if @destination do %>
          <dl class="grid grid-cols-2 gap-2 text-sm mb-3" id="tip-destination">
            <dt class="text-muted-foreground">Kind</dt>
            <dd>{@destination.kind}</dd>
            <dt class="text-muted-foreground">Fingerprint</dt>
            <dd class="font-mono">{@destination.fingerprint}</dd>
            <dt class="text-muted-foreground">Accepting tips</dt>
            <dd>{if @destination.accepting_tips, do: "yes", else: "no"}</dd>
          </dl>
          <div class="flex gap-2">
            <.button variant={:secondary} phx-click="toggle_accepting" id="toggle-accepting">
              {if @destination.accepting_tips, do: "Stop accepting tips", else: "Accept tips"}
            </.button>
            <.button variant={:ghost} tone={:danger} phx-click="retire_destination">
              Retire destination
            </.button>
          </div>
        <% end %>

        <.form
          for={@form}
          id="tip-destination-form"
          phx-submit="save_destination"
          class="mt-4 space-y-3"
        >
          <.input field={@form[:kind]} type="select" label="Kind" options={kind_options()} required />
          <.input
            field={@form[:destination]}
            label="Destination"
            placeholder="lno1… , you@example.com, or bc1…"
            required
          />
          <.input field={@form[:label]} label="Label" placeholder="Phone wallet" />
          <footer class="flex justify-end">
            <.button type="submit" variant={:primary}>
              {if @destination, do: "Replace destination", else: "Save destination"}
            </.button>
          </footer>
        </.form>
      </section>

      <section class="card !mx-0">
        <header class="mb-2">
          <h2 class="text-lg font-semibold">Settlements</h2>
          <p class="text-sm text-muted-foreground">
            {@export.received_sats} sats received, {@export.refunded_sats} sats refunded.
            Check each payment hash in your own wallet.
          </p>
        </header>

        <%= if @export.settlements == [] do %>
          <p class="text-sm text-muted-foreground">No tips yet.</p>
        <% else %>
          <table class="table" id="tip-settlements">
            <thead>
              <tr>
                <th>Sats</th>
                <th>State</th>
                <th>Payment hash</th>
                <th>Settled</th>
              </tr>
            </thead>
            <tbody>
              <tr :for={settlement <- @export.settlements}>
                <td>{settlement.amount_sats}</td>
                <td>{settlement.state}</td>
                <td class="font-mono text-xs">{settlement.payment_hash}</td>
                <td>
                  {settlement.settled_at &&
                    Calendar.strftime(settlement.settled_at, "%b %d, %Y %H:%M")}
                </td>
              </tr>
            </tbody>
          </table>
        <% end %>
      </section>
    </Layouts.app>
    """
  end
end