AkurAI Build
Menu

Public repository / 0b62821911ed33eb32e0decbe69888046cbea9fe

AkurAI-Platform / frontend/subscriptions.js

Latest change d11e160f26a6dfe754f18d1060107de9d6b6b898 - feat: complete managed platform control plane (AKURAI-PLATFORM-10, AKURAI-PLATFORM-11, AKURAI-PLATFORM-12, AKURAI-PLATFORM-13, AKURAI-PLATFORM-14, AKURAI-PLATFORM-16, AKURAI-PLATFORM-18) by Ólafur Búi Ólafsson

const section = document.querySelector("[data-subscriptions]");

if (section) {
  const productsRoot = document.getElementById("subscription-products");
  const selectionRoot = document.getElementById("subscription-selection");
  const emptyState = document.getElementById("subscription-empty");
  const count = document.getElementById("subscription-count");
  const total = document.getElementById("subscription-total");
  const period = document.getElementById("subscription-period");
  const continueButton = document.getElementById("subscription-continue");
  const paymentNote = document.getElementById("subscription-payment-note");
  const modal = document.getElementById("subscription-modal");
  const dialog = modal?.querySelector("[role=dialog]");
  const form = document.getElementById("subscription-form");
  const consent = document.getElementById("subscription-consent");
  const submit = document.getElementById("subscription-submit");
  const toasts = document.getElementById("payment-toasts");
  const intervalButtons = [...section.querySelectorAll("[data-interval]")];
  const selected = new Set();
  const locale = document.documentElement.lang || "en";
  const secureNote = paymentNote.textContent;
  let catalog = null;
  let interval = "monthly";
  let paymentsEnabled = false;
  let returnFocus = null;

  const copy = {
    countSuffix: section.dataset.countSuffix,
    perMonth: section.dataset.perMonth,
    perYear: section.dataset.perYear,
    unavailable: section.dataset.unavailable,
    loadError: section.dataset.loadError,
    validationError: section.dataset.validationError,
    consentError: section.dataset.consentError,
    checkoutError: section.dataset.checkoutError,
    checkoutUnavailable: section.dataset.checkoutUnavailable,
    submitting: section.dataset.submitting,
  };

  const formatMoney = (cents) => new Intl.NumberFormat(locale, {
    style: "currency",
    currency: catalog?.currency || "EUR",
  }).format(cents / 100);

  function unitCents() {
    return interval === "annual" ? catalog.annualUnitCents : catalog.monthlyUnitCents;
  }

  function orderedSelection() {
    return catalog.products.filter((product) => selected.has(product.id));
  }

  function renderSummary() {
    const products = orderedSelection();
    const unit = unitCents();
    selectionRoot.replaceChildren(...products.map((product) => {
      const item = document.createElement("li");
      const name = document.createElement("span");
      const price = document.createElement("span");
      name.textContent = product.name;
      price.textContent = formatMoney(unit);
      item.append(name, price);
      return item;
    }));
    emptyState.hidden = products.length > 0;
    count.textContent = `${products.length} ${copy.countSuffix}`;
    total.textContent = formatMoney(unit * products.length);
    period.textContent = interval === "annual" ? copy.perYear : copy.perMonth;
    continueButton.disabled = products.length === 0 || !paymentsEnabled;
    paymentNote.textContent = paymentsEnabled ? secureNote : copy.checkoutUnavailable;
  }

  function renderProducts() {
    const unit = unitCents();
    productsRoot.replaceChildren(...catalog.products.filter((p) => p.purchasable).map((product) => {
      const button = document.createElement("button");
      const name = document.createElement("strong");
      const detail = document.createElement("small");
      const check = document.createElement("span");
      const available = product.available !== false;
      button.type = "button";
      button.className = `subscription-product${selected.has(product.id) ? " is-selected" : ""}`;
      button.dataset.productId = product.id;
      button.disabled = !available;
      button.setAttribute("aria-pressed", String(selected.has(product.id)));
      name.textContent = product.name;
      detail.textContent = available
        ? `${formatMoney(unit)} ${interval === "annual" ? copy.perYear : copy.perMonth}${product.managedWorkspacePlan ? ` · ${product.managedWorkspacePlan.includedSeats} seats · ${product.managedWorkspacePlan.storageGb} GB · ${product.managedWorkspacePlan.supportLevel} support` : ""}`
        : copy.unavailable;
      check.className = "product-check";
      check.textContent = "✓";
      check.setAttribute("aria-hidden", "true");
      button.append(name, detail, check);
      if (available) {
        button.addEventListener("click", () => {
          if (selected.has(product.id)) selected.delete(product.id);
          else selected.add(product.id);
          renderProducts();
          renderSummary();
        });
      }
      return button;
    }));
  }

  function showToast(message) {
    const toast = document.createElement("div");
    toast.className = "payment-toast";
    toast.textContent = message;
    toasts.append(toast);
    setTimeout(() => toast.remove(), 6000);
  }

  function openModal() {
    returnFocus = document.activeElement;
    modal.hidden = false;
    document.body.classList.add("subscription-modal-open");
    form.elements.name.focus();
  }

  function closeModal() {
    modal.hidden = true;
    document.body.classList.remove("subscription-modal-open");
    if (returnFocus instanceof HTMLElement) returnFocus.focus();
  }

  intervalButtons.forEach((button) => {
    button.addEventListener("click", () => {
      interval = button.dataset.interval;
      intervalButtons.forEach((candidate) => {
        const active = candidate === button;
        candidate.classList.toggle("is-active", active);
        candidate.setAttribute("aria-pressed", String(active));
      });
      renderProducts();
      renderSummary();
    });
  });

  continueButton.addEventListener("click", openModal);
  modal.querySelectorAll("[data-modal-close]").forEach((button) => button.addEventListener("click", closeModal));
  consent.addEventListener("click", () => consent.setAttribute("aria-checked", String(consent.getAttribute("aria-checked") !== "true")));

  modal.addEventListener("keydown", (event) => {
    if (event.key === "Escape") {
      closeModal();
      return;
    }
    if (event.key !== "Tab") return;
    const focusable = [...dialog.querySelectorAll("button:not(:disabled), input:not(:disabled)")];
    if (focusable.length === 0) return;
    const first = focusable[0];
    const last = focusable[focusable.length - 1];
    if (event.shiftKey && document.activeElement === first) {
      event.preventDefault();
      last.focus();
    } else if (!event.shiftKey && document.activeElement === last) {
      event.preventDefault();
      first.focus();
    }
  });

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    if (!paymentsEnabled) {
      showToast(copy.checkoutUnavailable);
      return;
    }
    const name = form.elements.name.value.trim();
    const email = form.elements.email.value.trim();
    const company = form.elements.company.value.trim();
    if (!name || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      showToast(copy.validationError);
      (!name ? form.elements.name : form.elements.email).focus();
      return;
    }
    if (consent.getAttribute("aria-checked") !== "true") {
      showToast(copy.consentError);
      consent.focus();
      return;
    }

    const originalLabel = submit.textContent;
    submit.disabled = true;
    submit.textContent = copy.submitting;
    try {
      const response = await fetch("/api/payments/checkout", {
        method: "POST",
        headers: { "content-type": "application/json", accept: "application/json" },
        body: JSON.stringify({
          productIds: orderedSelection().map((product) => product.id),
          interval,
          customer: { name, email, ...(company ? { company } : {}) },
        }),
      });
      if (!response.ok) throw new Error("checkout_failed");
      const checkout = await response.json();
      if (!checkout.orderId || !checkout.redirectUrl || !checkout.manageToken) throw new Error("invalid_checkout_response");
      sessionStorage.setItem(`akurai_manage_${checkout.orderId}`, checkout.manageToken);
      window.location.assign(checkout.redirectUrl);
    } catch {
      showToast(copy.checkoutError);
      submit.disabled = false;
      submit.textContent = originalLabel;
    }
  });

  Promise.all([
    fetch("/api/payments/catalog", { headers: { accept: "application/json" } })
      .then((response) => {
        if (!response.ok) throw new Error("catalog_failed");
        return response.json();
      }),
    fetch("/api/payments/health", { headers: { accept: "application/json" } })
      .then((response) => {
        if (!response.ok) throw new Error("health_failed");
        return response.json();
      })
      .catch(() => ({ paymentsEnabled: false })),
  ])
    .then(([value, health]) => {
      catalog = value;
      paymentsEnabled = health.paymentsEnabled === true;
      renderProducts();
      renderSummary();
    })
    .catch(() => {
      productsRoot.replaceChildren();
      const message = document.createElement("p");
      message.className = "subscription-loading";
      message.textContent = copy.loadError;
      productsRoot.append(message);
      continueButton.disabled = true;
    });
}