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/booking.js +205 -21 3.0.7 → 3.0.16 View file →
@@ -11,22 +11,45 @@
11 11 // runtime is enqueued (it is — `yatra-booking` declares `wp-i18n` as a
12 12 // dependency in FrontendAssetsProvider). Falls back to the source string if
13 13 // wp.i18n is unavailable so the page never breaks on older environments.
14 14 (function () {
15 - if (typeof window.__ === 'function') return;
16 - if (window.wp && window.wp.i18n && typeof window.wp.i18n.__ === 'function') {
17 - window.__ = function (text, domain) { return window.wp.i18n.__(text, domain || 'yatra'); };
18 - window._n = function (s, p, n, domain) { return window.wp.i18n._n(s, p, n, domain || 'yatra'); };
19 - window._x = function (text, ctx, domain) { return window.wp.i18n._x(text, ctx, domain || 'yatra'); };
20 - window.sprintf = window.sprintf || (window.wp.i18n.sprintf || function (fmt) { return fmt; });
15 + var i18n = (window.wp && window.wp.i18n) ? window.wp.i18n : null;
16 + if (i18n && typeof i18n.__ === 'function') {
17 + window.__ = function (text, domain) { return i18n.__(text, domain || 'yatra'); };
18 + window._n = function (s, p, n, domain) { return i18n._n(s, p, n, domain || 'yatra'); };
19 + window._x = function (text, ctx, domain) { return i18n._x(text, ctx, domain || 'yatra'); };
20 + // Always bind WordPress's own sprintf (it substitutes %d/%s/%1$s).
21 + // Do NOT defer to a pre-existing window.sprintf: a theme or other
22 + // plugin can define an incompatible global that leaves placeholders
23 + // unsubstituted (the cause of literal "%d"/"%s" in the UI).
24 + if (typeof i18n.sprintf === 'function') {
25 + window.sprintf = function () { return i18n.sprintf.apply(i18n, arguments); };
26 + } else if (typeof window.sprintf !== 'function') {
27 + window.sprintf = function (fmt) { return fmt; };
28 + }
21 29 } else {
22 - window.__ = function (text) { return text; };
23 - window._n = function (s, p, n) { return n === 1 ? s : p; };
24 - window._x = function (text) { return text; };
25 - window.sprintf = window.sprintf || function (fmt) { return fmt; };
30 + // wp.i18n not present — install passthroughs only if nothing usable
31 + // exists yet (don't clobber a real binding installed earlier).
32 + if (typeof window.__ !== 'function') window.__ = function (text) { return text; };
33 + if (typeof window._n !== 'function') window._n = function (s, p, n) { return n === 1 ? s : p; };
34 + if (typeof window._x !== 'function') window._x = function (text) { return text; };
35 + if (typeof window.sprintf !== 'function') window.sprintf = function (fmt) { return fmt; };
26 36 }
27 37 })();
28 38
39 +// Parse a numeric min/max attribute while preserving a legitimate 0.
40 +// `parseInt(el.getAttribute('max')) || 20` treated max="0" — what a sold-out
41 +// departure renders — as "unset" and fell back to the placeholder ceiling, so
42 +// the guest selector stayed usable and ignored the departure's real capacity.
43 +// Only an absent or non-numeric value may fall back. Installed once and shared
44 +// by every frontend script, so load order does not matter.
45 +if (typeof window.yatraNumOr !== 'function') {
46 + window.yatraNumOr = function (raw, fallback) {
47 + var parsed = parseInt(raw, 10);
48 + return isNaN(parsed) ? fallback : parsed;
49 + };
50 +}
51 +
29 52 (function($) {
30 53 'use strict';
31 54
32 55 // API configuration - available throughout the module
@@ -54,8 +77,43 @@
54 77
55 78 $(document).ready(function() {
56 79 const $form = $('#yatra-booking-form');
57 80 const $submitBtn = $('#yatra-submit-booking');
81 +
82 + // --- Guest "Create an account" reveal ---
83 + // The optional account-creation block (#yatra-account-fields) ships hidden
84 + // with non-required password inputs. Without this handler the guest can
85 + // tick "Create an account" but never see a password field, so either no
86 + // account is created (empty password) or autofill populates the hidden
87 + // field asymmetrically and the server rejects with "Passwords do not
88 + // match" — with no visible field to correct. Reveal + require the fields
89 + // only when opted in; hide + clear + un-require otherwise so a pure-guest
90 + // submit is never blocked and no stale/autofilled password is sent.
91 + (function initCreateAccountToggle() {
92 + const $accountFields = $('#yatra-account-fields');
93 + const $checkbox = $('#create-account');
94 + if (!$accountFields.length || !$checkbox.length) {
95 + return;
96 + }
97 + const $pw = $accountFields.find('#account_password');
98 + const $pwConfirm = $accountFields.find('#account_password_confirm');
99 +
100 + const applyState = function (checked) {
101 + if (checked) {
102 + $accountFields.show();
103 + $pw.add($pwConfirm).attr('required', 'required');
104 + } else {
105 + $accountFields.hide();
106 + $pw.add($pwConfirm).removeAttr('required').val('');
107 + }
108 + };
109 +
110 + // Honor the current state on load (covers browser-restored checkboxes).
111 + applyState($checkbox.is(':checked'));
112 + $checkbox.on('change', function () {
113 + applyState($(this).is(':checked'));
114 + });
115 + })();
58 116 const isRemainingPayment = Boolean(window.yatraBookingData?.isRemainingPayment) || ($form.data('is-remaining-payment') === 'yes');
59 117 const remainingAmount = isRemainingPayment
60 118 ? parseFloat(window.yatraBookingData?.remainingAmount ?? $form.data('payment-due')) || 0
61 119 : 0;
@@ -254,13 +312,31 @@
254 312 .split('.').join('__DECIMAL__')
255 313 .replace(/__THOUSAND__/g, thousandSeparator)
256 314 .replace(/__DECIMAL__/g, decimalSeparator);
257 315
258 - // Position currency based on settings (before or after)
259 - if (currencyPosition === 'right' || currencyPosition === 'after') {
316 + // Placement must match PHP yatra_format_price() exactly: the Settings
317 + // UI uses left / right / left_space / right_space; legacy values are
318 + // before (= left_space) and after (= right_space). Only *_space adds a
319 + // space, so a "left" setting renders "$208.35", not "$ 208.35".
320 + let pos = String(currencyPosition || '').toLowerCase().trim();
321 + if (pos === 'before') {
322 + pos = 'left_space';
323 + } else if (pos === 'after') {
324 + pos = 'right_space';
325 + }
326 + if (['left', 'right', 'left_space', 'right_space'].indexOf(pos) === -1) {
327 + pos = 'left_space';
328 + }
329 +
330 + if (pos === 'right') {
331 + return formattedAmount + symbol;
332 + }
333 + if (pos === 'right_space') {
260 334 return formattedAmount + ' ' + symbol;
261 335 }
262 -
336 + if (pos === 'left') {
337 + return symbol + formattedAmount;
338 + }
263 339 return symbol + ' ' + formattedAmount;
264 340 }
265 341
266 342 /**
@@ -337,9 +413,9 @@
337 413 }
338 414 const flexDue = paymentMethod === 'deposit' || paymentMethod === 'partial';
339 415
340 416 if (isOffline) {
341 - $buttonText.text(__('Complete Booking', 'yatra'));
417 + $buttonText.text(window.yatraBookingData?.i18n?.complete_booking || __('Complete Booking', 'yatra'));
342 418 const showAmount = flexDue ? due : totalSafe;
343 419 if (showAmount > 0) {
344 420 $payAmount.text(formatCurrency(showAmount, currency)).show();
345 421 } else {
@@ -345,9 +421,9 @@
345 421 } else {
346 422 $payAmount.hide();
347 423 }
348 424 } else {
349 - $buttonText.text(__('Pay Now', 'yatra'));
425 + $buttonText.text(window.yatraBookingData?.i18n?.pay_now || __('Pay Now', 'yatra'));
350 426 const payVal = flexDue && due > 0 ? due : totalSafe;
351 427 if (payVal > 0) {
352 428 $payAmount.text(formatCurrency(payVal, currency)).show();
353 429 } else {
@@ -367,9 +443,15 @@
367 443 if (!$firstTraveler.length) return;
368 444
369 445 // Clone the first traveler form
370 446 const $newTraveler = $firstTraveler.clone();
371 -
447 +
448 + // Additional travelers are never the lead, so drop any fields the
449 + // operator marked "Lead traveler only" (data-applies-to="lead").
450 + // Traveler 1 is rendered server-side with these; clones must not
451 + // carry them. ('all'/unset fields stay.)
452 + $newTraveler.find('[data-applies-to="lead"]').remove();
453 +
372 454 // Update index references
373 455 $newTraveler.attr('data-traveler-index', index);
374 456 $newTraveler.find('.yatra-traveler-title').text(
375 457 /* translators: %d: traveler sequence number. */
@@ -601,9 +683,14 @@
601 683 '<p style="font-size: 18px; color: #6b7280; margin-bottom: 8px;">' + message + '</p>' +
602 684 '<p style="font-size: 16px; color: #111827; font-weight: 600;">' + referenceText + '</p>' +
603 685 footerHtml +
604 686 '</div>');
605 -
687 +
688 + // The booking-form header ("Complete Your Booking" + subtitle) is a
689 + // SIBLING of the form, not inside it — replacing the form alone left
690 + // that header showing above the confirmation. Hide it so the success
691 + // screen doesn't repeat the booking-form heading.
692 + $form.siblings('.yatra-booking-header').hide();
606 693 $form.html($success);
607 694 }
608 695
609 696 // =====================
@@ -643,10 +730,10 @@
643 730
644 731 const $btn = $(this);
645 732 const $input = $('#number-of-travelers');
646 733 let currentValue = parseInt($input.val()) || 1;
647 - const min = parseInt($input.attr('min')) || 1;
648 - const max = parseInt($input.attr('max')) || 20;
734 + const min = window.yatraNumOr($input.attr('min'), 1);
735 + const max = window.yatraNumOr($input.attr('max'), 20);
649 736
650 737 if ($btn.hasClass('yatra-qty-plus')) {
651 738 if (currentValue < max) {
652 739 currentValue++;
@@ -676,10 +763,10 @@
676 763 const $btn = $(this);
677 764 const $row = $btn.closest('.yatra-quantity-row');
678 765 const $input = $row.find('.yatra-qty-input');
679 766 let currentValue = parseInt($input.val()) || 0;
680 - const min = parseInt($input.attr('min')) || 0;
681 - const max = parseInt($input.attr('max')) || 20;
767 + const min = window.yatraNumOr($input.attr('min'), 0);
768 + const max = window.yatraNumOr($input.attr('max'), 20);
682 769
683 770 if ($btn.hasClass('yatra-qty-plus')) {
684 771 if (currentValue < max) {
685 772 currentValue++;
@@ -1065,8 +1152,25 @@
1065 1152 /**
1066 1153 * Submit booking to server
1067 1154 */
1068 1155 function submitBookingToServer(bookingData, originalBtnHtml) {
1156 + // reCAPTCHA v3: attach a token when the booking form is protected,
1157 + // then run the real submit. Wraps every call path (direct submit and
1158 + // the gateway `proceedWithSubmission` hook). No-op unless the operator
1159 + // opted booking into reCAPTCHA (off by default), so payment flows are
1160 + // unchanged otherwise.
1161 + const yatraRc = window.yatraRecaptcha;
1162 + if (yatraRc && yatraRc.protects && yatraRc.protects('booking') && !bookingData.recaptcha_token) {
1163 + yatraRc.execute('booking').then(function (recaptchaToken) {
1164 + if (recaptchaToken) { bookingData.recaptcha_token = recaptchaToken; }
1165 + submitBookingToServerInner(bookingData, originalBtnHtml);
1166 + });
1167 + return;
1168 + }
1169 + submitBookingToServerInner(bookingData, originalBtnHtml);
1170 + }
1171 +
1172 + function submitBookingToServerInner(bookingData, originalBtnHtml) {
1069 1173 const { base: restBase, isPlain } = getRestBase();
1070 1174 const siteBase = (window.yatraBookingData?.siteUrl || window.location.origin || '').replace(/\/$/, '');
1071 1175 let createUrl;
1072 1176 if (isPlain) {
@@ -1803,8 +1907,24 @@
1803 1907 password: password,
1804 1908 confirm_password: confirmPassword
1805 1909 };
1806 1910
1911 + // reCAPTCHA v3: this form posts straight to /auth/register, so it must
1912 + // carry its own token when the operator protects registration — the
1913 + // server rejects an empty one outright, whatever the score threshold.
1914 + // Resolves to '' when reCAPTCHA is off or unavailable, so the normal
1915 + // (unprotected) flow is completely unchanged.
1916 + const yatraRcReg = window.yatraRecaptcha;
1917 + const regTokenPromise = (yatraRcReg && typeof yatraRcReg.protects === 'function'
1918 + && yatraRcReg.protects('registration') && typeof yatraRcReg.execute === 'function')
1919 + ? yatraRcReg.execute('registration')
1920 + : Promise.resolve('');
1921 +
1922 + regTokenPromise.catch(function () { return ''; }).then(function (regToken) {
1923 + if (regToken) {
1924 + registerData.recaptcha_token = regToken;
1925 + }
1926 +
1807 1927 fetch(apiUrl + '/auth/register', {
1808 1928 method: 'POST',
1809 1929 headers: {
1810 1930 'Content-Type': 'application/json',
@@ -1868,8 +1988,72 @@
1868 1988 $btn.prop('disabled', false);
1869 1989 $btnText.show();
1870 1990 $btnLoading.hide();
1871 1991 });
1992 + }); // end reCAPTCHA token wrapper
1872 1993 });
1994 +
1995 + // ---------------------------------------------------------------------
1996 + // Date field picker upgrade (fast year navigation for Date of Birth)
1997 + // ---------------------------------------------------------------------
1998 + // A native <input type="date"> popup only steps one month at a time, so
1999 + // reaching a birth year like 1990 takes many clicks. Where flatpickr is
2000 + // available (and on non-touch devices, since mobile native pickers already
2001 + // have good year wheels) we upgrade `.yatra-js-date` inputs to a flatpickr
2002 + // whose header year is directly typeable/scrollable. Fully guarded: with no
2003 + // flatpickr, or on mobile, the native input is left untouched and working.
2004 + var yatraIsMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent || '');
2005 +
2006 + function yatraUpgradeDateField(input) {
2007 + if (!input || input.dataset.yatraFp === '1') return;
2008 + if (typeof window.flatpickr === 'undefined') return;
2009 + if (yatraIsMobile) return; // native mobile date wheels already navigate years well
2010 + input.dataset.yatraFp = '1';
2011 + var isDob = input.getAttribute('data-yatra-dob') === '1';
2012 + // Turn off the native date UI so flatpickr is the only picker; the
2013 + // submitted value stays ISO (Y-m-d) so FormData/back-end are unchanged.
2014 + try { input.type = 'text'; } catch (e) {}
2015 + window.flatpickr(input, {
2016 + dateFormat: 'Y-m-d',
2017 + allowInput: true,
2018 + maxDate: isDob ? 'today' : null,
2019 + defaultDate: input.value || null
2020 + });
2021 + }
2022 +
2023 + function yatraUpgradeDateFields(root) {
2024 + var scope = (root && root.querySelectorAll) ? root : document;
2025 + var list = scope.querySelectorAll('input.yatra-js-date');
2026 + for (var i = 0; i < list.length; i++) {
2027 + yatraUpgradeDateField(list[i]);
2028 + }
2029 + }
2030 +
2031 + function yatraInitDatePickers() {
2032 + yatraUpgradeDateFields(document);
2033 + // The traveler form is injected via AJAX, so watch for inserted inputs.
2034 + if (typeof MutationObserver === 'undefined' || !document.body) return;
2035 + var obs = new MutationObserver(function (mutations) {
2036 + for (var m = 0; m < mutations.length; m++) {
2037 + var added = mutations[m].addedNodes;
2038 + for (var n = 0; n < added.length; n++) {
2039 + var node = added[n];
2040 + if (!node || node.nodeType !== 1) continue;
2041 + if (node.matches && node.matches('input.yatra-js-date')) {
2042 + yatraUpgradeDateField(node);
2043 + } else if (node.querySelectorAll) {
2044 + yatraUpgradeDateFields(node);
2045 + }
2046 + }
2047 + }
2048 + });
2049 + obs.observe(document.body, { childList: true, subtree: true });
2050 + }
2051 +
2052 + if (document.readyState === 'loading') {
2053 + document.addEventListener('DOMContentLoaded', yatraInitDatePickers);
2054 + } else {
2055 + yatraInitDatePickers();
2056 + }
1873 2057
1874 2058 // End of document ready
1875 2059 })(jQuery);