# ai-builder/2.7.10/assets/js/credits.js

AI Builder – Generate pages, blocks, images &amp; translate with AI, version 2.7.10. 1,196 lines.

- Page: https://pluginprobe.com/plugins/ai-builder/2.7.10/code/assets/js/credits.js
- Raw: https://pluginprobe.com/plugins/ai-builder/2.7.10/raw/assets/js/credits.js
- Modified: 2026-04-22T13:11:52+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/ai-builder/2.7.10/code/assets/js/credits.js#L10-L20`.

```javascript
let userData = null;
let hasSubscription = false;
let currentPlan = null;

// --- Top-up configuration (must mirror backend values) ---
const TOPUP_PRICE_PER_CREDIT_USD = 0.012;
const TOPUP_MIN_AMOUNT_USD = 5;
const TOPUP_MAX_AMOUNT_USD = 1200;

// --- Billing period toggle ('monthly' | 'yearly') ---
let currentBilling = "monthly";

// Libellés d'affichage des plans (display uniquement, les identifiants
// côté back-end restent 'essential', 'premium', 'creator', + variantes
// '_year'). On remappe ici pour l'UI de la page Credits.
const PLAN_DISPLAY_LABELS = {
  basic: "Basic",
  essential: "Starter",
  premium: "Pro",
  creator: "Agency",
};

function getPlanDisplayLabel(planId) {
  if (!planId) return "";
  const base = String(planId).replace(/_year$/, "");
  if (PLAN_DISPLAY_LABELS[base]) return PLAN_DISPLAY_LABELS[base];
  // Fallback : capitaliser la valeur brute
  return base.charAt(0).toUpperCase() + base.slice(1);
}

// Charger les informations utilisateur au chargement
document.addEventListener("DOMContentLoaded", function () {
  // Vérifier les paramètres de succès dans l'URL
  checkSuccessParameters();

  // Attacher les événements aux boutons de plan (une seule fois au chargement)
  attachPlanButtonEvents();

  // Initialiser le switch mensuel / annuel
  initBillingToggle();

  // Initialiser la section top-up (input montant personnalisé)
  initTopupSection();

  // Charger les informations utilisateur
  loadUserInfo();
});

// Fonction pour vérifier les paramètres de succès dans l'URL
function checkSuccessParameters() {
  const urlParams = new URLSearchParams(window.location.search);
  const type = urlParams.get("type");
  const sessionId = urlParams.get("session_id");

  if (type && sessionId) {
    if (type === "credits_success" || type === "topup_success") {
      showMessage(
        "Credit purchase completed successfully! Your credits will be updated shortly.",
        "success"
      );
      // Recharger les données après un délai pour s'assurer que les crédits sont mis à jour
      setTimeout(() => {
        loadUserInfo();
      }, 3000);
      // Nettoyer l'URL après affichage du message
      setTimeout(() => {
        const newUrl =
          window.location.pathname +
          window.location.search
            .replace(/[?&]type=[^&]*&session_id=[^&]*/, "")
            .replace(/^&/, "?");
        window.history.replaceState({}, document.title, newUrl);
      }, 5000);
    } else if (type === "subscription_success") {
      showMessage(
        "Subscription activated successfully! Your subscription is now active.",
        "success"
      );
      // Recharger les données après un délai pour s'assurer que l'abonnement est mis à jour
      setTimeout(() => {
        loadUserInfo();
      }, 3000);
      // Nettoyer l'URL après affichage du message
      setTimeout(() => {
        const newUrl =
          window.location.pathname +
          window.location.search
            .replace(/[?&]type=[^&]*&session_id=[^&]*/, "")
            .replace(/^&/, "?");
        window.history.replaceState({}, document.title, newUrl);
      }, 5000);
    }
  }
}

// Fonction pour charger les informations utilisateur
async function loadUserInfo() {
  try {
    const tokenResponse = await fetch(ajaxurl, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: "action=aibui_get_token&nonce=" + aiBuilderVars.nonce,
    });

    const tokenData = await tokenResponse.json();

    if (!tokenData.success || !tokenData.data.token) {
      showMessage("Authentication failed", "error");
      return;
    }

    const jwtToken = tokenData.data.token;

    const response = await fetch(window.config.apiUrl + "/user/profile", {
      method: "GET",
      headers: {
        Authorization: `Bearer ${jwtToken}`,
        "Content-Type": "application/json",
      },
    });

    if (response.ok) {
      userData = await response.json();
      displayUserInfo(userData?.user);
      // updateSubscriptionButton() n'est plus nécessaire car updatePlansDisplay() gère tout
    } else {
      showMessage("Failed to load user information", "error");
    }
  } catch (error) {
    console.error("Error loading user info:", error);
    showMessage("Network error while loading user information", "error");
  }
}

// Afficher les informations utilisateur
function displayUserInfo(data) {
  // Afficher les crédits
  const creditsDisplay = document.getElementById("current-credits");
  const creditsBreakdown = data.aiCredits || {
    onAccountCreation: 0,
    monthlySubscription: 0,
    paid: 0,
  };

  const totalCredits =
    creditsBreakdown.onAccountCreation +
    creditsBreakdown.monthlySubscription +
    creditsBreakdown.paid;

  if (creditsDisplay) {
    creditsDisplay.innerHTML = `
        <span class="credits-number">${totalCredits}</span>
        <span class="credits-label">credits</span>
    `;
  }

  // Afficher le plan
  const planDisplay = document.getElementById("current-plan");
  if (planDisplay) {
    const plan = data.plan || "basic";
    let resiliateText = "";
    if (data.resiliateAt) {
      const resiliateDate = new Date(data.resiliateAt);
      const now = new Date();
      const isEnded = resiliateDate < now.setHours(0, 0, 0, 0);
      if (isEnded) {
        resiliateText = `<span class="resiliate-badge">Subscription has ended on <strong>${resiliateDate.toLocaleDateString()}</strong></span>`;
      } else {
        resiliateText = `<span class="resiliate-badge">Subscription will end on <strong>${resiliateDate.toLocaleDateString()}</strong></span>`;
      }
    }
    planDisplay.innerHTML = `
        <span class="plan-badge ${plan}">${getPlanDisplayLabel(plan)}</span>
        ${resiliateText}
    `;
  }

  // Afficher le message dans la carte Subscription (section management)
  const subscriptionCard = document.querySelector(
    ".ai-subscription-actions .ai-action-content"
  );
  if (subscriptionCard) {
    let resiliateMsg = subscriptionCard.querySelector(".resiliate-badge");
    if (data.resiliateAt) {
      if (!resiliateMsg) {
        resiliateMsg = document.createElement("div");
        resiliateMsg.className = "resiliate-badge";
        resiliateMsg.innerHTML = `Subscription will end on <strong>${new Date(
          data.resiliateAt
        ).toLocaleDateString()}</strong>`;
        subscriptionCard.appendChild(resiliateMsg);
      } else {
        resiliateMsg.innerHTML = `Subscription will end on <strong>${new Date(
          data.resiliateAt
        ).toLocaleDateString()}</strong>`;
      }
    } else if (resiliateMsg) {
      resiliateMsg.remove();
    }
  }

  // Vérifier si l'utilisateur a un abonnement
  hasSubscription = data.plan && data.plan !== "basic";
  currentPlan = data.plan || "basic";
  window.hasResiliate = !!data.resiliateAt;

  // Si l'utilisateur est sur un plan annuel, pré-sélectionner le switch "Yearly"
  // pour que son plan s'affiche bien comme "Current Plan".
  if (isCurrentPlanYearly() && currentBilling !== "yearly") {
    currentBilling = "yearly";
    applyBillingPeriod();
  } else {
    // applyBillingPeriod() appelle déjà updatePlansDisplay()
    applyBillingPeriod();
  }

  // Mettre à jour l'affichage de la section Buy Credits
  updateCreditPurchaseSection();
}

// Attacher les événements aux boutons de plan
function attachPlanButtonEvents() {
  // Utiliser la délégation d'événements pour éviter les problèmes de duplication
  const plansContainer = document.getElementById("subscription-plans-container");
  if (plansContainer) {
    // Retirer l'ancien listener s'il existe
    plansContainer.removeEventListener("click", handlePlanButtonClick);
    // Ajouter le nouveau listener
    plansContainer.addEventListener("click", handlePlanButtonClick);
  }

  // Bouton d'annulation
  const cancelBtn = document.getElementById("subscription-btn");
  if (cancelBtn) {
    cancelBtn.onclick = showCancelModal;
  }
}

// Gestionnaire de clic pour les boutons de plan (délégation d'événements)
function handlePlanButtonClick(event) {
  const button = event.target.closest(".ai-plan-btn");
  if (!button || button.disabled) return;

  const subscriptionType = button.getAttribute("data-subscription-type");
  if (subscriptionType) {
    // Vérifier dynamiquement l'état de l'abonnement
    if (hasSubscription && !window.hasResiliate) {
      // Abonnement actif -> on demande confirmation avant de changer de plan
      openSwitchSubscriptionModal(subscriptionType, button);
    } else {
      // Utilisateur sans abonnement ou abonnement annulé -> nouveau checkout
      handleSubscription(subscriptionType, button);
    }
  }
}

// --- Switch subscription confirmation modal ---

// Contexte mémorisé pendant que la modale est ouverte
let pendingSwitch = { subscriptionType: null, buttonElement: null };

function openSwitchSubscriptionModal(subscriptionType, buttonElement) {
  if (!subscriptionType) return;

  // Sécurité : si l'utilisateur tente de "switcher" vers son propre plan,
  // on ne montre pas la modale et on délègue à handleChangeSubscription
  // qui affichera un message "already on this plan".
  if (subscriptionType === currentPlan) {
    handleChangeSubscription(subscriptionType, buttonElement);
    return;
  }

  pendingSwitch = { subscriptionType, buttonElement: buttonElement || null };

  const modal = document.getElementById("switch-subscription-modal");
  const label = document.getElementById("switch-target-plan-label");
  if (label) {
    const periodSuffix =
      /_year$/.test(subscriptionType) ? " (yearly)" : " (monthly)";
    label.textContent = `${getPlanDisplayLabel(subscriptionType)}${periodSuffix}`;
  }

  // Réinitialiser le bouton de confirmation
  const confirmBtn = document.getElementById("switch-subscription-confirm-btn");
  if (confirmBtn) {
    confirmBtn.disabled = false;
    const loading = confirmBtn.querySelector(".ai-loading");
    const btnText = confirmBtn.querySelector(".btn-text");
    if (loading) loading.style.display = "none";
    if (btnText) btnText.textContent = "Confirm switch";
  }

  if (modal) modal.style.display = "flex";
}

function closeSwitchSubscriptionModal() {
  const modal = document.getElementById("switch-subscription-modal");
  if (modal) modal.style.display = "none";
  pendingSwitch = { subscriptionType: null, buttonElement: null };
}

async function confirmSwitchSubscription() {
  const { subscriptionType, buttonElement } = pendingSwitch;
  if (!subscriptionType) {
    closeSwitchSubscriptionModal();
    return;
  }

  const confirmBtn = document.getElementById("switch-subscription-confirm-btn");
  const loading = confirmBtn ? confirmBtn.querySelector(".ai-loading") : null;
  const btnText = confirmBtn ? confirmBtn.querySelector(".btn-text") : null;
  if (confirmBtn) confirmBtn.disabled = true;
  if (loading) loading.style.display = "inline-block";
  if (btnText) btnText.textContent = "Switching...";

  try {
    await handleChangeSubscription(subscriptionType, buttonElement);
  } finally {
    closeSwitchSubscriptionModal();
  }
}

// Helpers pour gérer les plans annuels ( _year )
function getCurrentPlanBase() {
  if (!currentPlan) return null;
  return currentPlan.replace(/_year$/, "");
}

function isCurrentPlanYearly() {
  return typeof currentPlan === "string" && /_year$/.test(currentPlan);
}

// Mettre à jour l'affichage des plans
function updatePlansDisplay() {
  const planWrappers = document.querySelectorAll(".ai-plan-card-wrapper");
  const cancelCard = document.getElementById("cancel-subscription-card");

  if (!planWrappers || planWrappers.length === 0) {
    return; // Les plans ne sont pas encore chargés
  }

  const planBase = getCurrentPlanBase();
  const yearlyCurrent = isCurrentPlanYearly();

  planWrappers.forEach((wrapper) => {
    const planType = wrapper.getAttribute("data-plan");
    const button = wrapper.querySelector(".ai-plan-btn");
    if (!button) return;

    const btnText = button.querySelector(".btn-text");
    if (!btnText) return;

    // Réinitialiser l'état
    wrapper.setAttribute("data-current-plan", "false");
    button.classList.remove("current-plan");
    button.disabled = false;

    // Libellé d'affichage du plan pour les CTAs ("Starter", "Pro", "Agency").
    const planLabel = getPlanDisplayLabel(planType);
    // Suffixe selon la période sélectionnée
    const periodSuffix = currentBilling === "yearly" ? " (yearly)" : "";

    // Est-ce que ce plan, dans sa période actuellement affichée, correspond
    // exactement à l'abonnement en cours ?
    const isExactCurrent =
      planBase === planType &&
      yearlyCurrent === (currentBilling === "yearly");

    if (hasSubscription && !window.hasResiliate) {
      if (isExactCurrent) {
        // Plan + période = abonnement en cours
        wrapper.setAttribute("data-current-plan", "true");
        button.classList.add("current-plan");
        btnText.textContent = "Current Plan";
        button.disabled = true;
      } else {
        // Autre plan / autre période -> option de changement
        btnText.textContent = `Switch to ${planLabel}${periodSuffix}`;
        button.disabled = false;
      }
    } else {
      // Pas d'abonnement actif (ou abonnement résilié)
      btnText.textContent = `Choose ${planLabel}${periodSuffix}`;
      button.disabled = false;
    }
  });

  // Afficher/masquer la carte d'annulation
  if (cancelCard) {
    if (hasSubscription && !window.hasResiliate) {
      cancelCard.style.display = "block";
    } else {
      cancelCard.style.display = "none";
    }
  }
}

// Mettre à jour le bouton d'abonnement (fonction conservée pour compatibilité mais non utilisée)
// L'affichage des plans est maintenant géré par updatePlansDisplay()
// Cette fonction peut être utilisée pour le bouton d'annulation si nécessaire
function updateSubscriptionButton() {
  const btn = document.getElementById("subscription-btn");
  if (!btn) return; // Sécurité : le bouton n'existe pas

  // Reconstruit le HTML interne du bouton à chaque fois
  btn.innerHTML = `
        <span class="ai-loading" style="display: none;"></span>
        <span class="btn-text"></span>
    `;
  const btnText = btn.querySelector(".btn-text");
  const loading = btn.querySelector(".ai-loading");
  if (!btnText || !loading) return; // Sécurité : structure inattendue

  if (hasSubscription && window.hasResiliate) {
    btnText.textContent = "Reactivate Subscription";
    btn.className = "ai-primary-btn ai-reactivate-btn";
    btn.onclick = handleReactivateSubscription;
  } else if (hasSubscription) {
    btnText.textContent = "Cancel Subscription";
    btn.className = "ai-cancel-subscription-btn";
    btn.style.opacity = "0.7";
    btn.style.fontWeight = "200";
    btn.onclick = showCancelModal;
  } else {
    // Ce cas ne devrait plus se produire car nous avons maintenant les boutons de plan individuels
    btnText.textContent = "Get Subscription";
    btn.className = "ai-primary-btn";
    btn.onclick = null; // Ne pas assigner handleSubscription sans paramètre
  }
}

// Fonction pour réactiver l'abonnement
async function handleReactivateSubscription() {
  setLoading("subscription-btn", true);
  try {
    const tokenResponse = await fetch(ajaxurl, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: "action=aibui_get_token&nonce=" + aiBuilderVars.nonce,
    });
    const tokenData = await tokenResponse.json();
    const jwtToken = tokenData.data.token;
    const response = await fetch(
      window.config.apiUrl + "/payments/resume-subscription",
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${jwtToken}`,
          "Content-Type": "application/json",
        },
      }
    );
    const data = await response.json();
    if (response.ok) {
      showMessage("Subscription reactivated successfully!", "success");
      setTimeout(() => {
        loadUserInfo();
      }, 1000);
    } else {
      showMessage(data.message || "Failed to reactivate subscription", "error");
    }
  } catch (error) {
    console.error("Error reactivating subscription:", error);
    showMessage("Network error while reactivating subscription", "error");
  } finally {
    setLoading("subscription-btn", false);
  }
}

// Mettre à jour l'affichage de la section Buy Credits (top-up)
function updateCreditPurchaseSection() {
  const creditCard = document.getElementById("credit-purchase-card");
  const buyCreditsBtn = document.getElementById("buy-credits-btn");
  const lockOverlay = document.getElementById("topup-lock-overlay");
  if (!creditCard || !buyCreditsBtn) return;

  const amountInput = document.getElementById("topup-amount");
  const presets = document.querySelectorAll(".ai-topup-preset");

  const locked = !hasSubscription;

  creditCard.setAttribute("data-locked", locked ? "true" : "false");
  if (lockOverlay) {
    lockOverlay.hidden = !locked;
  }

  // Griser / désactiver tous les contrôles quand pas d'abonnement
  if (amountInput) amountInput.disabled = locked;
  presets.forEach((btn) => {
    btn.disabled = locked;
  });

  buyCreditsBtn.disabled = locked;
  buyCreditsBtn.onclick = locked ? null : handleCreditPurchase;

  // Rafraîchir l'affichage après changement d'état
  refreshTopupDisplay();
}

// --- Top-up custom amount helpers ---

function computeCreditsFromAmount(amountDollars) {
  if (!Number.isFinite(amountDollars) || amountDollars <= 0) return 0;
  const amountCents = Math.round(amountDollars * 100);
  return Math.floor(amountCents / 100 / TOPUP_PRICE_PER_CREDIT_USD);
}

function clampTopupAmount(value) {
  const n = Number(value);
  if (!Number.isFinite(n)) return TOPUP_MIN_AMOUNT_USD;
  return Math.min(
    TOPUP_MAX_AMOUNT_USD,
    Math.max(TOPUP_MIN_AMOUNT_USD, Math.round(n))
  );
}

function getTopupRawAmount() {
  const amountInput = document.getElementById("topup-amount");
  if (!amountInput) return TOPUP_MIN_AMOUNT_USD;
  const raw = amountInput.value;
  if (raw === "" || raw === null) return NaN;
  return Number(raw);
}

function refreshTopupDisplay() {
  const amountInput = document.getElementById("topup-amount");
  const payEl = document.getElementById("topup-pay-amount");
  const creditsEl = document.getElementById("topup-credits-amount");
  const errorEl = document.getElementById("topup-error");
  const wrapper = document.querySelector(".ai-topup-amount-wrapper");
  const buyBtn = document.getElementById("buy-credits-btn");
  const btnText = buyBtn ? buyBtn.querySelector(".btn-text") : null;
  const presets = document.querySelectorAll(".ai-topup-preset");

  if (!amountInput) return;

  const raw = getTopupRawAmount();
  const hasValue = !Number.isNaN(raw);
  const isBelow = hasValue && raw < TOPUP_MIN_AMOUNT_USD;
  const isAbove = hasValue && raw > TOPUP_MAX_AMOUNT_USD;
  const isInvalid = !hasValue || isBelow || isAbove;

  // Valeur "effective" utilisée pour l'aperçu
  const effective = hasValue
    ? Math.min(TOPUP_MAX_AMOUNT_USD, Math.max(TOPUP_MIN_AMOUNT_USD, raw))
    : TOPUP_MIN_AMOUNT_USD;

  const credits = computeCreditsFromAmount(effective);

  if (payEl) {
    payEl.textContent = `$${effective.toFixed(2)}`;
  }
  if (creditsEl) {
    creditsEl.textContent = `${credits.toLocaleString("en-US")} credits`;
  }

  if (wrapper) {
    wrapper.classList.toggle("has-error", isInvalid && hasValue);
  }

  if (errorEl) {
    if (!hasValue) {
      errorEl.textContent = "";
    } else if (isBelow) {
      errorEl.textContent = `Minimum amount is $${TOPUP_MIN_AMOUNT_USD}.`;
    } else if (isAbove) {
      errorEl.textContent = `Maximum amount is $${TOPUP_MAX_AMOUNT_USD.toLocaleString(
        "en-US"
      )}.`;
    } else {
      errorEl.textContent = "";
    }
  }

  // Mise à jour visuelle du preset actif
  presets.forEach((btn) => {
    const presetAmount = Number(btn.getAttribute("data-amount"));
    btn.classList.toggle(
      "is-active",
      hasValue && !isInvalid && presetAmount === raw
    );
  });

  // Bouton d'achat
  if (buyBtn) {
    const locked = !hasSubscription;
    const canBuy = !locked && !isInvalid && hasValue && credits > 0;
    buyBtn.disabled = !canBuy;
    if (btnText && !buyBtn.classList.contains("is-loading")) {
      if (locked) {
        btnText.textContent = "Purchase credits";
      } else if (isInvalid || !hasValue) {
        btnText.textContent = "Enter a valid amount";
      } else {
        btnText.textContent = `Purchase ${credits.toLocaleString(
          "en-US"
        )} credits for $${effective.toFixed(2)}`;
      }
    }
  }
}

function initTopupSection() {
  const amountInput = document.getElementById("topup-amount");
  const presets = document.querySelectorAll(".ai-topup-preset");

  if (!amountInput) return;

  amountInput.addEventListener("input", function () {
    refreshTopupDisplay();
  });

  amountInput.addEventListener("blur", function () {
    const raw = getTopupRawAmount();
    if (Number.isNaN(raw)) {
      amountInput.value = String(TOPUP_MIN_AMOUNT_USD);
    } else {
      amountInput.value = String(clampTopupAmount(raw));
    }
    refreshTopupDisplay();
  });

  presets.forEach((btn) => {
    btn.addEventListener("click", function () {
      if (btn.disabled) return;
      const presetAmount = Number(btn.getAttribute("data-amount"));
      if (!Number.isFinite(presetAmount)) return;
      amountInput.value = String(clampTopupAmount(presetAmount));
      refreshTopupDisplay();
    });
  });

  refreshTopupDisplay();
}

// --- Billing period (monthly / yearly) ---

function formatMonthlyEquivalent(yearlyPrice) {
  // Montant mensuel équivalent (ex: 90 / 12 = 7.5 -> "$7.50")
  const value = yearlyPrice / 12;
  const rounded = Math.round(value * 100) / 100;
  // Affiche sans décimales inutiles: 7.5 -> "$7.50", 15.83 -> "$15.83"
  const hasCents = Math.round(rounded * 100) % 100 !== 0;
  return `$${rounded.toFixed(hasCents ? 2 : 0)}`;
}

function formatPerCreditYearly(perCredit) {
  // Ex: 0.01 -> "$0.010", 0.00791666 -> "$0.00792", 0.00680555 -> "$0.00681".
  // On arrondit à 5 décimales puis on retire les zéros de fin en conservant
  // au minimum 3 décimales pour un affichage cohérent.
  if (!Number.isFinite(perCredit) || perCredit <= 0) return "";
  let str = (Math.round(perCredit * 100000) / 100000).toFixed(5);
  str = str.replace(/(\.\d{3}\d*?)0+$/, "$1");
  return `$${str}`;
}

function applyBillingPeriod() {
  const planWrappers = document.querySelectorAll(".ai-plan-card-wrapper");
  const isYearly = currentBilling === "yearly";

  // État visuel du switch
  document.querySelectorAll(".ai-billing-option").forEach((btn) => {
    const active = btn.getAttribute("data-billing") === currentBilling;
    btn.classList.toggle("is-active", active);
    btn.setAttribute("aria-selected", active ? "true" : "false");
  });

  planWrappers.forEach((wrapper) => {
    const monthlyPrice = Number(wrapper.getAttribute("data-monthly-price"));
    const yearlyPrice = Number(wrapper.getAttribute("data-yearly-price"));
    const monthlyCredits = Number(
      wrapper.getAttribute("data-monthly-credits")
    );
    const monthlyType = wrapper.getAttribute("data-monthly-type");
    const yearlyType = wrapper.getAttribute("data-yearly-type");

    const amountEl = wrapper.querySelector(".ai-price-amount");
    const oldEl = wrapper.querySelector(".ai-price-old");
    const billedEl = wrapper.querySelector(".ai-price-billed");
    const creditEl = wrapper.querySelector(".ai-price-credit");
    const button = wrapper.querySelector(".ai-plan-btn");

    // Sauvegarde du libellé mensuel d'origine (hardcodé dans le HTML) pour
    // pouvoir le restaurer quand on revient en monthly, sans avoir à le
    // recalculer (les valeurs monthly sont des arrondis marketing).
    if (creditEl && !creditEl.dataset.monthlyText) {
      creditEl.dataset.monthlyText = creditEl.textContent.trim();
    }

    if (isYearly && Number.isFinite(yearlyPrice)) {
      // Prix effectif mensuel à partir du tarif annuel
      if (amountEl) amountEl.textContent = formatMonthlyEquivalent(yearlyPrice);
      if (oldEl) {
        oldEl.textContent = `$${monthlyPrice}`;
        oldEl.hidden = false;
      }
      if (billedEl) {
        billedEl.textContent = `Billed annually at $${yearlyPrice}/year`;
        billedEl.hidden = false;
      }
      // Prix par crédit recalculé sur la base annuelle :
      //   yearlyPrice / (monthlyCredits * 12)
      if (creditEl && Number.isFinite(monthlyCredits) && monthlyCredits > 0) {
        const perCredit = yearlyPrice / (monthlyCredits * 12);
        const formatted = formatPerCreditYearly(perCredit);
        if (formatted) {
          creditEl.textContent = `${formatted} per credit`;
        }
      }
      if (button && yearlyType) {
        button.setAttribute("data-subscription-type", yearlyType);
      }
    } else {
      if (amountEl) amountEl.textContent = `$${monthlyPrice}`;
      if (oldEl) {
        oldEl.hidden = true;
      }
      if (billedEl) {
        billedEl.hidden = true;
      }
      // Restaurer le libellé mensuel d'origine (ex: "$0.012 per credit")
      if (creditEl && creditEl.dataset.monthlyText) {
        creditEl.textContent = creditEl.dataset.monthlyText;
      }
      if (button && monthlyType) {
        button.setAttribute("data-subscription-type", monthlyType);
      }
    }
  });

  // Recalculer les libellés "Current Plan" / "Switch to ..." selon la période
  updatePlansDisplay();
}

function initBillingToggle() {
  const toggle = document.querySelector(".ai-billing-toggle");
  if (!toggle) return;

  toggle.addEventListener("click", function (event) {
    const btn = event.target.closest(".ai-billing-option");
    if (!btn) return;
    const billing = btn.getAttribute("data-billing");
    if (!billing || billing === currentBilling) return;
    currentBilling = billing;
    applyBillingPeriod();
  });

  // État initial
  applyBillingPeriod();
}

// Gérer l'abonnement (nouveau checkout)
async function handleSubscription(subscriptionType, buttonElement) {
  if (!subscriptionType) {
    showMessage("Please select a subscription plan", "error");
    return;
  }

  // Désactiver tous les boutons pendant le traitement
  const planButtons = document.querySelectorAll(".ai-plan-btn");
  planButtons.forEach((btn) => {
    btn.disabled = true;
  });

  // Afficher le loading sur le bouton cliqué
  if (buttonElement) {
    const loading = buttonElement.querySelector(".ai-loading");
    const btnText = buttonElement.querySelector(".btn-text");
    if (loading) loading.style.display = "inline-block";
    if (btnText) {
      const originalText = btnText.textContent;
      btnText.textContent = "Processing...";
    }
  }

  try {
    const tokenResponse = await fetch(ajaxurl, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: "action=aibui_get_token&nonce=" + aiBuilderVars.nonce,
    });

    const tokenData = await tokenResponse.json();
    if (!tokenData.success || !tokenData.data.token) {
      showMessage("Authentication failed", "error");
      return;
    }

    const jwtToken = tokenData.data.token;

    const response = await fetch(
      window.config.apiUrl + "/payments/create-checkout-session-abo",
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${jwtToken}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          subscriptionType: subscriptionType,
          urlFrom: window.location.href,
        }),
      }
    );

    const data = await response.json();

    if (response.ok && data.url) {
      window.location.href = data.url;
    } else {
      showMessage(
        data.message || "Failed to create subscription checkout",
        "error"
      );
      // Réactiver les boutons en cas d'erreur
      planButtons.forEach((btn) => {
        btn.disabled = false;
      });
      if (buttonElement) {
        const loading = buttonElement.querySelector(".ai-loading");
        const btnText = buttonElement.querySelector(".btn-text");
        if (loading) loading.style.display = "none";
        if (btnText) {
          const subType = buttonElement.getAttribute("data-subscription-type");
          const periodSuffix =
            currentBilling === "yearly" ? " (yearly)" : "";
          btnText.textContent = subType
            ? `Choose ${getPlanDisplayLabel(subType)}${periodSuffix}`
            : "Choose Plan";
        }
      }
    }
  } catch (error) {
    console.error("Error creating subscription:", error);
    showMessage("Network error while creating subscription", "error");
    // Réactiver les boutons en cas d'erreur
    const planButtons = document.querySelectorAll(".ai-plan-btn");
    planButtons.forEach((btn) => {
      btn.disabled = false;
    });
    if (buttonElement) {
      const loading = buttonElement.querySelector(".ai-loading");
      const btnText = buttonElement.querySelector(".btn-text");
      if (loading) loading.style.display = "none";
      if (btnText) {
        const subType = buttonElement.getAttribute("data-subscription-type");
        const periodSuffix =
          currentBilling === "yearly" ? " (yearly)" : "";
        btnText.textContent = subType
          ? `Choose ${getPlanDisplayLabel(subType)}${periodSuffix}`
          : "Choose Plan";
      }
    }
  }
}

// Gérer le changement d'abonnement
async function handleChangeSubscription(subscriptionType, buttonElement) {
  if (!subscriptionType) {
    showMessage("Please select a subscription plan", "error");
    return;
  }

  // Vérifier si c'est le même plan
  if (subscriptionType === currentPlan) {
    showMessage("You are already on this plan", "info");
    return;
  }

  // Désactiver tous les boutons pendant le traitement
  const planButtons = document.querySelectorAll(".ai-plan-btn");
  planButtons.forEach((btn) => {
    btn.disabled = true;
  });

  // Afficher le loading sur le bouton cliqué
  if (buttonElement) {
    const loading = buttonElement.querySelector(".ai-loading");
    const btnText = buttonElement.querySelector(".btn-text");
    if (loading) loading.style.display = "inline-block";
    if (btnText) {
      const originalText = btnText.textContent;
      btnText.textContent = "Switching...";
    }
  }

  try {
    const tokenResponse = await fetch(ajaxurl, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: "action=aibui_get_token&nonce=" + aiBuilderVars.nonce,
    });

    const tokenData = await tokenResponse.json();
    if (!tokenData.success || !tokenData.data.token) {
      showMessage("Authentication failed", "error");
      return;
    }

    const jwtToken = tokenData.data.token;

    const response = await fetch(
      window.config.apiUrl + "/payments/change-subscription",
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${jwtToken}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          subscriptionType: subscriptionType,
        }),
      }
    );

    const data = await response.json();

    if (response.ok) {
      showMessage("Subscription changed successfully!", "success");
      // Recharger les informations utilisateur
      setTimeout(() => {
        loadUserInfo();
      }, 1000);
    } else {
      showMessage(
        data.message || "Failed to change subscription",
        "error"
      );
      // Réactiver les boutons en cas d'erreur
      planButtons.forEach((btn) => {
        btn.disabled = false;
      });
      if (buttonElement) {
        const loading = buttonElement.querySelector(".ai-loading");
        const btnText = buttonElement.querySelector(".btn-text");
        if (loading) loading.style.display = "none";
        if (btnText) {
          const periodSuffix =
            /_year$/.test(subscriptionType) ? " (yearly)" : "";
          btnText.textContent = `Switch to ${getPlanDisplayLabel(
            subscriptionType
          )}${periodSuffix}`;
        }
      }
    }
  } catch (error) {
    console.error("Error changing subscription:", error);
    showMessage("Network error while changing subscription", "error");
    // Réactiver les boutons en cas d'erreur
    const planButtons = document.querySelectorAll(".ai-plan-btn");
    planButtons.forEach((btn) => {
      btn.disabled = false;
    });
    if (buttonElement) {
      const loading = buttonElement.querySelector(".ai-loading");
      const btnText = buttonElement.querySelector(".btn-text");
      if (loading) loading.style.display = "none";
      if (btnText) {
        const periodSuffix =
          /_year$/.test(subscriptionType) ? " (yearly)" : "";
        btnText.textContent = `Switch to ${getPlanDisplayLabel(
          subscriptionType
        )}${periodSuffix}`;
      }
    }
  }
}

// Acheter des crédits (top-up avec montant personnalisé)
async function handleCreditPurchase() {
  if (!hasSubscription) {
    showMessage(
      "An active subscription is required to buy top-up credits.",
      "error"
    );
    return;
  }

  const buyBtn = document.getElementById("buy-credits-btn");
  const btnText = buyBtn ? buyBtn.querySelector(".btn-text") : null;
  const loading = buyBtn ? buyBtn.querySelector(".ai-loading") : null;

  // Validation du montant côté front (miroir du back)
  const raw = getTopupRawAmount();
  if (
    Number.isNaN(raw) ||
    raw < TOPUP_MIN_AMOUNT_USD ||
    raw > TOPUP_MAX_AMOUNT_USD
  ) {
    showMessage(
      `Please enter an amount between $${TOPUP_MIN_AMOUNT_USD} and $${TOPUP_MAX_AMOUNT_USD.toLocaleString(
        "en-US"
      )}.`,
      "error"
    );
    refreshTopupDisplay();
    return;
  }

  const amountDollars = clampTopupAmount(raw);
  const credits = computeCreditsFromAmount(amountDollars);
  if (credits <= 0) {
    showMessage("Amount too low to grant any credit.", "error");
    return;
  }

  // Loading state
  if (buyBtn) {
    buyBtn.classList.add("is-loading");
    buyBtn.disabled = true;
  }
  if (loading) loading.style.display = "inline-block";
  const previousBtnText = btnText ? btnText.textContent : "";
  if (btnText) btnText.textContent = "Redirecting to checkout...";

  try {
    const tokenResponse = await fetch(ajaxurl, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: "action=aibui_get_token&nonce=" + aiBuilderVars.nonce,
    });

    const tokenData = await tokenResponse.json();
    if (!tokenData.success || !tokenData.data.token) {
      showMessage("Authentication failed", "error");
      return;
    }
    const jwtToken = tokenData.data.token;

    const urlFrom = window.location.href;
    const response = await fetch(
      window.config.apiUrl + "/payments/create-checkout-session-topup",
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${jwtToken}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          urlFrom,
          amount: amountDollars,
        }),
      }
    );

    const data = await response.json();

    if (response.ok && data.url) {
      window.location.href = data.url;
      return;
    }

    showMessage(data.message || "Failed to create credit checkout", "error");
  } catch (error) {
    console.error("Error creating credit checkout:", error);
    showMessage("Network error while creating credit checkout", "error");
  } finally {
    if (buyBtn) {
      buyBtn.classList.remove("is-loading");
      buyBtn.disabled = false;
    }
    if (loading) loading.style.display = "none";
    if (btnText && previousBtnText) btnText.textContent = previousBtnText;
    refreshTopupDisplay();
  }
}

// Accéder au compte Stripe
async function handleStripeAccount() {
  try {
    const tokenResponse = await fetch(ajaxurl, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: "action=aibui_get_token&nonce=" + aiBuilderVars.nonce,
    });

    const tokenData = await tokenResponse.json();
    const jwtToken = tokenData.data.token;

    const urlFrom = window.location.href;
    const response = await fetch(
      window.config.apiUrl + "/payments/stripe-customer-portal",
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${jwtToken}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          urlFrom,
        }),
      }
    );

    const data = await response.json();

    if (response.ok && data.url) {
      window.open(data.url, "_blank");
    } else {
      showMessage(data.message || "Failed to access Stripe account", "error");
    }
  } catch (error) {
    console.error("Error accessing Stripe account:", error);
    showMessage("Network error while accessing Stripe account", "error");
  }
}

// Afficher la modal de confirmation
function showCancelModal() {
  document.getElementById("confirmation-modal").style.display = "flex";
}

// Fermer la modal
function closeModal() {
  document.getElementById("confirmation-modal").style.display = "none";
}

// Confirmer l'annulation de l'abonnement
async function confirmCancelSubscription() {
  const btn = document.querySelector(".ai-danger-btn");
  const loading = btn.querySelector(".ai-loading");
  const text = btn.textContent;

  loading.style.display = "inline-block";
  btn.textContent = "";
  btn.appendChild(loading);
  btn.appendChild(document.createTextNode(text));
  btn.disabled = true;

  try {
    const tokenResponse = await fetch(ajaxurl, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: "action=aibui_get_token&nonce=" + aiBuilderVars.nonce,
    });

    const tokenData = await tokenResponse.json();
    const jwtToken = tokenData.data.token;

    const response = await fetch(
      window.config.apiUrl + "/payments/cancel-subscription",
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${jwtToken}`,
          "Content-Type": "application/json",
        },
      }
    );

    const data = await response.json();

    if (response.ok) {
      showMessage("Subscription cancelled successfully", "success");
      closeModal();
      // Forcer l'état local pour l'UI
      window.hasResiliate = true;
      hasSubscription = false; // Mettre à jour l'état local
      // Recharger les informations utilisateur (qui va mettre à jour l'affichage via updatePlansDisplay)
      setTimeout(() => {
        loadUserInfo();
      }, 1000);
    } else {
      showMessage(data.message || "Failed to cancel subscription", "error");
    }
  } catch (error) {
    console.error("Error cancelling subscription:", error);
    showMessage("Network error while cancelling subscription", "error");
  } finally {
    loading.style.display = "none";
    btn.textContent = text;
    btn.disabled = false;
  }
}

// Fonctions utilitaires
function showMessage(message, type = "info") {
  const container = document.getElementById("message-container");
  container.innerHTML = `<div class="ai-message ${type}">${message}</div>`;
  container.scrollIntoView({ behavior: "smooth" });
}

function setLoading(buttonId, isLoading) {
  const button = document.getElementById(buttonId);
  const loading = button.querySelector(".ai-loading");
  const text = button.querySelector(".btn-text");

  if (isLoading) {
    loading.style.display = "inline-block";
    button.disabled = true;
  } else {
    loading.style.display = "none";
    button.disabled = false;
  }
}

```
