Subscriptions

Memberships and recurring subscriptions use the same <u-ticketable-list> component as tickets — set ticketable-type="subscription". Filter by state for an active-only view, or show the full history with colored state badges.

View this guide as Markdown

Active subscriptions

Cards for currently active subscriptions using filter="state=active". ticketable-conditional hides the end date when a subscription runs indefinitely, and unidy-attr wires the manage link from the data.

  • ticketable-type="subscription" — one component for tickets and memberships
  • State filtering — filter="state=active" server-side
  • Conditional markup — end date only when present
<u-signed-in>
  <!-- Same list component as tickets — just a different ticketable-type -->
  <u-ticketable-list
    ticketable-type="subscription"
    filter="state=active"
    limit="3"
    container-class="grid gap-4">
    <template>
      <div class="rounded-lg border border-border bg-white p-5 shadow-sm">
        <div class="flex items-start justify-between gap-4">
          <div>
            <h3 class="text-lg font-bold">
              <ticketable-value name="title"></ticketable-value>
            </h3>
            <p class="mt-1 text-sm text-text-light">
              Since <ticketable-value name="starts_at" date-format="dd.MM.yyyy"></ticketable-value>
              <ticketable-conditional when="ends_at">
                · renews/ends <ticketable-value name="ends_at" date-format="dd.MM.yyyy"
                ></ticketable-value>
              </ticketable-conditional>
            </p>
            <p class="mt-2 font-semibold text-primary">
              <ticketable-value name="price" default="—"></ticketable-value>
            </p>
          </div>
          <span class="state-badge">
            <ticketable-value name="state"></ticketable-value>
          </span>
        </div>
        <a
          unidy-attr
          unidy-attr-href="{{button_cta_url}}"
          class="btn btn-primary !min-h-0 !px-3 !py-1.5 mt-4 text-sm">
          Manage
        </a>
      </div>
    </template>

    <!-- shown when there are no active subscriptions -->
    <p slot="empty" class="py-6 text-center text-sm text-text-light">No active subscriptions.</p>
  </u-ticketable-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 subscriptions.
  </p>
</u-signed-in>
Live demo

No active subscriptions.

Sign in on the Auth page to see your subscriptions.

Subscription history

The unfiltered list shows every subscription with its lifecycle state. A data-state attribute stamped from the item data drives the badge colors in plain CSS — active is green, cancelled red.

  • Full lifecycle — active, cancelled and expired together
  • State badges — unidy-attr-data-state + CSS attribute selectors
  • Pagination — same u-pagination components as tickets
<u-signed-in>
  <!-- No state filter: active, cancelled and expired subscriptions together -->
  <u-ticketable-list
    ticketable-type="subscription"
    target="#subscription-history-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">Subscription</th>
            <th class="px-4 py-3">Period</th>
            <th class="px-4 py-3">Price</th>
            <th class="px-4 py-3">State</th>
          </tr>
        </thead>
        <tbody id="subscription-history-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">
          <ticketable-value name="title"></ticketable-value>
        </td>
        <td class="whitespace-nowrap px-4 py-3 text-sm text-text-light">
          <ticketable-value name="starts_at" date-format="dd.MM.yyyy"></ticketable-value>
          – <ticketable-value name="ends_at" date-format="dd.MM.yyyy" default="ongoing"
          ></ticketable-value>
        </td>
        <td class="whitespace-nowrap px-4 py-3 font-semibold text-primary">
          <ticketable-value name="price" default="—"></ticketable-value>
        </td>
        <td class="whitespace-nowrap px-4 py-3">
          <!-- ticketable-state on the row lets CSS color the badge per state -->
          <span class="state-badge" unidy-attr unidy-attr-data-state="{{state}}">
            <ticketable-value name="state"></ticketable-value>
          </span>
        </td>
      </tr>
    </template>

    <!-- shown when the user has no subscriptions -->
    <p slot="empty" class="py-6 text-center text-sm text-text-light">No subscriptions yet.</p>
  </u-ticketable-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 subscription
    history.
  </p>
</u-signed-in>
Live demo
Subscription Period Price State

No subscriptions yet.

Sign in on the Auth page to see your subscription history.

Create test subscriptions through the Admin API

An authenticated demo-only control creates a subscription for the currently signed-in user through a protected server route and can remove the item again.

  • Authenticated user only — no arbitrary user id is accepted
  • Server-side credentials — Admin API access stays on the server
  • Create and clean up — add a test subscription and delete it again
<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">
        Create a subscription through the Admin API for the signed-in user. Admin credentials stay
        server-side.
      </p>
    </div>
    <div class="flex flex-wrap items-center gap-3">
      <button id="create-demo-subscription" class="btn btn-primary !min-h-0 !px-4 !py-2 text-sm"
        >+ Create Demo Subscription</button
      >
      <button
        id="delete-demo-subscription"
        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
      >
      <span id="demo-subscription-status" class="text-xs text-text-muted" role="status"></span>
    </div>
    <p class="text-xs text-text-muted">
      Reload after creating or deleting to refresh the SDK list above.
    </p>
  </div>
</u-signed-in>

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

<script type="module">
  import { Auth } from "@unidy.io/sdk";

  const createButton = document.getElementById("create-demo-subscription");
  const deleteButton = document.getElementById("delete-demo-subscription");
  const status = document.getElementById("demo-subscription-status");
  let lastId = null;

  function setStatus(message, failed = false) {
    status.textContent = message;
    status.className = `text-xs ${failed ? "text-red-500" : "text-green-600"}`;
  }

  async function authenticatedFetch(url, options = {}) {
    const auth = await Auth.getInstance();
    const token = await auth.getToken();
    if (typeof token !== "string") throw new Error("Please sign in again.");
    return fetch(url, {
      ...options,
      headers: { ...options.headers, Authorization: `Bearer ${token}` },
    });
  }

  createButton?.addEventListener("click", async () => {
    createButton.disabled = true;
    setStatus("Creating…");
    try {
      const response = await authenticatedFetch("/api/demo/subscriptions", { method: "POST" });
      const data = await response.json();
      if (!response.ok) throw new Error(data.error || "Create failed");
      lastId = data.id;
      deleteButton.disabled = !lastId;
      setStatus(`Created${data.title ? `: ${data.title}` : ""}.`);
    } catch (error) {
      setStatus(error instanceof Error ? error.message : "Create failed", true);
    } finally {
      createButton.disabled = false;
    }
  });

  deleteButton?.addEventListener("click", async () => {
    if (!lastId) return;
    deleteButton.disabled = true;
    setStatus("Deleting…");
    try {
      const response = await authenticatedFetch(
        `/api/demo/subscriptions?id=${encodeURIComponent(lastId)}`,
        { method: "DELETE" }
      );
      const data = await response.json();
      if (!response.ok) throw new Error(data.error || "Delete failed");
      lastId = null;
      setStatus("Deleted.");
    } catch (error) {
      setStatus(error instanceof Error ? error.message : "Delete failed", true);
      deleteButton.disabled = false;
    }
  });
</script>
Live demo
Demo Controls

Create a subscription through the Admin API for the signed-in user. Admin credentials stay server-side.

Reload after creating or deleting to refresh the SDK list above.

Sign in to use the Admin API demo controls.