| @@ -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; |
| @@ -625,9 +683,14 @@ | ||
| 625 | 683 | '<p style="font-size: 18px; color: #6b7280; margin-bottom: 8px;">' + message + '</p>' + |
| 626 | 684 | '<p style="font-size: 16px; color: #111827; font-weight: 600;">' + referenceText + '</p>' + |
| 627 | 685 | footerHtml + |
| 628 | 686 | '</div>'); |
| 629 | - | |
| 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(); | |
| 630 | 693 | $form.html($success); |
| 631 | 694 | } |
| 632 | 695 | |
| 633 | 696 | // ===================== |
| @@ -667,10 +730,10 @@ | ||
| 667 | 730 | |
| 668 | 731 | const $btn = $(this); |
| 669 | 732 | const $input = $('#number-of-travelers'); |
| 670 | 733 | let currentValue = parseInt($input.val()) || 1; |
| 671 | - const min = parseInt($input.attr('min')) || 1; | |
| 672 | - 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); | |
| 673 | 736 | |
| 674 | 737 | if ($btn.hasClass('yatra-qty-plus')) { |
| 675 | 738 | if (currentValue < max) { |
| 676 | 739 | currentValue++; |
| @@ -700,10 +763,10 @@ | ||
| 700 | 763 | const $btn = $(this); |
| 701 | 764 | const $row = $btn.closest('.yatra-quantity-row'); |
| 702 | 765 | const $input = $row.find('.yatra-qty-input'); |
| 703 | 766 | let currentValue = parseInt($input.val()) || 0; |
| 704 | - const min = parseInt($input.attr('min')) || 0; | |
| 705 | - 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); | |
| 706 | 769 | |
| 707 | 770 | if ($btn.hasClass('yatra-qty-plus')) { |
| 708 | 771 | if (currentValue < max) { |
| 709 | 772 | currentValue++; |
| @@ -1089,8 +1152,25 @@ | ||
| 1089 | 1152 | /** |
| 1090 | 1153 | * Submit booking to server |
| 1091 | 1154 | */ |
| 1092 | 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) { | |
| 1093 | 1173 | const { base: restBase, isPlain } = getRestBase(); |
| 1094 | 1174 | const siteBase = (window.yatraBookingData?.siteUrl || window.location.origin || '').replace(/\/$/, ''); |
| 1095 | 1175 | let createUrl; |
| 1096 | 1176 | if (isPlain) { |
| @@ -1827,8 +1907,24 @@ | ||
| 1827 | 1907 | password: password, |
| 1828 | 1908 | confirm_password: confirmPassword |
| 1829 | 1909 | }; |
| 1830 | 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 | + | |
| 1831 | 1927 | fetch(apiUrl + '/auth/register', { |
| 1832 | 1928 | method: 'POST', |
| 1833 | 1929 | headers: { |
| 1834 | 1930 | 'Content-Type': 'application/json', |
| @@ -1892,8 +1988,9 @@ | ||
| 1892 | 1988 | $btn.prop('disabled', false); |
| 1893 | 1989 | $btnText.show(); |
| 1894 | 1990 | $btnLoading.hide(); |
| 1895 | 1991 | }); |
| 1992 | + }); // end reCAPTCHA token wrapper | |
| 1896 | 1993 | }); |
| 1897 | 1994 | |
| 1898 | 1995 | // --------------------------------------------------------------------- |
| 1899 | 1996 | // Date field picker upgrade (fast year navigation for Date of Birth) |