PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / 2.7.10
AI Builder – Generate pages, blocks, images & translate with AI v2.7.10
2.8.0 2.7.10 2.7.9 2.7.8 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 All 123 releases
← All changes | assets/js/credits.js +502 -52 2.1.8 → 2.7.10 View file →
@@ -1,8 +1,34 @@
1 1 let userData = null;
2 2 let hasSubscription = false;
3 3 let currentPlan = null;
4 4
5 +// --- Top-up configuration (must mirror backend values) ---
6 +const TOPUP_PRICE_PER_CREDIT_USD = 0.012;
7 +const TOPUP_MIN_AMOUNT_USD = 5;
8 +const TOPUP_MAX_AMOUNT_USD = 1200;
9 +
10 +// --- Billing period toggle ('monthly' | 'yearly') ---
11 +let currentBilling = "monthly";
12 +
13 +// Libellés d'affichage des plans (display uniquement, les identifiants
14 +// côté back-end restent 'essential', 'premium', 'creator', + variantes
15 +// '_year'). On remappe ici pour l'UI de la page Credits.
16 +const PLAN_DISPLAY_LABELS = {
17 + basic: "Basic",
18 + essential: "Starter",
19 + premium: "Pro",
20 + creator: "Agency",
21 +};
22 +
23 +function getPlanDisplayLabel(planId) {
24 + if (!planId) return "";
25 + const base = String(planId).replace(/_year$/, "");
26 + if (PLAN_DISPLAY_LABELS[base]) return PLAN_DISPLAY_LABELS[base];
27 + // Fallback : capitaliser la valeur brute
28 + return base.charAt(0).toUpperCase() + base.slice(1);
29 +}
30 +
5 31 // Charger les informations utilisateur au chargement
6 32 document.addEventListener("DOMContentLoaded", function () {
7 33 // Vérifier les paramètres de succès dans l'URL
8 34 checkSuccessParameters();
@@ -9,8 +35,14 @@
9 35
10 36 // Attacher les événements aux boutons de plan (une seule fois au chargement)
11 37 attachPlanButtonEvents();
12 38
39 + // Initialiser le switch mensuel / annuel
40 + initBillingToggle();
41 +
42 + // Initialiser la section top-up (input montant personnalisé)
43 + initTopupSection();
44 +
13 45 // Charger les informations utilisateur
14 46 loadUserInfo();
15 47 });
16 48
@@ -20,9 +52,9 @@
20 52 const type = urlParams.get("type");
21 53 const sessionId = urlParams.get("session_id");
22 54
23 55 if (type && sessionId) {
24 - if (type === "credits_success") {
56 + if (type === "credits_success" || type === "topup_success") {
25 57 showMessage(
26 58 "Credit purchase completed successfully! Your credits will be updated shortly.",
27 59 "success"
28 60 );
@@ -137,10 +169,9 @@
137 169 resiliateText = `<span class="resiliate-badge">Subscription will end on <strong>${resiliateDate.toLocaleDateString()}</strong></span>`;
138 170 }
139 171 }
140 172 planDisplay.innerHTML = `
141 - <span class="plan-badge ${plan}">${plan.charAt(0).toUpperCase() + plan.slice(1)
142 - }</span>
173 + <span class="plan-badge ${plan}">${getPlanDisplayLabel(plan)}</span>
143 174 ${resiliateText}
144 175 `;
145 176 }
146 177
@@ -172,13 +203,20 @@
172 203 hasSubscription = data.plan && data.plan !== "basic";
173 204 currentPlan = data.plan || "basic";
174 205 window.hasResiliate = !!data.resiliateAt;
175 206
207 + // Si l'utilisateur est sur un plan annuel, pré-sélectionner le switch "Yearly"
208 + // pour que son plan s'affiche bien comme "Current Plan".
209 + if (isCurrentPlanYearly() && currentBilling !== "yearly") {
210 + currentBilling = "yearly";
211 + applyBillingPeriod();
212 + } else {
213 + // applyBillingPeriod() appelle déjà updatePlansDisplay()
214 + applyBillingPeriod();
215 + }
216 +
176 217 // Mettre à jour l'affichage de la section Buy Credits
177 218 updateCreditPurchaseSection();
178 -
179 - // Mettre à jour l'affichage des plans
180 - updatePlansDisplay();
181 219 }
182 220
183 221 // Attacher les événements aux boutons de plan
184 222 function attachPlanButtonEvents() {
@@ -206,10 +244,10 @@
206 244 const subscriptionType = button.getAttribute("data-subscription-type");
207 245 if (subscriptionType) {
208 246 // Vérifier dynamiquement l'état de l'abonnement
209 247 if (hasSubscription && !window.hasResiliate) {
210 - // Utilisateur avec abonnement actif -> changer de plan
211 - handleChangeSubscription(subscriptionType, button);
248 + // Abonnement actif -> on demande confirmation avant de changer de plan
249 + openSwitchSubscriptionModal(subscriptionType, button);
212 250 } else {
213 251 // Utilisateur sans abonnement ou abonnement annulé -> nouveau checkout
214 252 handleSubscription(subscriptionType, button);
215 253 }
@@ -215,8 +253,84 @@
215 253 }
216 254 }
217 255 }
218 256
257 +// --- Switch subscription confirmation modal ---
258 +
259 +// Contexte mémorisé pendant que la modale est ouverte
260 +let pendingSwitch = { subscriptionType: null, buttonElement: null };
261 +
262 +function openSwitchSubscriptionModal(subscriptionType, buttonElement) {
263 + if (!subscriptionType) return;
264 +
265 + // Sécurité : si l'utilisateur tente de "switcher" vers son propre plan,
266 + // on ne montre pas la modale et on délègue à handleChangeSubscription
267 + // qui affichera un message "already on this plan".
268 + if (subscriptionType === currentPlan) {
269 + handleChangeSubscription(subscriptionType, buttonElement);
270 + return;
271 + }
272 +
273 + pendingSwitch = { subscriptionType, buttonElement: buttonElement || null };
274 +
275 + const modal = document.getElementById("switch-subscription-modal");
276 + const label = document.getElementById("switch-target-plan-label");
277 + if (label) {
278 + const periodSuffix =
279 + /_year$/.test(subscriptionType) ? " (yearly)" : " (monthly)";
280 + label.textContent = `${getPlanDisplayLabel(subscriptionType)}${periodSuffix}`;
281 + }
282 +
283 + // Réinitialiser le bouton de confirmation
284 + const confirmBtn = document.getElementById("switch-subscription-confirm-btn");
285 + if (confirmBtn) {
286 + confirmBtn.disabled = false;
287 + const loading = confirmBtn.querySelector(".ai-loading");
288 + const btnText = confirmBtn.querySelector(".btn-text");
289 + if (loading) loading.style.display = "none";
290 + if (btnText) btnText.textContent = "Confirm switch";
291 + }
292 +
293 + if (modal) modal.style.display = "flex";
294 +}
295 +
296 +function closeSwitchSubscriptionModal() {
297 + const modal = document.getElementById("switch-subscription-modal");
298 + if (modal) modal.style.display = "none";
299 + pendingSwitch = { subscriptionType: null, buttonElement: null };
300 +}
301 +
302 +async function confirmSwitchSubscription() {
303 + const { subscriptionType, buttonElement } = pendingSwitch;
304 + if (!subscriptionType) {
305 + closeSwitchSubscriptionModal();
306 + return;
307 + }
308 +
309 + const confirmBtn = document.getElementById("switch-subscription-confirm-btn");
310 + const loading = confirmBtn ? confirmBtn.querySelector(".ai-loading") : null;
311 + const btnText = confirmBtn ? confirmBtn.querySelector(".btn-text") : null;
312 + if (confirmBtn) confirmBtn.disabled = true;
313 + if (loading) loading.style.display = "inline-block";
314 + if (btnText) btnText.textContent = "Switching...";
315 +
316 + try {
317 + await handleChangeSubscription(subscriptionType, buttonElement);
318 + } finally {
319 + closeSwitchSubscriptionModal();
320 + }
321 +}
322 +
323 +// Helpers pour gérer les plans annuels ( _year )
324 +function getCurrentPlanBase() {
325 + if (!currentPlan) return null;
326 + return currentPlan.replace(/_year$/, "");
327 +}
328 +
329 +function isCurrentPlanYearly() {
330 + return typeof currentPlan === "string" && /_year$/.test(currentPlan);
331 +}
332 +
219 333 // Mettre à jour l'affichage des plans
220 334 function updatePlansDisplay() {
221 335 const planWrappers = document.querySelectorAll(".ai-plan-card-wrapper");
222 336 const cancelCard = document.getElementById("cancel-subscription-card");
@@ -224,8 +338,11 @@
224 338 if (!planWrappers || planWrappers.length === 0) {
225 339 return; // Les plans ne sont pas encore chargés
226 340 }
227 341
342 + const planBase = getCurrentPlanBase();
343 + const yearlyCurrent = isCurrentPlanYearly();
344 +
228 345 planWrappers.forEach((wrapper) => {
229 346 const planType = wrapper.getAttribute("data-plan");
230 347 const button = wrapper.querySelector(".ai-plan-btn");
231 348 if (!button) return;
@@ -237,28 +354,34 @@
237 354 wrapper.setAttribute("data-current-plan", "false");
238 355 button.classList.remove("current-plan");
239 356 button.disabled = false;
240 357
358 + // Libellé d'affichage du plan pour les CTAs ("Starter", "Pro", "Agency").
359 + const planLabel = getPlanDisplayLabel(planType);
360 + // Suffixe selon la période sélectionnée
361 + const periodSuffix = currentBilling === "yearly" ? " (yearly)" : "";
362 +
363 + // Est-ce que ce plan, dans sa période actuellement affichée, correspond
364 + // exactement à l'abonnement en cours ?
365 + const isExactCurrent =
366 + planBase === planType &&
367 + yearlyCurrent === (currentBilling === "yearly");
368 +
241 369 if (hasSubscription && !window.hasResiliate) {
242 - // Utilisateur avec abonnement actif
243 - if (planType === currentPlan) {
244 - // Plan actuel
370 + if (isExactCurrent) {
371 + // Plan + période = abonnement en cours
245 372 wrapper.setAttribute("data-current-plan", "true");
246 373 button.classList.add("current-plan");
247 374 btnText.textContent = "Current Plan";
248 375 button.disabled = true;
249 376 } else {
250 - // Autre plan -> option de changement
251 - btnText.textContent = `Switch to ${planType.charAt(0).toUpperCase() + planType.slice(1)}`;
377 + // Autre plan / autre période -> option de changement
378 + btnText.textContent = `Switch to ${planLabel}${periodSuffix}`;
252 379 button.disabled = false;
253 380 }
254 - } else if (hasSubscription && window.hasResiliate) {
255 - // Abonnement annulé mais pas encore terminé
256 - btnText.textContent = `Choose ${planType.charAt(0).toUpperCase() + planType.slice(1)}`;
257 - button.disabled = false;
258 381 } else {
259 - // Pas d'abonnement
260 - btnText.textContent = `Choose ${planType.charAt(0).toUpperCase() + planType.slice(1)}`;
382 + // Pas d'abonnement actif (ou abonnement résilié)
383 + btnText.textContent = `Choose ${planLabel}${periodSuffix}`;
261 384 button.disabled = false;
262 385 }
263 386 });
264 387
@@ -293,9 +416,11 @@
293 416 btn.className = "ai-primary-btn ai-reactivate-btn";
294 417 btn.onclick = handleReactivateSubscription;
295 418 } else if (hasSubscription) {
296 419 btnText.textContent = "Cancel Subscription";
297 - btn.className = "ai-danger-btn";
420 + btn.className = "ai-cancel-subscription-btn";
421 + btn.style.opacity = "0.7";
422 + btn.style.fontWeight = "200";
298 423 btn.onclick = showCancelModal;
299 424 } else {
300 425 // Ce cas ne devrait plus se produire car nous avons maintenant les boutons de plan individuels
301 426 btnText.textContent = "Get Subscription";
@@ -341,38 +466,290 @@
341 466 setLoading("subscription-btn", false);
342 467 }
343 468 }
344 469
345 -// Mettre à jour l'affichage de la section Buy Credits
470 +// Mettre à jour l'affichage de la section Buy Credits (top-up)
346 471 function updateCreditPurchaseSection() {
347 472 const creditCard = document.getElementById("credit-purchase-card");
348 473 const buyCreditsBtn = document.getElementById("buy-credits-btn");
349 - const subscriptionMessage = document.getElementById(
350 - "subscription-required-message"
474 + const lockOverlay = document.getElementById("topup-lock-overlay");
475 + if (!creditCard || !buyCreditsBtn) return;
476 +
477 + const amountInput = document.getElementById("topup-amount");
478 + const presets = document.querySelectorAll(".ai-topup-preset");
479 +
480 + const locked = !hasSubscription;
481 +
482 + creditCard.setAttribute("data-locked", locked ? "true" : "false");
483 + if (lockOverlay) {
484 + lockOverlay.hidden = !locked;
485 + }
486 +
487 + // Griser / désactiver tous les contrôles quand pas d'abonnement
488 + if (amountInput) amountInput.disabled = locked;
489 + presets.forEach((btn) => {
490 + btn.disabled = locked;
491 + });
492 +
493 + buyCreditsBtn.disabled = locked;
494 + buyCreditsBtn.onclick = locked ? null : handleCreditPurchase;
495 +
496 + // Rafraîchir l'affichage après changement d'état
497 + refreshTopupDisplay();
498 +}
499 +
500 +// --- Top-up custom amount helpers ---
501 +
502 +function computeCreditsFromAmount(amountDollars) {
503 + if (!Number.isFinite(amountDollars) || amountDollars <= 0) return 0;
504 + const amountCents = Math.round(amountDollars * 100);
505 + return Math.floor(amountCents / 100 / TOPUP_PRICE_PER_CREDIT_USD);
506 +}
507 +
508 +function clampTopupAmount(value) {
509 + const n = Number(value);
510 + if (!Number.isFinite(n)) return TOPUP_MIN_AMOUNT_USD;
511 + return Math.min(
512 + TOPUP_MAX_AMOUNT_USD,
513 + Math.max(TOPUP_MIN_AMOUNT_USD, Math.round(n))
351 514 );
352 - const sectionHeader = document.querySelector(
353 - "#credit-purchase-section .ai-section-header p"
354 - );
515 +}
355 516
356 - if (hasSubscription) {
357 - // Utilisateur avec abonnement - afficher normalement
358 - creditCard.classList.remove("ai-disabled");
359 - buyCreditsBtn.disabled = false;
360 - buyCreditsBtn.onclick = handleCreditPurchase;
361 - subscriptionMessage.style.display = "none";
362 - sectionHeader.textContent =
363 - "Purchase additional credits when you need them";
364 - } else {
365 - // Utilisateur sans abonnement - griser et afficher message
366 - creditCard.classList.add("ai-disabled");
367 - buyCreditsBtn.disabled = true;
368 - buyCreditsBtn.onclick = null;
369 - subscriptionMessage.style.display = "flex";
370 - // sectionHeader.textContent =
371 - // "Active subscription required to purchase credits";
517 +function getTopupRawAmount() {
518 + const amountInput = document.getElementById("topup-amount");
519 + if (!amountInput) return TOPUP_MIN_AMOUNT_USD;
520 + const raw = amountInput.value;
521 + if (raw === "" || raw === null) return NaN;
522 + return Number(raw);
523 +}
524 +
525 +function refreshTopupDisplay() {
526 + const amountInput = document.getElementById("topup-amount");
527 + const payEl = document.getElementById("topup-pay-amount");
528 + const creditsEl = document.getElementById("topup-credits-amount");
529 + const errorEl = document.getElementById("topup-error");
530 + const wrapper = document.querySelector(".ai-topup-amount-wrapper");
531 + const buyBtn = document.getElementById("buy-credits-btn");
532 + const btnText = buyBtn ? buyBtn.querySelector(".btn-text") : null;
533 + const presets = document.querySelectorAll(".ai-topup-preset");
534 +
535 + if (!amountInput) return;
536 +
537 + const raw = getTopupRawAmount();
538 + const hasValue = !Number.isNaN(raw);
539 + const isBelow = hasValue && raw < TOPUP_MIN_AMOUNT_USD;
540 + const isAbove = hasValue && raw > TOPUP_MAX_AMOUNT_USD;
541 + const isInvalid = !hasValue || isBelow || isAbove;
542 +
543 + // Valeur "effective" utilisée pour l'aperçu
544 + const effective = hasValue
545 + ? Math.min(TOPUP_MAX_AMOUNT_USD, Math.max(TOPUP_MIN_AMOUNT_USD, raw))
546 + : TOPUP_MIN_AMOUNT_USD;
547 +
548 + const credits = computeCreditsFromAmount(effective);
549 +
550 + if (payEl) {
551 + payEl.textContent = `$${effective.toFixed(2)}`;
372 552 }
553 + if (creditsEl) {
554 + creditsEl.textContent = `${credits.toLocaleString("en-US")} credits`;
555 + }
556 +
557 + if (wrapper) {
558 + wrapper.classList.toggle("has-error", isInvalid && hasValue);
559 + }
560 +
561 + if (errorEl) {
562 + if (!hasValue) {
563 + errorEl.textContent = "";
564 + } else if (isBelow) {
565 + errorEl.textContent = `Minimum amount is $${TOPUP_MIN_AMOUNT_USD}.`;
566 + } else if (isAbove) {
567 + errorEl.textContent = `Maximum amount is $${TOPUP_MAX_AMOUNT_USD.toLocaleString(
568 + "en-US"
569 + )}.`;
570 + } else {
571 + errorEl.textContent = "";
572 + }
573 + }
574 +
575 + // Mise à jour visuelle du preset actif
576 + presets.forEach((btn) => {
577 + const presetAmount = Number(btn.getAttribute("data-amount"));
578 + btn.classList.toggle(
579 + "is-active",
580 + hasValue && !isInvalid && presetAmount === raw
581 + );
582 + });
583 +
584 + // Bouton d'achat
585 + if (buyBtn) {
586 + const locked = !hasSubscription;
587 + const canBuy = !locked && !isInvalid && hasValue && credits > 0;
588 + buyBtn.disabled = !canBuy;
589 + if (btnText && !buyBtn.classList.contains("is-loading")) {
590 + if (locked) {
591 + btnText.textContent = "Purchase credits";
592 + } else if (isInvalid || !hasValue) {
593 + btnText.textContent = "Enter a valid amount";
594 + } else {
595 + btnText.textContent = `Purchase ${credits.toLocaleString(
596 + "en-US"
597 + )} credits for $${effective.toFixed(2)}`;
598 + }
599 + }
600 + }
373 601 }
374 602
603 +function initTopupSection() {
604 + const amountInput = document.getElementById("topup-amount");
605 + const presets = document.querySelectorAll(".ai-topup-preset");
606 +
607 + if (!amountInput) return;
608 +
609 + amountInput.addEventListener("input", function () {
610 + refreshTopupDisplay();
611 + });
612 +
613 + amountInput.addEventListener("blur", function () {
614 + const raw = getTopupRawAmount();
615 + if (Number.isNaN(raw)) {
616 + amountInput.value = String(TOPUP_MIN_AMOUNT_USD);
617 + } else {
618 + amountInput.value = String(clampTopupAmount(raw));
619 + }
620 + refreshTopupDisplay();
621 + });
622 +
623 + presets.forEach((btn) => {
624 + btn.addEventListener("click", function () {
625 + if (btn.disabled) return;
626 + const presetAmount = Number(btn.getAttribute("data-amount"));
627 + if (!Number.isFinite(presetAmount)) return;
628 + amountInput.value = String(clampTopupAmount(presetAmount));
629 + refreshTopupDisplay();
630 + });
631 + });
632 +
633 + refreshTopupDisplay();
634 +}
635 +
636 +// --- Billing period (monthly / yearly) ---
637 +
638 +function formatMonthlyEquivalent(yearlyPrice) {
639 + // Montant mensuel équivalent (ex: 90 / 12 = 7.5 -> "$7.50")
640 + const value = yearlyPrice / 12;
641 + const rounded = Math.round(value * 100) / 100;
642 + // Affiche sans décimales inutiles: 7.5 -> "$7.50", 15.83 -> "$15.83"
643 + const hasCents = Math.round(rounded * 100) % 100 !== 0;
644 + return `$${rounded.toFixed(hasCents ? 2 : 0)}`;
645 +}
646 +
647 +function formatPerCreditYearly(perCredit) {
648 + // Ex: 0.01 -> "$0.010", 0.00791666 -> "$0.00792", 0.00680555 -> "$0.00681".
649 + // On arrondit à 5 décimales puis on retire les zéros de fin en conservant
650 + // au minimum 3 décimales pour un affichage cohérent.
651 + if (!Number.isFinite(perCredit) || perCredit <= 0) return "";
652 + let str = (Math.round(perCredit * 100000) / 100000).toFixed(5);
653 + str = str.replace(/(\.\d{3}\d*?)0+$/, "$1");
654 + return `$${str}`;
655 +}
656 +
657 +function applyBillingPeriod() {
658 + const planWrappers = document.querySelectorAll(".ai-plan-card-wrapper");
659 + const isYearly = currentBilling === "yearly";
660 +
661 + // État visuel du switch
662 + document.querySelectorAll(".ai-billing-option").forEach((btn) => {
663 + const active = btn.getAttribute("data-billing") === currentBilling;
664 + btn.classList.toggle("is-active", active);
665 + btn.setAttribute("aria-selected", active ? "true" : "false");
666 + });
667 +
668 + planWrappers.forEach((wrapper) => {
669 + const monthlyPrice = Number(wrapper.getAttribute("data-monthly-price"));
670 + const yearlyPrice = Number(wrapper.getAttribute("data-yearly-price"));
671 + const monthlyCredits = Number(
672 + wrapper.getAttribute("data-monthly-credits")
673 + );
674 + const monthlyType = wrapper.getAttribute("data-monthly-type");
675 + const yearlyType = wrapper.getAttribute("data-yearly-type");
676 +
677 + const amountEl = wrapper.querySelector(".ai-price-amount");
678 + const oldEl = wrapper.querySelector(".ai-price-old");
679 + const billedEl = wrapper.querySelector(".ai-price-billed");
680 + const creditEl = wrapper.querySelector(".ai-price-credit");
681 + const button = wrapper.querySelector(".ai-plan-btn");
682 +
683 + // Sauvegarde du libellé mensuel d'origine (hardcodé dans le HTML) pour
684 + // pouvoir le restaurer quand on revient en monthly, sans avoir à le
685 + // recalculer (les valeurs monthly sont des arrondis marketing).
686 + if (creditEl && !creditEl.dataset.monthlyText) {
687 + creditEl.dataset.monthlyText = creditEl.textContent.trim();
688 + }
689 +
690 + if (isYearly && Number.isFinite(yearlyPrice)) {
691 + // Prix effectif mensuel à partir du tarif annuel
692 + if (amountEl) amountEl.textContent = formatMonthlyEquivalent(yearlyPrice);
693 + if (oldEl) {
694 + oldEl.textContent = `$${monthlyPrice}`;
695 + oldEl.hidden = false;
696 + }
697 + if (billedEl) {
698 + billedEl.textContent = `Billed annually at $${yearlyPrice}/year`;
699 + billedEl.hidden = false;
700 + }
701 + // Prix par crédit recalculé sur la base annuelle :
702 + // yearlyPrice / (monthlyCredits * 12)
703 + if (creditEl && Number.isFinite(monthlyCredits) && monthlyCredits > 0) {
704 + const perCredit = yearlyPrice / (monthlyCredits * 12);
705 + const formatted = formatPerCreditYearly(perCredit);
706 + if (formatted) {
707 + creditEl.textContent = `${formatted} per credit`;
708 + }
709 + }
710 + if (button && yearlyType) {
711 + button.setAttribute("data-subscription-type", yearlyType);
712 + }
713 + } else {
714 + if (amountEl) amountEl.textContent = `$${monthlyPrice}`;
715 + if (oldEl) {
716 + oldEl.hidden = true;
717 + }
718 + if (billedEl) {
719 + billedEl.hidden = true;
720 + }
721 + // Restaurer le libellé mensuel d'origine (ex: "$0.012 per credit")
722 + if (creditEl && creditEl.dataset.monthlyText) {
723 + creditEl.textContent = creditEl.dataset.monthlyText;
724 + }
725 + if (button && monthlyType) {
726 + button.setAttribute("data-subscription-type", monthlyType);
727 + }
728 + }
729 + });
730 +
731 + // Recalculer les libellés "Current Plan" / "Switch to ..." selon la période
732 + updatePlansDisplay();
733 +}
734 +
735 +function initBillingToggle() {
736 + const toggle = document.querySelector(".ai-billing-toggle");
737 + if (!toggle) return;
738 +
739 + toggle.addEventListener("click", function (event) {
740 + const btn = event.target.closest(".ai-billing-option");
741 + if (!btn) return;
742 + const billing = btn.getAttribute("data-billing");
743 + if (!billing || billing === currentBilling) return;
744 + currentBilling = billing;
745 + applyBillingPeriod();
746 + });
747 +
748 + // État initial
749 + applyBillingPeriod();
750 +}
751 +
375 752 // Gérer l'abonnement (nouveau checkout)
376 753 async function handleSubscription(subscriptionType, buttonElement) {
377 754 if (!subscriptionType) {
378 755 showMessage("Please select a subscription plan", "error");
@@ -443,10 +820,13 @@
443 820 const loading = buttonElement.querySelector(".ai-loading");
444 821 const btnText = buttonElement.querySelector(".btn-text");
445 822 if (loading) loading.style.display = "none";
446 823 if (btnText) {
447 - btnText.textContent = buttonElement.getAttribute("data-subscription-type")
448 - ? `Choose ${buttonElement.getAttribute("data-subscription-type").charAt(0).toUpperCase() + buttonElement.getAttribute("data-subscription-type").slice(1)}`
824 + const subType = buttonElement.getAttribute("data-subscription-type");
825 + const periodSuffix =
826 + currentBilling === "yearly" ? " (yearly)" : "";
827 + btnText.textContent = subType
828 + ? `Choose ${getPlanDisplayLabel(subType)}${periodSuffix}`
449 829 : "Choose Plan";
450 830 }
451 831 }
452 832 }
@@ -462,10 +842,13 @@
462 842 const loading = buttonElement.querySelector(".ai-loading");
463 843 const btnText = buttonElement.querySelector(".btn-text");
464 844 if (loading) loading.style.display = "none";
465 845 if (btnText) {
466 - btnText.textContent = buttonElement.getAttribute("data-subscription-type")
467 - ? `Choose ${buttonElement.getAttribute("data-subscription-type").charAt(0).toUpperCase() + buttonElement.getAttribute("data-subscription-type").slice(1)}`
846 + const subType = buttonElement.getAttribute("data-subscription-type");
847 + const periodSuffix =
848 + currentBilling === "yearly" ? " (yearly)" : "";
849 + btnText.textContent = subType
850 + ? `Choose ${getPlanDisplayLabel(subType)}${periodSuffix}`
468 851 : "Choose Plan";
469 852 }
470 853 }
471 854 }
@@ -551,9 +934,13 @@
551 934 const loading = buttonElement.querySelector(".ai-loading");
552 935 const btnText = buttonElement.querySelector(".btn-text");
553 936 if (loading) loading.style.display = "none";
554 937 if (btnText) {
555 - btnText.textContent = `Switch to ${subscriptionType.charAt(0).toUpperCase() + subscriptionType.slice(1)}`;
938 + const periodSuffix =
939 + /_year$/.test(subscriptionType) ? " (yearly)" : "";
940 + btnText.textContent = `Switch to ${getPlanDisplayLabel(
941 + subscriptionType
942 + )}${periodSuffix}`;
556 943 }
557 944 }
558 945 }
559 946 } catch (error) {
@@ -568,16 +955,65 @@
568 955 const loading = buttonElement.querySelector(".ai-loading");
569 956 const btnText = buttonElement.querySelector(".btn-text");
570 957 if (loading) loading.style.display = "none";
571 958 if (btnText) {
572 - btnText.textContent = `Switch to ${subscriptionType.charAt(0).toUpperCase() + subscriptionType.slice(1)}`;
959 + const periodSuffix =
960 + /_year$/.test(subscriptionType) ? " (yearly)" : "";
961 + btnText.textContent = `Switch to ${getPlanDisplayLabel(
962 + subscriptionType
963 + )}${periodSuffix}`;
573 964 }
574 965 }
575 966 }
576 967 }
577 968
578 -// Acheter des crédits
969 +// Acheter des crédits (top-up avec montant personnalisé)
579 970 async function handleCreditPurchase() {
971 + if (!hasSubscription) {
972 + showMessage(
973 + "An active subscription is required to buy top-up credits.",
974 + "error"
975 + );
976 + return;
977 + }
978 +
979 + const buyBtn = document.getElementById("buy-credits-btn");
980 + const btnText = buyBtn ? buyBtn.querySelector(".btn-text") : null;
981 + const loading = buyBtn ? buyBtn.querySelector(".ai-loading") : null;
982 +
983 + // Validation du montant côté front (miroir du back)
984 + const raw = getTopupRawAmount();
985 + if (
986 + Number.isNaN(raw) ||
987 + raw < TOPUP_MIN_AMOUNT_USD ||
988 + raw > TOPUP_MAX_AMOUNT_USD
989 + ) {
990 + showMessage(
991 + `Please enter an amount between $${TOPUP_MIN_AMOUNT_USD} and $${TOPUP_MAX_AMOUNT_USD.toLocaleString(
992 + "en-US"
993 + )}.`,
994 + "error"
995 + );
996 + refreshTopupDisplay();
997 + return;
998 + }
999 +
1000 + const amountDollars = clampTopupAmount(raw);
1001 + const credits = computeCreditsFromAmount(amountDollars);
1002 + if (credits <= 0) {
1003 + showMessage("Amount too low to grant any credit.", "error");
1004 + return;
1005 + }
1006 +
1007 + // Loading state
1008 + if (buyBtn) {
1009 + buyBtn.classList.add("is-loading");
1010 + buyBtn.disabled = true;
1011 + }
1012 + if (loading) loading.style.display = "inline-block";
1013 + const previousBtnText = btnText ? btnText.textContent : "";
1014 + if (btnText) btnText.textContent = "Redirecting to checkout...";
1015 +
580 1016 try {
581 1017 const tokenResponse = await fetch(ajaxurl, {
582 1018 method: "POST",
583 1019 headers: { "Content-Type": "application/x-www-form-urlencoded" },
@@ -584,13 +1020,17 @@
584 1020 body: "action=aibui_get_token&nonce=" + aiBuilderVars.nonce,
585 1021 });
586 1022
587 1023 const tokenData = await tokenResponse.json();
1024 + if (!tokenData.success || !tokenData.data.token) {
1025 + showMessage("Authentication failed", "error");
1026 + return;
1027 + }
588 1028 const jwtToken = tokenData.data.token;
589 1029
590 1030 const urlFrom = window.location.href;
591 1031 const response = await fetch(
592 - window.config.apiUrl + "/payments/create-checkout-session-credits",
1032 + window.config.apiUrl + "/payments/create-checkout-session-topup",
593 1033 {
594 1034 method: "POST",
595 1035 headers: {
596 1036 Authorization: `Bearer ${jwtToken}`,
@@ -597,8 +1037,9 @@
597 1037 "Content-Type": "application/json",
598 1038 },
599 1039 body: JSON.stringify({
600 1040 urlFrom,
1041 + amount: amountDollars,
601 1042 }),
602 1043 }
603 1044 );
604 1045
@@ -605,14 +1046,23 @@
605 1046 const data = await response.json();
606 1047
607 1048 if (response.ok && data.url) {
608 1049 window.location.href = data.url;
609 - } else {
610 - showMessage(data.message || "Failed to create credit checkout", "error");
1050 + return;
611 1051 }
1052 +
1053 + showMessage(data.message || "Failed to create credit checkout", "error");
612 1054 } catch (error) {
613 1055 console.error("Error creating credit checkout:", error);
614 1056 showMessage("Network error while creating credit checkout", "error");
1057 + } finally {
1058 + if (buyBtn) {
1059 + buyBtn.classList.remove("is-loading");
1060 + buyBtn.disabled = false;
1061 + }
1062 + if (loading) loading.style.display = "none";
1063 + if (btnText && previousBtnText) btnText.textContent = previousBtnText;
1064 + refreshTopupDisplay();
615 1065 }
616 1066 }
617 1067
618 1068 // Accéder au compte Stripe