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 +100 -44 3.0.6 → 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')
@@ -1259,11 +1287,11 @@
1259 1287 container.appendChild(overlay);
1260 1288 }
1261 1289
1262 1290 formatFileSize(bytes) {
1263 - if (bytes === 0) return '0 Bytes';
1291 + if (bytes === 0) return '0 ' + __('Bytes', 'yatra');
1264 1292 const k = 1024;
1265 - const sizes = ['Bytes', 'KB', 'MB', 'GB'];
1293 + const sizes = [__('Bytes', 'yatra'), 'KB', 'MB', 'GB'];
1266 1294 const i = Math.floor(Math.log(bytes) / Math.log(k));
1267 1295 return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1268 1296 }
1269 1297
@@ -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 }
@@ -2447,13 +2475,17 @@
2447 2475 const input = row.querySelector('input[type="number"]');
2448 2476 const price = parseFloat(row.getAttribute('data-price')) || 0;
2449 2477 const count = parseInt(input?.value) || 0;
2450 2478 const pricingMode = row.getAttribute('data-pricing-mode') || (input ? input.getAttribute('data-pricing-mode') : '') || 'per_person';
2451 -
2479 + const groupOverflow = row.getAttribute('data-group-overflow') || (input ? input.getAttribute('data-group-overflow') : '') || 'block';
2480 + const maxPax = parseInt(row.getAttribute('data-max-pax') || (input ? input.getAttribute('data-max-pax') : '')) || 0;
2481 +
2452 2482 if (pricingMode === 'per_group') {
2453 - // Per group: charge flat price once if any travelers
2483 + // Per group: one flat price, or per block of maxPax when "per_block".
2454 2484 if (count > 0) {
2455 - total += price;
2485 + total += (groupOverflow === 'per_block' && maxPax > 0)
2486 + ? price * Math.ceil(count / maxPax)
2487 + : price;
2456 2488 }
2457 2489 } else {
2458 2490 // Per person: charge per traveler
2459 2491 total += price * count;
@@ -2832,16 +2864,28 @@
2832 2864 const enquiriesUrl = baseUrlRaw.includes('/yatra/v1')
2833 2865 ? `${baseUrlRaw}/enquiries`
2834 2866 : `${baseUrlRaw}/yatra/v1/enquiries`;
2835 2867
2836 - fetch(enquiriesUrl, {
2837 - method: 'POST',
2838 - headers: {
2839 - 'Content-Type': 'application/json',
2840 - },
2841 - credentials: 'omit', // public endpoint
2842 - body: JSON.stringify(payload),
2843 - })
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 + })
2844 2888 .then(response => response.json())
2845 2889 .then(result => {
2846 2890 if (result.success) {
2847 2891 this.showMessage('success', result.message || result.data?.message || __('Thank you for your enquiry! We will get back to you soon.', 'yatra'));
@@ -3534,9 +3578,9 @@
3534 3578 currency: window.yatraBookingData?.currency || 'USD',
3535 3579 num_items: sessionPayload.travelers || 1
3536 3580 });
3537 3581
3538 - console.log('[Facebook Pixel] InitiateCheckout tracked for trip:', sessionPayload.trip_id);
3582 + window.YATRA_DEBUG && console.log('[Facebook Pixel] InitiateCheckout tracked for trip:', sessionPayload.trip_id);
3539 3583 }
3540 3584
3541 3585 // Track Book Now click as begin_checkout (Google Analytics 4)
3542 3586 if (typeof gtag !== 'undefined' && window.yatraAdmin?.googleAnalytics?.measurementId) {
@@ -3554,9 +3598,9 @@
3554 3598 quantity: sessionPayload.travelers || 1
3555 3599 }]
3556 3600 });
3557 3601
3558 - console.log('[Google Analytics] begin_checkout tracked for trip:', sessionPayload.trip_id);
3602 + window.YATRA_DEBUG && console.log('[Google Analytics] begin_checkout tracked for trip:', sessionPayload.trip_id);
3559 3603 }
3560 3604
3561 3605 // Set booking session via REST API
3562 3606 // credentials: 'same-origin' is required to send cookies for session
@@ -3664,10 +3708,10 @@
3664 3708 const isMinus = btn.classList.contains('yatra-quantity-minus');
3665 3709 const isPlus = btn.classList.contains('yatra-quantity-plus');
3666 3710
3667 3711 let current = parseInt(input.value) || 0;
3668 - const min = parseInt(input.getAttribute('min')) || 0;
3669 - 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);
3670 3714 let newValue = current;
3671 3715
3672 3716 if (isPlus && current < max) {
3673 3717 newValue = current + 1;
@@ -3739,10 +3783,10 @@
3739 3783 const isMinus = btn.classList.contains('yatra-quantity-minus');
3740 3784 const isPlus = btn.classList.contains('yatra-quantity-plus');
3741 3785
3742 3786 let current = parseInt(input.value) || 1;
3743 - const min = parseInt(input.getAttribute('min')) || 1;
3744 - 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);
3745 3789 let newValue = current;
3746 3790
3747 3791 if (isPlus && current < max) {
3748 3792 newValue = current + 1;
@@ -3852,8 +3896,9 @@
3852 3896 // Check for traveler-based pricing first
3853 3897 const categoryInputs = this.section.querySelectorAll(`.yatra-availability-category[data-item="${itemIndex}"]`);
3854 3898 let totalPrice = 0;
3855 3899 let totalTravelers = 0;
3900 + let totalGroups = 0; // per-group categories: number of group prices charged
3856 3901
3857 3902 if (categoryInputs.length > 0) {
3858 3903 // Traveler-based pricing: sum up each category's price * quantity (respecting pricing mode)
3859 3904 categoryInputs.forEach((input) => {
@@ -3860,12 +3905,16 @@
3860 3905 const quantity = parseInt(input.value) || 0;
3861 3906 const row = input.closest('.yatra-quantity-row');
3862 3907 const price = row ? parseFloat(row.getAttribute('data-price')) || 0 : 0;
3863 3908 const pricingMode = (row ? row.getAttribute('data-pricing-mode') : null) || input.getAttribute('data-pricing-mode') || 'per_person';
3864 -
3909 + const groupOverflow = (row ? row.getAttribute('data-group-overflow') : null) || input.getAttribute('data-group-overflow') || 'block';
3910 + const maxPax = parseInt((row ? row.getAttribute('data-max-pax') : null) || input.getAttribute('data-max-pax')) || 0;
3911 +
3865 3912 if (pricingMode === 'per_group') {
3866 3913 if (quantity > 0) {
3867 - totalPrice += price;
3914 + const blocks = (groupOverflow === 'per_block' && maxPax > 0) ? Math.ceil(quantity / maxPax) : 1;
3915 + totalPrice += price * blocks;
3916 + totalGroups += blocks;
3868 3917 }
3869 3918 } else {
3870 3919 totalPrice += quantity * price;
3871 3920 }
@@ -3893,14 +3942,21 @@
3893 3942 } else {
3894 3943 totalAmountElement.textContent = String(finalPrice.toFixed(2));
3895 3944 }
3896 3945
3897 - // 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).
3898 3950 const cardRoot = this.section.querySelector('.yatra-availability-card[data-item="' + itemIndex + '"]');
3899 - 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;
3900 3953 if (headerSaleEl) {
3954 + const headerUnit = headerPriceWrap.getAttribute('data-price-unit') || 'per_person';
3901 3955 let perPerson = 0;
3902 - if (totalTravelers > 0) {
3956 + if (headerUnit === 'per_group') {
3957 + perPerson = totalGroups > 0 ? finalPrice / totalGroups : (totalPrice > 0 ? totalPrice : 0);
3958 + } else if (totalTravelers > 0) {
3903 3959 perPerson = finalPrice / totalTravelers;
3904 3960 } else if (totalPrice > 0) {
3905 3961 perPerson = totalPrice;
3906 3962 }
@@ -5187,9 +5243,9 @@
5187 5243 // Extract video ID from URL
5188 5244 let videoId = '';
5189 5245 if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) {
5190 5246 // YouTube video ID extraction
5191 - const matches = videoUrl.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/);
5247 + const matches = videoUrl.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/shorts\/|youtube\.com\/live\/)([^&\n?#\/]+)/);
5192 5248 videoId = matches ? matches[1] : '';
5193 5249 } else if (videoUrl.includes('vimeo.com')) {
5194 5250 // Vimeo video ID extraction
5195 5251 const matches = videoUrl.match(/vimeo\.com\/(?:.*#|.\/videos\/)?([0-9]+)/);