Menu
Public repository / e1dca3a75db23be353f8e6445134eba8af6e0797
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;
});
}