| @@ -35,8 +35,21 @@ | ||
| 35 | 35 | if (typeof window.sprintf !== 'function') window.sprintf = function (fmt) { return fmt; }; |
| 36 | 36 | } |
| 37 | 37 | })(); |
| 38 | 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 | + | |
| 39 | 52 | (function($) { |
| 40 | 53 | 'use strict'; |
| 41 | 54 | |
| 42 | 55 | // API configuration - available throughout the module |
| @@ -64,8 +77,43 @@ | ||
| 64 | 77 | |
| 65 | 78 | $(document).ready(function() { |
| 66 | 79 | const $form = $('#yatra-booking-form'); |
| 67 | 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 | + })(); | |
| 68 | 116 | const isRemainingPayment = Boolean(window.yatraBookingData?.isRemainingPayment) || ($form.data('is-remaining-payment') === 'yes'); |
| 69 | 117 | const remainingAmount = isRemainingPayment |
| 70 | 118 | ? parseFloat(window.yatraBookingData?.remainingAmount ?? $form.data('payment-due')) || 0 |
| 71 | 119 | : 0; |
| @@ -682,10 +730,10 @@ | ||
| 682 | 730 | |
| 683 | 731 | const $btn = $(this); |
| 684 | 732 | const $input = $('#number-of-travelers'); |
| 685 | 733 | let currentValue = parseInt($input.val()) || 1; |
| 686 | - const min = parseInt($input.attr('min')) || 1; | |
| 687 | - 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); | |
| 688 | 736 | |
| 689 | 737 | if ($btn.hasClass('yatra-qty-plus')) { |
| 690 | 738 | if (currentValue < max) { |
| 691 | 739 | currentValue++; |
| @@ -715,10 +763,10 @@ | ||
| 715 | 763 | const $btn = $(this); |
| 716 | 764 | const $row = $btn.closest('.yatra-quantity-row'); |
| 717 | 765 | const $input = $row.find('.yatra-qty-input'); |
| 718 | 766 | let currentValue = parseInt($input.val()) || 0; |
| 719 | - const min = parseInt($input.attr('min')) || 0; | |
| 720 | - 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); | |
| 721 | 769 | |
| 722 | 770 | if ($btn.hasClass('yatra-qty-plus')) { |
| 723 | 771 | if (currentValue < max) { |
| 724 | 772 | currentValue++; |
| @@ -1104,8 +1152,25 @@ | ||
| 1104 | 1152 | /** |
| 1105 | 1153 | * Submit booking to server |
| 1106 | 1154 | */ |
| 1107 | 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) { | |
| 1108 | 1173 | const { base: restBase, isPlain } = getRestBase(); |
| 1109 | 1174 | const siteBase = (window.yatraBookingData?.siteUrl || window.location.origin || '').replace(/\/$/, ''); |
| 1110 | 1175 | let createUrl; |
| 1111 | 1176 | if (isPlain) { |
| @@ -1842,8 +1907,24 @@ | ||
| 1842 | 1907 | password: password, |
| 1843 | 1908 | confirm_password: confirmPassword |
| 1844 | 1909 | }; |
| 1845 | 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 | + | |
| 1846 | 1927 | fetch(apiUrl + '/auth/register', { |
| 1847 | 1928 | method: 'POST', |
| 1848 | 1929 | headers: { |
| 1849 | 1930 | 'Content-Type': 'application/json', |
| @@ -1907,8 +1988,9 @@ | ||
| 1907 | 1988 | $btn.prop('disabled', false); |
| 1908 | 1989 | $btnText.show(); |
| 1909 | 1990 | $btnLoading.hide(); |
| 1910 | 1991 | }); |
| 1992 | + }); // end reCAPTCHA token wrapper | |
| 1911 | 1993 | }); |
| 1912 | 1994 | |
| 1913 | 1995 | // --------------------------------------------------------------------- |
| 1914 | 1996 | // Date field picker upgrade (fast year navigation for Date of Birth) |