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
ai-builder / assets / js / credits.js

credits.js in AI Builder – Generate pages, blocks, images & translate with AI 2.7.10, at assets/js/credits.js

1,196 lines 38.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 let userData = null;
2 let hasSubscription = false;
3 let currentPlan = null;
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
31 // Charger les informations utilisateur au chargement
32 document.addEventListener("DOMContentLoaded", function () {
33 // Vérifier les paramètres de succès dans l'URL
34 checkSuccessParameters();
35
36 // Attacher les événements aux boutons de plan (une seule fois au chargement)
37 attachPlanButtonEvents();
38
39 // Initialiser le switch mensuel / annuel
40 initBillingToggle();
41
42 // Initialiser la section top-up (input montant personnalisé)
43 initTopupSection();
44
45 // Charger les informations utilisateur
46 loadUserInfo();
47 });
48
49 // Fonction pour vérifier les paramètres de succès dans l'URL
50 function checkSuccessParameters() {
51 const urlParams = new URLSearchParams(window.location.search);
52 const type = urlParams.get("type");
53 const sessionId = urlParams.get("session_id");
54
55 if (type && sessionId) {
56 if (type === "credits_success" || type === "topup_success") {
57 showMessage(
58 "Credit purchase completed successfully! Your credits will be updated shortly.",
59 "success"
60 );
61 // Recharger les données après un délai pour s'assurer que les crédits sont mis à jour
62 setTimeout(() => {
63 loadUserInfo();
64 }, 3000);
65 // Nettoyer l'URL après affichage du message
66 setTimeout(() => {
67 const newUrl =
68 window.location.pathname +
69 window.location.search
70 .replace(/[?&]type=[^&]*&session_id=[^&]*/, "")
71 .replace(/^&/, "?");
72 window.history.replaceState({}, document.title, newUrl);
73 }, 5000);
74 } else if (type === "subscription_success") {
75 showMessage(
76 "Subscription activated successfully! Your subscription is now active.",
77 "success"
78 );
79 // Recharger les données après un délai pour s'assurer que l'abonnement est mis à jour
80 setTimeout(() => {
81 loadUserInfo();
82 }, 3000);
83 // Nettoyer l'URL après affichage du message
84 setTimeout(() => {
85 const newUrl =
86 window.location.pathname +
87 window.location.search
88 .replace(/[?&]type=[^&]*&session_id=[^&]*/, "")
89 .replace(/^&/, "?");
90 window.history.replaceState({}, document.title, newUrl);
91 }, 5000);
92 }
93 }
94 }
95
96 // Fonction pour charger les informations utilisateur
97 async function loadUserInfo() {
98 try {
99 const tokenResponse = await fetch(ajaxurl, {
100 method: "POST",
101 headers: { "Content-Type": "application/x-www-form-urlencoded" },
102 body: "action=aibui_get_token&nonce=" + aiBuilderVars.nonce,
103 });
104
105 const tokenData = await tokenResponse.json();
106
107 if (!tokenData.success || !tokenData.data.token) {
108 showMessage("Authentication failed", "error");
109 return;
110 }
111
112 const jwtToken = tokenData.data.token;
113
114 const response = await fetch(window.config.apiUrl + "/user/profile", {
115 method: "GET",
116 headers: {
117 Authorization: `Bearer ${jwtToken}`,
118 "Content-Type": "application/json",
119 },
120 });
121
122 if (response.ok) {
123 userData = await response.json();
124 displayUserInfo(userData?.user);
125 // updateSubscriptionButton() n'est plus nécessaire car updatePlansDisplay() gère tout
126 } else {
127 showMessage("Failed to load user information", "error");
128 }
129 } catch (error) {
130 console.error("Error loading user info:", error);
131 showMessage("Network error while loading user information", "error");
132 }
133 }
134
135 // Afficher les informations utilisateur
136 function displayUserInfo(data) {
137 // Afficher les crédits
138 const creditsDisplay = document.getElementById("current-credits");
139 const creditsBreakdown = data.aiCredits || {
140 onAccountCreation: 0,
141 monthlySubscription: 0,
142 paid: 0,
143 };
144
145 const totalCredits =
146 creditsBreakdown.onAccountCreation +
147 creditsBreakdown.monthlySubscription +
148 creditsBreakdown.paid;
149
150 if (creditsDisplay) {
151 creditsDisplay.innerHTML = `
152 <span class="credits-number">${totalCredits}</span>
153 <span class="credits-label">credits</span>
154 `;
155 }
156
157 // Afficher le plan
158 const planDisplay = document.getElementById("current-plan");
159 if (planDisplay) {
160 const plan = data.plan || "basic";
161 let resiliateText = "";
162 if (data.resiliateAt) {
163 const resiliateDate = new Date(data.resiliateAt);
164 const now = new Date();
165 const isEnded = resiliateDate < now.setHours(0, 0, 0, 0);
166 if (isEnded) {
167 resiliateText = `<span class="resiliate-badge">Subscription has ended on <strong>${resiliateDate.toLocaleDateString()}</strong></span>`;
168 } else {
169 resiliateText = `<span class="resiliate-badge">Subscription will end on <strong>${resiliateDate.toLocaleDateString()}</strong></span>`;
170 }
171 }
172 planDisplay.innerHTML = `
173 <span class="plan-badge ${plan}">${getPlanDisplayLabel(plan)}</span>
174 ${resiliateText}
175 `;
176 }
177
178 // Afficher le message dans la carte Subscription (section management)
179 const subscriptionCard = document.querySelector(
180 ".ai-subscription-actions .ai-action-content"
181 );
182 if (subscriptionCard) {
183 let resiliateMsg = subscriptionCard.querySelector(".resiliate-badge");
184 if (data.resiliateAt) {
185 if (!resiliateMsg) {
186 resiliateMsg = document.createElement("div");
187 resiliateMsg.className = "resiliate-badge";
188 resiliateMsg.innerHTML = `Subscription will end on <strong>${new Date(
189 data.resiliateAt
190 ).toLocaleDateString()}</strong>`;
191 subscriptionCard.appendChild(resiliateMsg);
192 } else {
193 resiliateMsg.innerHTML = `Subscription will end on <strong>${new Date(
194 data.resiliateAt
195 ).toLocaleDateString()}</strong>`;
196 }
197 } else if (resiliateMsg) {
198 resiliateMsg.remove();
199 }
200 }
201
202 // Vérifier si l'utilisateur a un abonnement
203 hasSubscription = data.plan && data.plan !== "basic";
204 currentPlan = data.plan || "basic";
205 window.hasResiliate = !!data.resiliateAt;
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
217 // Mettre à jour l'affichage de la section Buy Credits
218 updateCreditPurchaseSection();
219 }
220
221 // Attacher les événements aux boutons de plan
222 function attachPlanButtonEvents() {
223 // Utiliser la délégation d'événements pour éviter les problèmes de duplication
224 const plansContainer = document.getElementById("subscription-plans-container");
225 if (plansContainer) {
226 // Retirer l'ancien listener s'il existe
227 plansContainer.removeEventListener("click", handlePlanButtonClick);
228 // Ajouter le nouveau listener
229 plansContainer.addEventListener("click", handlePlanButtonClick);
230 }
231
232 // Bouton d'annulation
233 const cancelBtn = document.getElementById("subscription-btn");
234 if (cancelBtn) {
235 cancelBtn.onclick = showCancelModal;
236 }
237 }
238
239 // Gestionnaire de clic pour les boutons de plan (délégation d'événements)
240 function handlePlanButtonClick(event) {
241 const button = event.target.closest(".ai-plan-btn");
242 if (!button || button.disabled) return;
243
244 const subscriptionType = button.getAttribute("data-subscription-type");
245 if (subscriptionType) {
246 // Vérifier dynamiquement l'état de l'abonnement
247 if (hasSubscription && !window.hasResiliate) {
248 // Abonnement actif -> on demande confirmation avant de changer de plan
249 openSwitchSubscriptionModal(subscriptionType, button);
250 } else {
251 // Utilisateur sans abonnement ou abonnement annulé -> nouveau checkout
252 handleSubscription(subscriptionType, button);
253 }
254 }
255 }
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
333 // Mettre à jour l'affichage des plans
334 function updatePlansDisplay() {
335 const planWrappers = document.querySelectorAll(".ai-plan-card-wrapper");
336 const cancelCard = document.getElementById("cancel-subscription-card");
337
338 if (!planWrappers || planWrappers.length === 0) {
339 return; // Les plans ne sont pas encore chargés
340 }
341
342 const planBase = getCurrentPlanBase();
343 const yearlyCurrent = isCurrentPlanYearly();
344
345 planWrappers.forEach((wrapper) => {
346 const planType = wrapper.getAttribute("data-plan");
347 const button = wrapper.querySelector(".ai-plan-btn");
348 if (!button) return;
349
350 const btnText = button.querySelector(".btn-text");
351 if (!btnText) return;
352
353 // Réinitialiser l'état
354 wrapper.setAttribute("data-current-plan", "false");
355 button.classList.remove("current-plan");
356 button.disabled = false;
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
369 if (hasSubscription && !window.hasResiliate) {
370 if (isExactCurrent) {
371 // Plan + période = abonnement en cours
372 wrapper.setAttribute("data-current-plan", "true");
373 button.classList.add("current-plan");
374 btnText.textContent = "Current Plan";
375 button.disabled = true;
376 } else {
377 // Autre plan / autre période -> option de changement
378 btnText.textContent = `Switch to ${planLabel}${periodSuffix}`;
379 button.disabled = false;
380 }
381 } else {
382 // Pas d'abonnement actif (ou abonnement résilié)
383 btnText.textContent = `Choose ${planLabel}${periodSuffix}`;
384 button.disabled = false;
385 }
386 });
387
388 // Afficher/masquer la carte d'annulation
389 if (cancelCard) {
390 if (hasSubscription && !window.hasResiliate) {
391 cancelCard.style.display = "block";
392 } else {
393 cancelCard.style.display = "none";
394 }
395 }
396 }
397
398 // Mettre à jour le bouton d'abonnement (fonction conservée pour compatibilité mais non utilisée)
399 // L'affichage des plans est maintenant géré par updatePlansDisplay()
400 // Cette fonction peut être utilisée pour le bouton d'annulation si nécessaire
401 function updateSubscriptionButton() {
402 const btn = document.getElementById("subscription-btn");
403 if (!btn) return; // Sécurité : le bouton n'existe pas
404
405 // Reconstruit le HTML interne du bouton à chaque fois
406 btn.innerHTML = `
407 <span class="ai-loading" style="display: none;"></span>
408 <span class="btn-text"></span>
409 `;
410 const btnText = btn.querySelector(".btn-text");
411 const loading = btn.querySelector(".ai-loading");
412 if (!btnText || !loading) return; // Sécurité : structure inattendue
413
414 if (hasSubscription && window.hasResiliate) {
415 btnText.textContent = "Reactivate Subscription";
416 btn.className = "ai-primary-btn ai-reactivate-btn";
417 btn.onclick = handleReactivateSubscription;
418 } else if (hasSubscription) {
419 btnText.textContent = "Cancel Subscription";
420 btn.className = "ai-cancel-subscription-btn";
421 btn.style.opacity = "0.7";
422 btn.style.fontWeight = "200";
423 btn.onclick = showCancelModal;
424 } else {
425 // Ce cas ne devrait plus se produire car nous avons maintenant les boutons de plan individuels
426 btnText.textContent = "Get Subscription";
427 btn.className = "ai-primary-btn";
428 btn.onclick = null; // Ne pas assigner handleSubscription sans paramètre
429 }
430 }
431
432 // Fonction pour réactiver l'abonnement
433 async function handleReactivateSubscription() {
434 setLoading("subscription-btn", true);
435 try {
436 const tokenResponse = await fetch(ajaxurl, {
437 method: "POST",
438 headers: { "Content-Type": "application/x-www-form-urlencoded" },
439 body: "action=aibui_get_token&nonce=" + aiBuilderVars.nonce,
440 });
441 const tokenData = await tokenResponse.json();
442 const jwtToken = tokenData.data.token;
443 const response = await fetch(
444 window.config.apiUrl + "/payments/resume-subscription",
445 {
446 method: "POST",
447 headers: {
448 Authorization: `Bearer ${jwtToken}`,
449 "Content-Type": "application/json",
450 },
451 }
452 );
453 const data = await response.json();
454 if (response.ok) {
455 showMessage("Subscription reactivated successfully!", "success");
456 setTimeout(() => {
457 loadUserInfo();
458 }, 1000);
459 } else {
460 showMessage(data.message || "Failed to reactivate subscription", "error");
461 }
462 } catch (error) {
463 console.error("Error reactivating subscription:", error);
464 showMessage("Network error while reactivating subscription", "error");
465 } finally {
466 setLoading("subscription-btn", false);
467 }
468 }
469
470 // Mettre à jour l'affichage de la section Buy Credits (top-up)
471 function updateCreditPurchaseSection() {
472 const creditCard = document.getElementById("credit-purchase-card");
473 const buyCreditsBtn = document.getElementById("buy-credits-btn");
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))
514 );
515 }
516
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)}`;
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 }
601 }
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
752 // Gérer l'abonnement (nouveau checkout)
753 async function handleSubscription(subscriptionType, buttonElement) {
754 if (!subscriptionType) {
755 showMessage("Please select a subscription plan", "error");
756 return;
757 }
758
759 // Désactiver tous les boutons pendant le traitement
760 const planButtons = document.querySelectorAll(".ai-plan-btn");
761 planButtons.forEach((btn) => {
762 btn.disabled = true;
763 });
764
765 // Afficher le loading sur le bouton cliqué
766 if (buttonElement) {
767 const loading = buttonElement.querySelector(".ai-loading");
768 const btnText = buttonElement.querySelector(".btn-text");
769 if (loading) loading.style.display = "inline-block";
770 if (btnText) {
771 const originalText = btnText.textContent;
772 btnText.textContent = "Processing...";
773 }
774 }
775
776 try {
777 const tokenResponse = await fetch(ajaxurl, {
778 method: "POST",
779 headers: { "Content-Type": "application/x-www-form-urlencoded" },
780 body: "action=aibui_get_token&nonce=" + aiBuilderVars.nonce,
781 });
782
783 const tokenData = await tokenResponse.json();
784 if (!tokenData.success || !tokenData.data.token) {
785 showMessage("Authentication failed", "error");
786 return;
787 }
788
789 const jwtToken = tokenData.data.token;
790
791 const response = await fetch(
792 window.config.apiUrl + "/payments/create-checkout-session-abo",
793 {
794 method: "POST",
795 headers: {
796 Authorization: `Bearer ${jwtToken}`,
797 "Content-Type": "application/json",
798 },
799 body: JSON.stringify({
800 subscriptionType: subscriptionType,
801 urlFrom: window.location.href,
802 }),
803 }
804 );
805
806 const data = await response.json();
807
808 if (response.ok && data.url) {
809 window.location.href = data.url;
810 } else {
811 showMessage(
812 data.message || "Failed to create subscription checkout",
813 "error"
814 );
815 // Réactiver les boutons en cas d'erreur
816 planButtons.forEach((btn) => {
817 btn.disabled = false;
818 });
819 if (buttonElement) {
820 const loading = buttonElement.querySelector(".ai-loading");
821 const btnText = buttonElement.querySelector(".btn-text");
822 if (loading) loading.style.display = "none";
823 if (btnText) {
824 const subType = buttonElement.getAttribute("data-subscription-type");
825 const periodSuffix =
826 currentBilling === "yearly" ? " (yearly)" : "";
827 btnText.textContent = subType
828 ? `Choose ${getPlanDisplayLabel(subType)}${periodSuffix}`
829 : "Choose Plan";
830 }
831 }
832 }
833 } catch (error) {
834 console.error("Error creating subscription:", error);
835 showMessage("Network error while creating subscription", "error");
836 // Réactiver les boutons en cas d'erreur
837 const planButtons = document.querySelectorAll(".ai-plan-btn");
838 planButtons.forEach((btn) => {
839 btn.disabled = false;
840 });
841 if (buttonElement) {
842 const loading = buttonElement.querySelector(".ai-loading");
843 const btnText = buttonElement.querySelector(".btn-text");
844 if (loading) loading.style.display = "none";
845 if (btnText) {
846 const subType = buttonElement.getAttribute("data-subscription-type");
847 const periodSuffix =
848 currentBilling === "yearly" ? " (yearly)" : "";
849 btnText.textContent = subType
850 ? `Choose ${getPlanDisplayLabel(subType)}${periodSuffix}`
851 : "Choose Plan";
852 }
853 }
854 }
855 }
856
857 // Gérer le changement d'abonnement
858 async function handleChangeSubscription(subscriptionType, buttonElement) {
859 if (!subscriptionType) {
860 showMessage("Please select a subscription plan", "error");
861 return;
862 }
863
864 // Vérifier si c'est le même plan
865 if (subscriptionType === currentPlan) {
866 showMessage("You are already on this plan", "info");
867 return;
868 }
869
870 // Désactiver tous les boutons pendant le traitement
871 const planButtons = document.querySelectorAll(".ai-plan-btn");
872 planButtons.forEach((btn) => {
873 btn.disabled = true;
874 });
875
876 // Afficher le loading sur le bouton cliqué
877 if (buttonElement) {
878 const loading = buttonElement.querySelector(".ai-loading");
879 const btnText = buttonElement.querySelector(".btn-text");
880 if (loading) loading.style.display = "inline-block";
881 if (btnText) {
882 const originalText = btnText.textContent;
883 btnText.textContent = "Switching...";
884 }
885 }
886
887 try {
888 const tokenResponse = await fetch(ajaxurl, {
889 method: "POST",
890 headers: { "Content-Type": "application/x-www-form-urlencoded" },
891 body: "action=aibui_get_token&nonce=" + aiBuilderVars.nonce,
892 });
893
894 const tokenData = await tokenResponse.json();
895 if (!tokenData.success || !tokenData.data.token) {
896 showMessage("Authentication failed", "error");
897 return;
898 }
899
900 const jwtToken = tokenData.data.token;
901
902 const response = await fetch(
903 window.config.apiUrl + "/payments/change-subscription",
904 {
905 method: "POST",
906 headers: {
907 Authorization: `Bearer ${jwtToken}`,
908 "Content-Type": "application/json",
909 },
910 body: JSON.stringify({
911 subscriptionType: subscriptionType,
912 }),
913 }
914 );
915
916 const data = await response.json();
917
918 if (response.ok) {
919 showMessage("Subscription changed successfully!", "success");
920 // Recharger les informations utilisateur
921 setTimeout(() => {
922 loadUserInfo();
923 }, 1000);
924 } else {
925 showMessage(
926 data.message || "Failed to change subscription",
927 "error"
928 );
929 // Réactiver les boutons en cas d'erreur
930 planButtons.forEach((btn) => {
931 btn.disabled = false;
932 });
933 if (buttonElement) {
934 const loading = buttonElement.querySelector(".ai-loading");
935 const btnText = buttonElement.querySelector(".btn-text");
936 if (loading) loading.style.display = "none";
937 if (btnText) {
938 const periodSuffix =
939 /_year$/.test(subscriptionType) ? " (yearly)" : "";
940 btnText.textContent = `Switch to ${getPlanDisplayLabel(
941 subscriptionType
942 )}${periodSuffix}`;
943 }
944 }
945 }
946 } catch (error) {
947 console.error("Error changing subscription:", error);
948 showMessage("Network error while changing subscription", "error");
949 // Réactiver les boutons en cas d'erreur
950 const planButtons = document.querySelectorAll(".ai-plan-btn");
951 planButtons.forEach((btn) => {
952 btn.disabled = false;
953 });
954 if (buttonElement) {
955 const loading = buttonElement.querySelector(".ai-loading");
956 const btnText = buttonElement.querySelector(".btn-text");
957 if (loading) loading.style.display = "none";
958 if (btnText) {
959 const periodSuffix =
960 /_year$/.test(subscriptionType) ? " (yearly)" : "";
961 btnText.textContent = `Switch to ${getPlanDisplayLabel(
962 subscriptionType
963 )}${periodSuffix}`;
964 }
965 }
966 }
967 }
968
969 // Acheter des crédits (top-up avec montant personnalisé)
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
1016 try {
1017 const tokenResponse = await fetch(ajaxurl, {
1018 method: "POST",
1019 headers: { "Content-Type": "application/x-www-form-urlencoded" },
1020 body: "action=aibui_get_token&nonce=" + aiBuilderVars.nonce,
1021 });
1022
1023 const tokenData = await tokenResponse.json();
1024 if (!tokenData.success || !tokenData.data.token) {
1025 showMessage("Authentication failed", "error");
1026 return;
1027 }
1028 const jwtToken = tokenData.data.token;
1029
1030 const urlFrom = window.location.href;
1031 const response = await fetch(
1032 window.config.apiUrl + "/payments/create-checkout-session-topup",
1033 {
1034 method: "POST",
1035 headers: {
1036 Authorization: `Bearer ${jwtToken}`,
1037 "Content-Type": "application/json",
1038 },
1039 body: JSON.stringify({
1040 urlFrom,
1041 amount: amountDollars,
1042 }),
1043 }
1044 );
1045
1046 const data = await response.json();
1047
1048 if (response.ok && data.url) {
1049 window.location.href = data.url;
1050 return;
1051 }
1052
1053 showMessage(data.message || "Failed to create credit checkout", "error");
1054 } catch (error) {
1055 console.error("Error creating credit checkout:", error);
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();
1065 }
1066 }
1067
1068 // Accéder au compte Stripe
1069 async function handleStripeAccount() {
1070 try {
1071 const tokenResponse = await fetch(ajaxurl, {
1072 method: "POST",
1073 headers: { "Content-Type": "application/x-www-form-urlencoded" },
1074 body: "action=aibui_get_token&nonce=" + aiBuilderVars.nonce,
1075 });
1076
1077 const tokenData = await tokenResponse.json();
1078 const jwtToken = tokenData.data.token;
1079
1080 const urlFrom = window.location.href;
1081 const response = await fetch(
1082 window.config.apiUrl + "/payments/stripe-customer-portal",
1083 {
1084 method: "POST",
1085 headers: {
1086 Authorization: `Bearer ${jwtToken}`,
1087 "Content-Type": "application/json",
1088 },
1089 body: JSON.stringify({
1090 urlFrom,
1091 }),
1092 }
1093 );
1094
1095 const data = await response.json();
1096
1097 if (response.ok && data.url) {
1098 window.open(data.url, "_blank");
1099 } else {
1100 showMessage(data.message || "Failed to access Stripe account", "error");
1101 }
1102 } catch (error) {
1103 console.error("Error accessing Stripe account:", error);
1104 showMessage("Network error while accessing Stripe account", "error");
1105 }
1106 }
1107
1108 // Afficher la modal de confirmation
1109 function showCancelModal() {
1110 document.getElementById("confirmation-modal").style.display = "flex";
1111 }
1112
1113 // Fermer la modal
1114 function closeModal() {
1115 document.getElementById("confirmation-modal").style.display = "none";
1116 }
1117
1118 // Confirmer l'annulation de l'abonnement
1119 async function confirmCancelSubscription() {
1120 const btn = document.querySelector(".ai-danger-btn");
1121 const loading = btn.querySelector(".ai-loading");
1122 const text = btn.textContent;
1123
1124 loading.style.display = "inline-block";
1125 btn.textContent = "";
1126 btn.appendChild(loading);
1127 btn.appendChild(document.createTextNode(text));
1128 btn.disabled = true;
1129
1130 try {
1131 const tokenResponse = await fetch(ajaxurl, {
1132 method: "POST",
1133 headers: { "Content-Type": "application/x-www-form-urlencoded" },
1134 body: "action=aibui_get_token&nonce=" + aiBuilderVars.nonce,
1135 });
1136
1137 const tokenData = await tokenResponse.json();
1138 const jwtToken = tokenData.data.token;
1139
1140 const response = await fetch(
1141 window.config.apiUrl + "/payments/cancel-subscription",
1142 {
1143 method: "POST",
1144 headers: {
1145 Authorization: `Bearer ${jwtToken}`,
1146 "Content-Type": "application/json",
1147 },
1148 }
1149 );
1150
1151 const data = await response.json();
1152
1153 if (response.ok) {
1154 showMessage("Subscription cancelled successfully", "success");
1155 closeModal();
1156 // Forcer l'état local pour l'UI
1157 window.hasResiliate = true;
1158 hasSubscription = false; // Mettre à jour l'état local
1159 // Recharger les informations utilisateur (qui va mettre à jour l'affichage via updatePlansDisplay)
1160 setTimeout(() => {
1161 loadUserInfo();
1162 }, 1000);
1163 } else {
1164 showMessage(data.message || "Failed to cancel subscription", "error");
1165 }
1166 } catch (error) {
1167 console.error("Error cancelling subscription:", error);
1168 showMessage("Network error while cancelling subscription", "error");
1169 } finally {
1170 loading.style.display = "none";
1171 btn.textContent = text;
1172 btn.disabled = false;
1173 }
1174 }
1175
1176 // Fonctions utilitaires
1177 function showMessage(message, type = "info") {
1178 const container = document.getElementById("message-container");
1179 container.innerHTML = `<div class="ai-message ${type}">${message}</div>`;
1180 container.scrollIntoView({ behavior: "smooth" });
1181 }
1182
1183 function setLoading(buttonId, isLoading) {
1184 const button = document.getElementById(buttonId);
1185 const loading = button.querySelector(".ai-loading");
1186 const text = button.querySelector(".btn-text");
1187
1188 if (isLoading) {
1189 loading.style.display = "inline-block";
1190 button.disabled = true;
1191 } else {
1192 loading.style.display = "none";
1193 button.disabled = false;
1194 }
1195 }
1196