PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
← All changes | assets/js/trip.js +85 -37 3.0.8 → 3.0.16 View file →
@@ -5,20 +5,43 @@
5 5 /* jshint esversion: 6 */
6 6
7 7 // Translation helpers (booking.js installs the same shims if it runs first;
8 8 // idempotent setup so either load order works).
9 +// Parse a numeric min/max attribute while preserving a legitimate 0.
10 +// `parseInt(el.getAttribute('max')) || 99` treated max="0" — what a sold-out
11 +// departure renders — as "unset" and fell back to the placeholder ceiling, so
12 +// the guest selector stayed usable and ignored the departure's real capacity.
13 +// Only an absent or non-numeric value may fall back. Installed once and shared
14 +// by every frontend script, so load order does not matter.
15 +if (typeof window.yatraNumOr !== 'function') {
16 + window.yatraNumOr = function (raw, fallback) {
17 + var parsed = parseInt(raw, 10);
18 + return isNaN(parsed) ? fallback : parsed;
19 + };
20 +}
21 +
9 22 (function () {
10 - if (typeof window.__ === 'function') return;
11 - if (window.wp && window.wp.i18n && typeof window.wp.i18n.__ === 'function') {
12 - window.__ = function (text, domain) { return window.wp.i18n.__(text, domain || 'yatra'); };
13 - window._n = function (s, p, n, domain) { return window.wp.i18n._n(s, p, n, domain || 'yatra'); };
14 - window._x = function (text, ctx, domain) { return window.wp.i18n._x(text, ctx, domain || 'yatra'); };
15 - window.sprintf = window.sprintf || (window.wp.i18n.sprintf || function (fmt) { return fmt; });
23 + var i18n = (window.wp && window.wp.i18n) ? window.wp.i18n : null;
24 + if (i18n && typeof i18n.__ === 'function') {
25 + window.__ = function (text, domain) { return i18n.__(text, domain || 'yatra'); };
26 + window._n = function (s, p, n, domain) { return i18n._n(s, p, n, domain || 'yatra'); };
27 + window._x = function (text, ctx, domain) { return i18n._x(text, ctx, domain || 'yatra'); };
28 + // Always bind WordPress's own sprintf (it substitutes %d/%s/%1$s).
29 + // Do NOT defer to a pre-existing window.sprintf: a theme or other
30 + // plugin can define an incompatible global that leaves placeholders
31 + // unsubstituted (the cause of literal "%d"/"%s" in the UI).
32 + if (typeof i18n.sprintf === 'function') {
33 + window.sprintf = function () { return i18n.sprintf.apply(i18n, arguments); };
34 + } else if (typeof window.sprintf !== 'function') {
35 + window.sprintf = function (fmt) { return fmt; };
36 + }
16 37 } else {
17 - window.__ = function (text) { return text; };
18 - window._n = function (s, p, n) { return n === 1 ? s : p; };
19 - window._x = function (text) { return text; };
20 - window.sprintf = window.sprintf || function (fmt) { return fmt; };
38 + // wp.i18n not present — install passthroughs only if nothing usable
39 + // exists yet (don't clobber a real binding installed earlier).
40 + if (typeof window.__ !== 'function') window.__ = function (text) { return text; };
41 + if (typeof window._n !== 'function') window._n = function (s, p, n) { return n === 1 ? s : p; };
42 + if (typeof window._x !== 'function') window._x = function (text) { return text; };
43 + if (typeof window.sprintf !== 'function') window.sprintf = function (fmt) { return fmt; };
21 44 }
22 45 })();
23 46
24 47 (function() {
@@ -182,9 +205,14 @@
182 205 const decimalSep = (typeof cfg.decimalSeparator === 'string') ? cfg.decimalSeparator : '.';
183 206
184 207 const num = Number(amount) || 0;
185 208
186 - const formatted = new Intl.NumberFormat(undefined, {
209 + // Format with a FIXED 'en-US' base (grouping ',', decimal '.') so the
210 + // replace-with-configured-separators below is deterministic. With the
211 + // viewer's locale (undefined), a European browser already yields
212 + // '1.234,56' and the naive comma/dot replacement SWAPS the separators —
213 + // making the configured decimal separator appear ignored.
214 + const formatted = new Intl.NumberFormat('en-US', {
187 215 minimumFractionDigits: decimals,
188 216 maximumFractionDigits: decimals,
189 217 }).format(num)
190 218 .replace(/,/g, 'TEMP_THOUSAND')
@@ -1581,10 +1609,10 @@
1581 1609 if (!minusBtn || !plusBtn) return;
1582 1610
1583 1611 const updateButtonStates = () => {
1584 1612 const current = parseInt(input.value) || 1;
1585 - const min = parseInt(input.getAttribute('min')) || 1;
1586 - const max = parseInt(input.getAttribute('max')) || 20;
1613 + const min = window.yatraNumOr(input.getAttribute('min'), 1);
1614 + const max = window.yatraNumOr(input.getAttribute('max'), 20);
1587 1615
1588 1616 minusBtn.disabled = current <= min;
1589 1617 plusBtn.disabled = current >= max;
1590 1618 };
@@ -1594,9 +1622,9 @@
1594 1622 e.preventDefault();
1595 1623 e.stopPropagation();
1596 1624
1597 1625 const current = parseInt(input.value) || 1;
1598 - const min = parseInt(input.getAttribute('min')) || 1;
1626 + const min = window.yatraNumOr(input.getAttribute('min'), 1);
1599 1627
1600 1628 if (current > min) {
1601 1629 input.value = current - 1;
1602 1630 updateButtonStates();
@@ -1609,9 +1637,9 @@
1609 1637 e.preventDefault();
1610 1638 e.stopPropagation();
1611 1639
1612 1640 const current = parseInt(input.value) || 1;
1613 - const max = parseInt(input.getAttribute('max')) || 20;
1641 + const max = window.yatraNumOr(input.getAttribute('max'), 20);
1614 1642
1615 1643 if (current < max) {
1616 1644 input.value = current + 1;
1617 1645 updateButtonStates();
@@ -2355,15 +2383,15 @@
2355 2383 const row = travelerSelector.querySelector('.yatra-quantity-row[data-category-id="' + categoryId + '"]');
2356 2384 if (row) {
2357 2385 const cardInput = row.querySelector('input[type="number"]');
2358 2386 if (cardInput) {
2359 - const max = parseInt(cardInput.getAttribute('max')) || 99;
2387 + const max = window.yatraNumOr(cardInput.getAttribute('max'), 99);
2360 2388 cardInput.value = Math.min(value, max);
2361 2389
2362 2390 // Update button states
2363 2391 const minusBtn = row.querySelector('.yatra-quantity-minus');
2364 2392 const plusBtn = row.querySelector('.yatra-quantity-plus');
2365 - const min = parseInt(cardInput.getAttribute('min')) || 0;
2393 + const min = window.yatraNumOr(cardInput.getAttribute('min'), 0);
2366 2394 if (minusBtn) minusBtn.disabled = cardInput.value <= min;
2367 2395 if (plusBtn) plusBtn.disabled = cardInput.value >= max;
2368 2396 }
2369 2397 }
@@ -2374,9 +2402,9 @@
2374 2402
2375 2403 } else if (simpleTravelers && numTravelersInput) {
2376 2404 // Simple pricing - sync total count
2377 2405 const totalCount = parseInt(numTravelersInput.value) || 1;
2378 - const max = parseInt(simpleTravelers.getAttribute('max')) || 99;
2406 + const max = window.yatraNumOr(simpleTravelers.getAttribute('max'), 99);
2379 2407 simpleTravelers.value = Math.min(totalCount, max);
2380 2408
2381 2409 // Update button states
2382 2410 const controls = simpleTravelers.closest('.yatra-quantity-controls-inline');
@@ -2382,9 +2410,9 @@
2382 2410 const controls = simpleTravelers.closest('.yatra-quantity-controls-inline');
2383 2411 if (controls) {
2384 2412 const minusBtn = controls.querySelector('.yatra-quantity-minus');
2385 2413 const plusBtn = controls.querySelector('.yatra-quantity-plus');
2386 - const min = parseInt(simpleTravelers.getAttribute('min')) || 1;
2414 + const min = window.yatraNumOr(simpleTravelers.getAttribute('min'), 1);
2387 2415 if (minusBtn) minusBtn.disabled = simpleTravelers.value <= min;
2388 2416 if (plusBtn) plusBtn.disabled = simpleTravelers.value >= max;
2389 2417 }
2390 2418 }
@@ -2836,16 +2864,28 @@
2836 2864 const enquiriesUrl = baseUrlRaw.includes('/yatra/v1')
2837 2865 ? `${baseUrlRaw}/enquiries`
2838 2866 : `${baseUrlRaw}/yatra/v1/enquiries`;
2839 2867
2840 - fetch(enquiriesUrl, {
2841 - method: 'POST',
2842 - headers: {
2843 - 'Content-Type': 'application/json',
2844 - },
2845 - credentials: 'omit', // public endpoint
2846 - body: JSON.stringify(payload),
2847 - })
2868 + // reCAPTCHA v3: attach a fresh token when the enquiry form is protected.
2869 + const yatraRc = window.yatraRecaptcha;
2870 + const enquiryTokenPromise = (yatraRc && yatraRc.protects && yatraRc.protects('enquiry'))
2871 + ? yatraRc.execute('enquiry')
2872 + : Promise.resolve('');
2873 +
2874 + enquiryTokenPromise
2875 + .then(recaptchaToken => {
2876 + if (recaptchaToken) {
2877 + payload.recaptcha_token = recaptchaToken;
2878 + }
2879 + return fetch(enquiriesUrl, {
2880 + method: 'POST',
2881 + headers: {
2882 + 'Content-Type': 'application/json',
2883 + },
2884 + credentials: 'omit', // public endpoint
2885 + body: JSON.stringify(payload),
2886 + });
2887 + })
2848 2888 .then(response => response.json())
2849 2889 .then(result => {
2850 2890 if (result.success) {
2851 2891 this.showMessage('success', result.message || result.data?.message || __('Thank you for your enquiry! We will get back to you soon.', 'yatra'));
@@ -3668,10 +3708,10 @@
3668 3708 const isMinus = btn.classList.contains('yatra-quantity-minus');
3669 3709 const isPlus = btn.classList.contains('yatra-quantity-plus');
3670 3710
3671 3711 let current = parseInt(input.value) || 0;
3672 - const min = parseInt(input.getAttribute('min')) || 0;
3673 - const max = parseInt(input.getAttribute('max')) || 999;
3712 + const min = window.yatraNumOr(input.getAttribute('min'), 0);
3713 + const max = window.yatraNumOr(input.getAttribute('max'), 999);
3674 3714 let newValue = current;
3675 3715
3676 3716 if (isPlus && current < max) {
3677 3717 newValue = current + 1;
@@ -3743,10 +3783,10 @@
3743 3783 const isMinus = btn.classList.contains('yatra-quantity-minus');
3744 3784 const isPlus = btn.classList.contains('yatra-quantity-plus');
3745 3785
3746 3786 let current = parseInt(input.value) || 1;
3747 - const min = parseInt(input.getAttribute('min')) || 1;
3748 - const max = parseInt(input.getAttribute('max')) || 999;
3787 + const min = window.yatraNumOr(input.getAttribute('min'), 1);
3788 + const max = window.yatraNumOr(input.getAttribute('max'), 999);
3749 3789 let newValue = current;
3750 3790
3751 3791 if (isPlus && current < max) {
3752 3792 newValue = current + 1;
@@ -3856,8 +3896,9 @@
3856 3896 // Check for traveler-based pricing first
3857 3897 const categoryInputs = this.section.querySelectorAll(`.yatra-availability-category[data-item="${itemIndex}"]`);
3858 3898 let totalPrice = 0;
3859 3899 let totalTravelers = 0;
3900 + let totalGroups = 0; // per-group categories: number of group prices charged
3860 3901
3861 3902 if (categoryInputs.length > 0) {
3862 3903 // Traveler-based pricing: sum up each category's price * quantity (respecting pricing mode)
3863 3904 categoryInputs.forEach((input) => {
@@ -3869,11 +3910,11 @@
3869 3910 const maxPax = parseInt((row ? row.getAttribute('data-max-pax') : null) || input.getAttribute('data-max-pax')) || 0;
3870 3911
3871 3912 if (pricingMode === 'per_group') {
3872 3913 if (quantity > 0) {
3873 - totalPrice += (groupOverflow === 'per_block' && maxPax > 0)
3874 - ? price * Math.ceil(quantity / maxPax)
3875 - : price;
3914 + const blocks = (groupOverflow === 'per_block' && maxPax > 0) ? Math.ceil(quantity / maxPax) : 1;
3915 + totalPrice += price * blocks;
3916 + totalGroups += blocks;
3876 3917 }
3877 3918 } else {
3878 3919 totalPrice += quantity * price;
3879 3920 }
@@ -3901,14 +3942,21 @@
3901 3942 } else {
3902 3943 totalAmountElement.textContent = String(finalPrice.toFixed(2));
3903 3944 }
3904 3945
3905 - // Header "per person" must match chargable average (row totals + group discount), not static PHP.
3946 + // Header price must match the chargeable amount (row totals + group discount), not static PHP.
3947 + // Its unit follows the card's price label: "per person" → average per traveller;
3948 + // "per group" → the price per group actually charged (never divided by headcount,
3949 + // which would turn a flat $1,100 group price into "$550 per group" for two travellers).
3906 3950 const cardRoot = this.section.querySelector('.yatra-availability-card[data-item="' + itemIndex + '"]');
3907 - const headerSaleEl = cardRoot ? cardRoot.querySelector('.yatra-card-header-price .yatra-sale-price') : null;
3951 + const headerPriceWrap = cardRoot ? cardRoot.querySelector('.yatra-card-header-price') : null;
3952 + const headerSaleEl = headerPriceWrap ? headerPriceWrap.querySelector('.yatra-sale-price') : null;
3908 3953 if (headerSaleEl) {
3954 + const headerUnit = headerPriceWrap.getAttribute('data-price-unit') || 'per_person';
3909 3955 let perPerson = 0;
3910 - if (totalTravelers > 0) {
3956 + if (headerUnit === 'per_group') {
3957 + perPerson = totalGroups > 0 ? finalPrice / totalGroups : (totalPrice > 0 ? totalPrice : 0);
3958 + } else if (totalTravelers > 0) {
3911 3959 perPerson = finalPrice / totalTravelers;
3912 3960 } else if (totalPrice > 0) {
3913 3961 perPerson = totalPrice;
3914 3962 }