Transactions

Show a user's order and payment history with <u-transaction-list>. It follows the same template pattern as tickets — <transaction-value> placeholders, filter attribute, pagination — over transaction data synced into Unidy from your shop or ERP.

View this guide as Markdown

Transaction history table

A paginated order-history table. transaction-value reads any field from the transaction — reference, totals, dates, payment status — and the financial_status is stamped onto a badge for CSS coloring.

  • <transaction-value> — reference, placed_at, total, financial_status, …
  • target rendering + pagination — identical mechanics to u-ticketable-list
  • Skeleton loading — placeholders while the page loads
<u-signed-in>
  <!-- Order/purchase history. Same template pattern as tickets, with
       transaction-value placeholders and filter/pagination support. -->
  <u-transaction-list
    id="transaction-history"
    target="#transactions-body"
    limit="5"
    skeleton-all-text="true">
    <div class="overflow-x-auto">
      <table class="min-w-full rounded-lg border border-border bg-white">
        <thead
          class="bg-background-light text-left text-xs uppercase tracking-wider text-text-muted">
          <tr>
            <th class="px-4 py-3">Reference</th>
            <th class="px-4 py-3">Placed</th>
            <th class="px-4 py-3">Total</th>
            <th class="px-4 py-3">Payment</th>
          </tr>
        </thead>
        <tbody id="transactions-body" class="divide-y divide-border"></tbody>
      </table>
    </div>

    <div class="mt-4 flex items-center gap-2">
      <u-pagination-button
        direction="prev"
        class-name="px-3 py-2 rounded-lg border border-border bg-white text-sm cursor-pointer hover:bg-background-light disabled:opacity-50 disabled:cursor-not-allowed">
      </u-pagination-button>
      <u-pagination-page class-name="px-3 py-2 text-sm"></u-pagination-page>
      <u-pagination-button
        direction="next"
        class-name="px-3 py-2 rounded-lg border border-border bg-white text-sm cursor-pointer hover:bg-background-light disabled:opacity-50 disabled:cursor-not-allowed">
      </u-pagination-button>
    </div>

    <template>
      <tr class="transition-colors hover:bg-background-light">
        <td class="whitespace-nowrap px-4 py-3 font-medium">
          <transaction-value name="reference" default="—"></transaction-value>
        </td>
        <td class="whitespace-nowrap px-4 py-3 text-sm text-text-light">
          <transaction-value name="placed_at" date-format="dd.MM.yyyy HH:mm"></transaction-value>
        </td>
        <td class="whitespace-nowrap px-4 py-3 font-semibold text-primary">
          <transaction-value name="total" default="—"></transaction-value>
          <transaction-value name="currency"></transaction-value>
        </td>
        <td class="whitespace-nowrap px-4 py-3">
          <span class="state-badge" unidy-attr unidy-attr-data-state="{{financial_status}}">
            <transaction-value name="financial_status" default="unknown"></transaction-value>
          </span>
        </td>
      </tr>
    </template>

    <!-- shown when the user has no transactions -->
    <p slot="empty" class="py-6 text-center text-sm text-text-light">No transactions yet.</p>
  </u-transaction-list>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    Sign in on the <a href="/auth" class="text-primary underline">Auth page</a> to see your transactions.
  </p>
</u-signed-in>
Live demo
Reference Placed Total Payment

No transactions yet.

Sign in on the Auth page to see your transactions.

Filtering by payment status

Set the filter attribute (e.g. financial_status=paid) and the list re-fetches. Filters support state, financial_status, order_type, source_platform and external_id — combine with ";".

  • Live filter attribute — no custom fetch code
  • Rich filter keys — state, financial_status, order_type, source_platform
<u-signed-in>
  <div class="mb-3 flex items-center justify-end">
    <select
      id="financial-status-filter"
      class="rounded-lg border border-border bg-white px-3 py-2 text-sm">
      <option value="">All payment states</option>
      <option value="paid">Paid</option>
      <option value="pending">Pending</option>
      <option value="refunded">Refunded</option>
    </select>
  </div>

  <u-transaction-list id="filtered-transactions" limit="3" container-class="grid gap-3">
    <template>
      <div class="flex items-center justify-between rounded-lg border border-border bg-white p-4">
        <div>
          <p class="font-medium">
            <transaction-value name="reference" default="(no reference)"></transaction-value>
          </p>
          <p class="text-sm text-text-light">
            <transaction-value name="order_type" default="order"></transaction-value>
            via <transaction-value name="source_platform" default="unknown"></transaction-value>
          </p>
        </div>
        <div class="text-right">
          <p class="font-semibold text-primary">
            <transaction-value name="total" default="—"></transaction-value>
          </p>
          <span class="state-badge" unidy-attr unidy-attr-data-state="{{financial_status}}">
            <transaction-value name="financial_status" default="unknown"></transaction-value>
          </span>
        </div>
      </div>
    </template>

    <!-- shown when no transactions match the filter -->
    <p slot="empty" class="py-6 text-center text-sm text-text-light">
      No transactions match this filter.
    </p>
  </u-transaction-list>

  <script is:inline>
    document.getElementById("financial-status-filter").addEventListener("change", (event) => {
      // filter uses key=value pairs joined by ";"
      document
        .getElementById("filtered-transactions")
        .setAttribute("filter", event.target.value ? `financial_status=${event.target.value}` : "");
    });
  </script>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    Sign in on the <a href="/auth" class="text-primary underline">Auth page</a> to filter transactions.
  </p>
</u-signed-in>
Live demo

No transactions match this filter.

Sign in on the Auth page to filter transactions.

Admin API: Create & Delete

Demo-only section: creates and deletes transactions via the Admin API. Transactions are created globally (no user-scoped endpoint in the API) — after creation, reload the page to see them appear in your history list.

  • client_credentials flow — Admin API token fetched server-side
  • POST /api/demo/transactions — creates a test fan-shop order (paid)
  • DELETE /api/demo/transactions?id=… — removes it
<u-signed-in>
  <div class="rounded-lg border border-amber-200 bg-amber-50/40 p-5 space-y-4">
    <div class="flex flex-wrap items-center gap-2">
      <span class="text-xs font-semibold uppercase tracking-wider text-amber-700 bg-amber-100 border border-amber-200 px-2 py-0.5 rounded-full">Demo Controls</span>
      <p class="text-sm text-text-light">Creates and deletes test transactions via the Admin API (server-side, credentials never exposed).</p>
    </div>

    <div class="flex flex-wrap items-center gap-3">
      <button id="create-tx-btn" class="btn btn-primary !min-h-0 !px-4 !py-2 text-sm">
        + Create Demo Transaction
      </button>
      <button
        id="delete-tx-btn"
        class="!min-h-0 !px-4 !py-2 text-sm rounded-lg border border-red-200 text-red-600 bg-white hover:bg-red-50 transition-colors cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
        disabled>
        Delete Last
      </button>
      <button
        id="delete-all-tx-btn"
        class="!min-h-0 !px-4 !py-2 text-sm rounded-lg border border-red-300 text-red-700 bg-red-50 hover:bg-red-100 transition-colors cursor-pointer">
        Delete All
      </button>
      <span id="tx-demo-status" class="text-xs text-text-muted"></span>
    </div>
    <p class="text-xs text-text-muted">After creating, reload the page to see the transaction appear in the list above.</p>
  </div>
</u-signed-in>

<u-signed-in not>
  <p class="text-sm text-text-light italic">Sign in to use demo controls.</p>
</u-signed-in>

<script type="module">
  import { getUnidyClient, profileState } from "https://cdn.jsdelivr.net/npm/@unidy.io/sdk@1.9.0/dist/sdk/index.esm.js";

  const createBtn = document.getElementById("create-tx-btn");
  const deleteBtn = document.getElementById("delete-tx-btn");
  const deleteAllBtn = document.getElementById("delete-all-tx-btn");
  const statusEl = document.getElementById("tx-demo-status");
  let lastId = null;

  function setStatus(msg, error = false) {
    if (!statusEl) return;
    statusEl.textContent = msg;
    statusEl.className = `text-xs ${error ? "text-red-500" : "text-green-600"}`;
  }

  async function getUserEmail() {
    const [err, profile] = await getUnidyClient().profile.get();
    const email = profile?.email?.value ?? profileState?.profile?.email;
    if (err || !email) {
      const detail = err ? (typeof err === "string" ? err : (err.message || err.code || JSON.stringify(err))) : "no email in profile";
      throw new Error(`Could not get user email: ${detail}`);
    }
    return email;
  }

  createBtn?.addEventListener("click", async () => {
    createBtn.disabled = true;
    setStatus("Creating…");
    try {
      const email = await getUserEmail();

      const res = await fetch("/api/demo/transactions", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email }),
      });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || "Create failed");

      lastId = data.id;
      if (deleteBtn) deleteBtn.disabled = false;
      setStatus(`Created: reference ${data.reference || data.id?.slice(0, 8)}…`);
    } catch (e) {
      setStatus(e.message, true);
    } finally {
      if (createBtn) createBtn.disabled = false;
    }
  });

  deleteBtn?.addEventListener("click", async () => {
    if (!lastId) return;
    deleteBtn.disabled = true;
    setStatus("Deleting…");
    try {
      const res = await fetch(`/api/demo/transactions?id=${lastId}`, { method: "DELETE" });
      if (!res.ok) {
        const data = await res.json().catch(() => ({}));
        throw new Error(data.error || "Delete failed");
      }
      lastId = null;
      setStatus("Deleted. Reload to see the list update.");
    } catch (e) {
      setStatus(e.message, true);
      if (deleteBtn) deleteBtn.disabled = false;
    }
  });

  deleteAllBtn?.addEventListener("click", async () => {
    deleteAllBtn.disabled = true;
    setStatus("Deleting all…");
    try {
      const email = await getUserEmail();
      const res = await fetch(`/api/demo/transactions?all=true&email=${encodeURIComponent(email)}`, { method: "DELETE" });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) throw new Error(data.error || "Delete all failed");
      lastId = null;
      if (deleteBtn) deleteBtn.disabled = true;
      setStatus(`Deleted ${data.deleted ?? "all"} transactions. Reload to update.`);
    } catch (e) {
      setStatus(e.message, true);
    } finally {
      if (deleteAllBtn) deleteAllBtn.disabled = false;
    }
  });
</script>
Live demo
Demo Controls

Creates and deletes test transactions via the Admin API (server-side, credentials never exposed).

After creating, reload the page to see the transaction appear in the list above.

Sign in to use demo controls.