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 +396 -97 3.0.4 → 3.0.16 View file →
@@ -1,13 +1,55 @@
1 1 /**
2 2 * Booking Page JavaScript
3 - *
3 + *
4 4 * Uses FormData API to automatically collect all form fields
5 5 * and submit via REST API
6 - *
6 + *
7 7 * @package Yatra
8 8 */
9 9
10 +// Translation helper. Resolves through `wp.i18n.__` when WordPress's i18n
11 +// runtime is enqueued (it is — `yatra-booking` declares `wp-i18n` as a
12 +// dependency in FrontendAssetsProvider). Falls back to the source string if
13 +// wp.i18n is unavailable so the page never breaks on older environments.
14 +(function () {
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 + }
29 + } else {
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; };
36 + }
37 +})();
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 +
10 52 (function($) {
11 53 'use strict';
12 54
13 55 // API configuration - available throughout the module
@@ -35,8 +77,43 @@
35 77
36 78 $(document).ready(function() {
37 79 const $form = $('#yatra-booking-form');
38 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 + })();
39 116 const isRemainingPayment = Boolean(window.yatraBookingData?.isRemainingPayment) || ($form.data('is-remaining-payment') === 'yes');
40 117 const remainingAmount = isRemainingPayment
41 118 ? parseFloat(window.yatraBookingData?.remainingAmount ?? $form.data('payment-due')) || 0
42 119 : 0;
@@ -63,30 +140,35 @@
63 140 // Country options for dynamically added travelers
64 141 const countryOptions = generateCountryOptions();
65 142
66 143 /**
67 - * Generate country select options HTML
144 + * Generate country select options HTML.
145 + *
146 + * Each English country name is wrapped in `__()` so it's
147 + * extractable and translatable per WordPress conventions.
148 + * Keys (ISO 3166-1 alpha-2 codes) are untranslated, as they
149 + * should be — they're identifiers, not user-facing text.
68 150 */
69 151 function generateCountryOptions() {
70 152 const countries = {
71 - 'AF': 'Afghanistan', 'AL': 'Albania', 'DZ': 'Algeria', 'AR': 'Argentina',
72 - 'AU': 'Australia', 'AT': 'Austria', 'BD': 'Bangladesh', 'BE': 'Belgium',
73 - 'BR': 'Brazil', 'BT': 'Bhutan', 'CA': 'Canada', 'CN': 'China',
74 - 'CO': 'Colombia', 'CZ': 'Czech Republic', 'DK': 'Denmark', 'EG': 'Egypt',
75 - 'FI': 'Finland', 'FR': 'France', 'DE': 'Germany', 'GR': 'Greece',
76 - 'HK': 'Hong Kong', 'HU': 'Hungary', 'IS': 'Iceland', 'IN': 'India',
77 - 'ID': 'Indonesia', 'IE': 'Ireland', 'IL': 'Israel', 'IT': 'Italy',
78 - 'JP': 'Japan', 'KE': 'Kenya', 'KR': 'South Korea', 'MY': 'Malaysia',
79 - 'MV': 'Maldives', 'MX': 'Mexico', 'NL': 'Netherlands', 'NZ': 'New Zealand',
80 - 'NP': 'Nepal', 'NO': 'Norway', 'PK': 'Pakistan', 'PE': 'Peru',
81 - 'PH': 'Philippines', 'PL': 'Poland', 'PT': 'Portugal', 'RO': 'Romania',
82 - 'RU': 'Russia', 'SA': 'Saudi Arabia', 'SG': 'Singapore', 'ZA': 'South Africa',
83 - 'ES': 'Spain', 'LK': 'Sri Lanka', 'SE': 'Sweden', 'CH': 'Switzerland',
84 - 'TW': 'Taiwan', 'TH': 'Thailand', 'TR': 'Turkey', 'AE': 'United Arab Emirates',
85 - 'GB': 'United Kingdom', 'US': 'United States', 'VN': 'Vietnam'
153 + 'AF': __('Afghanistan', 'yatra'), 'AL': __('Albania', 'yatra'), 'DZ': __('Algeria', 'yatra'), 'AR': __('Argentina', 'yatra'),
154 + 'AU': __('Australia', 'yatra'), 'AT': __('Austria', 'yatra'), 'BD': __('Bangladesh', 'yatra'), 'BE': __('Belgium', 'yatra'),
155 + 'BR': __('Brazil', 'yatra'), 'BT': __('Bhutan', 'yatra'), 'CA': __('Canada', 'yatra'), 'CN': __('China', 'yatra'),
156 + 'CO': __('Colombia', 'yatra'), 'CZ': __('Czech Republic', 'yatra'), 'DK': __('Denmark', 'yatra'), 'EG': __('Egypt', 'yatra'),
157 + 'FI': __('Finland', 'yatra'), 'FR': __('France', 'yatra'), 'DE': __('Germany', 'yatra'), 'GR': __('Greece', 'yatra'),
158 + 'HK': __('Hong Kong', 'yatra'), 'HU': __('Hungary', 'yatra'), 'IS': __('Iceland', 'yatra'), 'IN': __('India', 'yatra'),
159 + 'ID': __('Indonesia', 'yatra'), 'IE': __('Ireland', 'yatra'), 'IL': __('Israel', 'yatra'), 'IT': __('Italy', 'yatra'),
160 + 'JP': __('Japan', 'yatra'), 'KE': __('Kenya', 'yatra'), 'KR': __('South Korea', 'yatra'), 'MY': __('Malaysia', 'yatra'),
161 + 'MV': __('Maldives', 'yatra'), 'MX': __('Mexico', 'yatra'), 'NL': __('Netherlands', 'yatra'), 'NZ': __('New Zealand', 'yatra'),
162 + 'NP': __('Nepal', 'yatra'), 'NO': __('Norway', 'yatra'), 'PK': __('Pakistan', 'yatra'), 'PE': __('Peru', 'yatra'),
163 + 'PH': __('Philippines', 'yatra'), 'PL': __('Poland', 'yatra'), 'PT': __('Portugal', 'yatra'), 'RO': __('Romania', 'yatra'),
164 + 'RU': __('Russia', 'yatra'), 'SA': __('Saudi Arabia', 'yatra'), 'SG': __('Singapore', 'yatra'), 'ZA': __('South Africa', 'yatra'),
165 + 'ES': __('Spain', 'yatra'), 'LK': __('Sri Lanka', 'yatra'), 'SE': __('Sweden', 'yatra'), 'CH': __('Switzerland', 'yatra'),
166 + 'TW': __('Taiwan', 'yatra'), 'TH': __('Thailand', 'yatra'), 'TR': __('Turkey', 'yatra'), 'AE': __('United Arab Emirates', 'yatra'),
167 + 'GB': __('United Kingdom', 'yatra'), 'US': __('United States', 'yatra'), 'VN': __('Vietnam', 'yatra')
86 168 };
87 -
88 - let options = '<option value="">Select Country</option>';
169 +
170 + let options = '<option value="">' + __('Select Country', 'yatra') + '</option>';
89 171 for (const [code, name] of Object.entries(countries)) {
90 172 options += `<option value="${code}">${name}</option>`;
91 173 }
92 174 return options;
@@ -129,14 +211,20 @@
129 211 let total = 0;
130 212 $('.yatra-qty-input[data-category-id]').each(function() {
131 213 const count = parseInt($(this).val()) || 0;
132 214 const price = parseFloat($(this).data('price')) || 0;
133 - const pricingMode = $(this).data('pricing-mode') || $(this).closest('.yatra-quantity-row').data('pricing-mode') || 'per_person';
134 -
215 + const $row = $(this).closest('.yatra-quantity-row');
216 + const pricingMode = $(this).data('pricing-mode') || $row.data('pricing-mode') || 'per_person';
217 + const groupOverflow = $(this).data('group-overflow') || $row.data('group-overflow') || 'block';
218 + const maxPax = parseInt($(this).data('max-pax') || $row.data('max-pax')) || 0;
219 +
135 220 if (pricingMode === 'per_group') {
136 - // Per group: charge flat price once if any travelers in this category
221 + // Per group: one flat price for the whole group, or one flat
222 + // price per block of maxPax people when overflow is "per_block".
137 223 if (count > 0) {
138 - total += price;
224 + total += (groupOverflow === 'per_block' && maxPax > 0)
225 + ? price * Math.ceil(count / maxPax)
226 + : price;
139 227 }
140 228 } else {
141 229 // Per person: charge per traveler
142 230 total += count * price;
@@ -158,12 +246,17 @@
158 246 $('.yatra-qty-input[data-category-id]').each(function() {
159 247 const count = parseInt($(this).val()) || 0;
160 248 const categoryLabel = $(this).closest('.yatra-quantity-row').find('.yatra-quantity-title').text();
161 249 if (count > 0) {
162 - parts.push(categoryLabel + ' x ' + count);
250 + parts.push(sprintf(
251 + /* translators: 1: traveler category label, 2: count. */
252 + __('%1$s x %2$d', 'yatra'),
253 + categoryLabel,
254 + count
255 + ));
163 256 }
164 257 });
165 - const displayText = parts.length > 0 ? parts.join(', ') : 'Select travelers';
258 + const displayText = parts.length > 0 ? parts.join(', ') : __('Select travelers', 'yatra');
166 259 $('#yatra-travelers-display').text(displayText);
167 260 }
168 261
169 262 /**
@@ -169,10 +262,13 @@
169 262 /**
170 263 * Update display text for regular pricing
171 264 */
172 265 function updateRegularTravelersDisplayText(count) {
173 - const label = count === 1 ? 'traveler' : 'travelers';
174 - $('#yatra-travelers-display-regular').text(count + ' ' + label);
266 + $('#yatra-travelers-display-regular').text(sprintf(
267 + /* translators: %d: number of travelers on this booking. */
268 + _n('%d traveler', '%d travelers', count, 'yatra'),
269 + count
270 + ));
175 271 }
176 272
177 273
178 274 /**
@@ -216,13 +312,31 @@
216 312 .split('.').join('__DECIMAL__')
217 313 .replace(/__THOUSAND__/g, thousandSeparator)
218 314 .replace(/__DECIMAL__/g, decimalSeparator);
219 315
220 - // Position currency based on settings (before or after)
221 - 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') {
222 334 return formattedAmount + ' ' + symbol;
223 335 }
224 -
336 + if (pos === 'left') {
337 + return symbol + formattedAmount;
338 + }
225 339 return symbol + ' ' + formattedAmount;
226 340 }
227 341
228 342 /**
@@ -299,9 +413,9 @@
299 413 }
300 414 const flexDue = paymentMethod === 'deposit' || paymentMethod === 'partial';
301 415
302 416 if (isOffline) {
303 - $buttonText.text('Complete Booking');
417 + $buttonText.text(window.yatraBookingData?.i18n?.complete_booking || __('Complete Booking', 'yatra'));
304 418 const showAmount = flexDue ? due : totalSafe;
305 419 if (showAmount > 0) {
306 420 $payAmount.text(formatCurrency(showAmount, currency)).show();
307 421 } else {
@@ -307,9 +421,9 @@
307 421 } else {
308 422 $payAmount.hide();
309 423 }
310 424 } else {
311 - $buttonText.text('Pay Now');
425 + $buttonText.text(window.yatraBookingData?.i18n?.pay_now || __('Pay Now', 'yatra'));
312 426 const payVal = flexDue && due > 0 ? due : totalSafe;
313 427 if (payVal > 0) {
314 428 $payAmount.text(formatCurrency(payVal, currency)).show();
315 429 } else {
@@ -329,16 +443,30 @@
329 443 if (!$firstTraveler.length) return;
330 444
331 445 // Clone the first traveler form
332 446 const $newTraveler = $firstTraveler.clone();
333 -
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 +
334 454 // Update index references
335 455 $newTraveler.attr('data-traveler-index', index);
336 - $newTraveler.find('.yatra-traveler-title').text('Traveler ' + index);
456 + $newTraveler.find('.yatra-traveler-title').text(
457 + /* translators: %d: traveler sequence number. */
458 + sprintf(__('Traveler %d', 'yatra'), index)
459 + );
337 460
338 - // Add "Additional traveler" note
461 + // Add "Additional traveler" note. The class wrapper is
462 + // markup (not translatable); only the visible label runs
463 + // through __() so translators see a clean msgid in the
464 + // pot rather than HTML noise.
339 465 if (!$newTraveler.find('.yatra-traveler-note').length) {
340 - $newTraveler.find('.yatra-traveler-header').append('<span class="yatra-traveler-note">Additional traveler</span>');
466 + $newTraveler.find('.yatra-traveler-header').append(
467 + '<span class="yatra-traveler-note">' + __('Additional traveler', 'yatra') + '</span>'
468 + );
341 469 }
342 470
343 471 // Update all field IDs and names
344 472 $newTraveler.find('input, select, textarea').each(function() {
@@ -452,15 +580,30 @@
452 580
453 581 // Validate all required fields
454 582 $form.find('input[required], select[required], textarea[required]').each(function() {
455 583 const $field = $(this);
456 - const value = $field.val();
457 -
458 - if (!value || value.trim() === '') {
584 + const type = ($field.attr('type') || '').toLowerCase();
585 +
586 + // Checkbox/radio: .val() returns the value attribute regardless of
587 + // checked state, so an unchecked required box would wrongly pass.
588 + // Test the checked state instead.
589 + let invalid;
590 + if (type === 'checkbox' || type === 'radio') {
591 + invalid = !$field.is(':checked');
592 + } else {
593 + const value = $field.val();
594 + invalid = !value || value.trim() === '';
595 + }
596 +
597 + if (invalid) {
459 598 isValid = false;
460 599 $field.addClass('error');
461 600 const label = $field.closest('.yatra-form-group').find('label').text().replace('*', '').trim();
462 - errors.push(label + ' is required');
601 + errors.push(sprintf(
602 + /* translators: %s: form field label. */
603 + __('%s is required', 'yatra'),
604 + label
605 + ));
463 606 }
464 607 });
465 608
466 609 // Validate email format
@@ -470,9 +613,9 @@
470 613 const value = $field.val();
471 614 if (value && !isValidEmail(value)) {
472 615 isValid = false;
473 616 $field.addClass('error');
474 - errors.push('Please enter a valid email address');
617 + errors.push(__('Please enter a valid email address', 'yatra'));
475 618 }
476 619 });
477 620
478 621 // Validate terms checkbox
@@ -477,15 +620,15 @@
477 620
478 621 // Validate terms checkbox
479 622 if (!$('input[name="accept_terms"]').is(':checked')) {
480 623 isValid = false;
481 - errors.push('Please accept the Terms and Conditions');
624 + errors.push(__('Please accept the Terms and Conditions', 'yatra'));
482 625 }
483 626
484 627 // Validate privacy checkbox
485 628 if (!$('input[name="accept_privacy"]').is(':checked')) {
486 629 isValid = false;
487 - errors.push('Please accept the Privacy Policy');
630 + errors.push(__('Please accept the Privacy Policy', 'yatra'));
488 631 }
489 632
490 633 if (!isValid && errors.length > 0) {
491 634 showFormError(errors[0]);
@@ -521,22 +664,33 @@
521 664 * @param {{ heading?: string, footerHtml?: string, iconColor?: string }|undefined} opts
522 665 */
523 666 function showSuccessMessage(message, reference, opts) {
524 667 opts = opts || {};
525 - const heading = opts.heading || 'Booking Confirmed!';
668 + const heading = opts.heading || __('Booking Confirmed!', 'yatra');
526 669 const iconColor = opts.iconColor || '#22c55e';
670 + const confirmationEmailText = __('A confirmation email has been sent to your email address.', 'yatra');
527 671 const footerHtml = opts.footerHtml !== undefined
528 672 ? opts.footerHtml
529 - : '<p style="margin-top: 24px; color: #6b7280;">A confirmation email has been sent to your email address.</p>';
673 + : '<p style="margin-top: 24px; color: #6b7280;">' + confirmationEmailText + '</p>';
674 + const referenceText = sprintf(
675 + /* translators: %s: booking reference number. */
676 + __('Reference: %s', 'yatra'),
677 + reference
678 + );
530 679 const $success = $('<div class="yatra-booking-success" style="text-align: center; padding: 60px 20px;">' +
531 680 '<svg style="width: 80px; height: 80px; color: ' + iconColor + '; margin-bottom: 24px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
532 681 '<circle cx="12" cy="12" r="10"></circle><polyline points="9,12 12,15 16,10"></polyline></svg>' +
533 682 '<h2 style="font-size: 28px; margin-bottom: 12px; color: #111827;">' + heading + '</h2>' +
534 683 '<p style="font-size: 18px; color: #6b7280; margin-bottom: 8px;">' + message + '</p>' +
535 - '<p style="font-size: 16px; color: #111827; font-weight: 600;">Reference: ' + reference + '</p>' +
684 + '<p style="font-size: 16px; color: #111827; font-weight: 600;">' + referenceText + '</p>' +
536 685 footerHtml +
537 686 '</div>');
538 -
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();
539 693 $form.html($success);
540 694 }
541 695
542 696 // =====================
@@ -576,10 +730,10 @@
576 730
577 731 const $btn = $(this);
578 732 const $input = $('#number-of-travelers');
579 733 let currentValue = parseInt($input.val()) || 1;
580 - const min = parseInt($input.attr('min')) || 1;
581 - 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);
582 736
583 737 if ($btn.hasClass('yatra-qty-plus')) {
584 738 if (currentValue < max) {
585 739 currentValue++;
@@ -609,10 +763,10 @@
609 763 const $btn = $(this);
610 764 const $row = $btn.closest('.yatra-quantity-row');
611 765 const $input = $row.find('.yatra-qty-input');
612 766 let currentValue = parseInt($input.val()) || 0;
613 - const min = parseInt($input.attr('min')) || 0;
614 - 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);
615 769
616 770 if ($btn.hasClass('yatra-qty-plus')) {
617 771 if (currentValue < max) {
618 772 currentValue++;
@@ -742,15 +896,15 @@
742 896 const $gatewayInfo = $('#yatra-gateway-info');
743 897 const $gatewayDetails = $('#yatra-gateway-details');
744 898
745 899 const gatewayMessages = {
746 - 'pay_later': '<p>Your booking will be reserved. Full payment is required before the trip date.</p>',
747 - 'bank_transfer': '<p>After completing your booking, you will receive bank details via email. Your booking will be confirmed once payment is received.</p>',
748 - 'stripe': '<p>You will be securely redirected to complete your payment with credit or debit card.</p>',
749 - 'paypal': '<p>You will be redirected to PayPal to complete your payment securely.</p>',
750 - 'razorpay': '<p>You will be redirected to Razorpay to complete your payment.</p>',
751 - 'esewa': '<p>You will be redirected to eSewa to complete your payment.</p>',
752 - 'khalti': '<p>You will be redirected to Khalti to complete your payment.</p>',
900 + 'pay_later': '<p>' + __('Your booking will be reserved. Full payment is required before the trip date.', 'yatra') + '</p>',
901 + 'bank_transfer': '<p>' + __('After completing your booking, you will receive bank details via email. Your booking will be confirmed once payment is received.', 'yatra') + '</p>',
902 + 'stripe': '<p>' + __('You will be securely redirected to complete your payment with credit or debit card.', 'yatra') + '</p>',
903 + 'paypal': '<p>' + __('You will be redirected to PayPal to complete your payment securely.', 'yatra') + '</p>',
904 + 'razorpay': '<p>' + __('You will be redirected to Razorpay to complete your payment.', 'yatra') + '</p>',
905 + 'esewa': '<p>' + __('You will be redirected to eSewa to complete your payment.', 'yatra') + '</p>',
906 + 'khalti': '<p>' + __('You will be redirected to Khalti to complete your payment.', 'yatra') + '</p>',
753 907 };
754 908
755 909 if (gatewayMessages[gateway] && $gatewayDetails.length) {
756 910 $gatewayDetails.html(gatewayMessages[gateway]);
@@ -805,29 +959,56 @@
805 959 /**
806 960 * Handle payment response from server
807 961 */
808 962 function handlePaymentResponse(response, originalBtnHtml) {
809 -
810 -
811 -
963 +
964 +
965 +
812 966 if (!response.success) {
813 - const errMsg = response.message || 'An error occurred. Please try again.';
967 + const errMsg = response.message || __('An error occurred. Please try again.', 'yatra');
814 968 showFormError(errMsg);
815 969 $submitBtn.prop('disabled', false).html(originalBtnHtml);
816 970 return;
817 971 }
818 -
972 +
973 + // Guest email verification path — booking is held in
974 + // pending_verification until the customer clicks the
975 + // magic link in their inbox. Render a "check your email"
976 + // success screen and DO NOT redirect to payment.
977 + if (response.code === 'email_verification_required') {
978 + const data = response.data || {};
979 + const reference = data.reference || __('N/A', 'yatra');
980 + const email = data.email || '';
981 + showSuccessMessage(
982 + response.message || __("We've sent a verification email. Click the link to complete your booking.", 'yatra'),
983 + reference,
984 + {
985 + heading: __('Check your email', 'yatra'),
986 + iconColor: '#2563eb',
987 + footerHtml:
988 + '<p style="margin-top: 20px; color: #374151;">' +
989 + __('We sent a verification link to:', 'yatra') +
990 + ' <strong>' + (email || __('your email', 'yatra')) + '</strong>' +
991 + '</p>' +
992 + '<p style="margin-top: 12px; color: #6b7280; font-size: 14px;">' +
993 + __("Didn't see it? Check your spam folder. The link expires in 48 hours.", 'yatra') +
994 + '</p>'
995 + }
996 + );
997 + return;
998 + }
999 +
819 1000 const data = response.data || {};
820 1001
821 1002 if (data.waitlist) {
822 - const reference = data.reference || 'N/A';
1003 + const reference = data.reference || __('N/A', 'yatra');
823 1004 showSuccessMessage(
824 - response.message || "You're on the waitlist for this departure.",
1005 + response.message || __("You're on the waitlist for this departure.", 'yatra'),
825 1006 reference,
826 1007 {
827 - heading: "You're on the waitlist",
1008 + heading: __("You're on the waitlist", 'yatra'),
828 1009 iconColor: '#ca8a04',
829 - footerHtml: '<p style="margin-top: 24px; color: #6b7280;">We will contact you if a space opens up.</p>'
1010 + footerHtml: '<p style="margin-top: 24px; color: #6b7280;">' + __('We will contact you if a space opens up.', 'yatra') + '</p>'
830 1011 }
831 1012 );
832 1013 return;
833 1014 }
@@ -865,9 +1046,9 @@
865 1046 document.removeEventListener('yatra_payment_failed', onFailed);
866 1047 document.removeEventListener('yatra_payment_cancelled', onCancelled);
867 1048 };
868 1049 const onFailed = (e) => {
869 - showFormError(e.detail?.error || 'Payment failed. Please try again.');
1050 + showFormError(e.detail?.error || __('Payment failed. Please try again.', 'yatra'));
870 1051 $submitBtn.prop('disabled', false).html(originalBtnHtml);
871 1052 cleanup();
872 1053 };
873 1054 const onCancelled = () => {
@@ -879,10 +1060,10 @@
879 1060 return;
880 1061 }
881 1062
882 1063 // Default: show success message
883 - const reference = data.reference || 'N/A';
884 - showSuccessMessage(response.message || 'Success!', reference);
1064 + const reference = data.reference || __('N/A', 'yatra');
1065 + showSuccessMessage(response.message || __('Success!', 'yatra'), reference);
885 1066 }
886 1067
887 1068 // Form submission - using FormData API
888 1069 $form.on('submit', function(e) {
@@ -971,8 +1152,25 @@
971 1152 /**
972 1153 * Submit booking to server
973 1154 */
974 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) {
975 1173 const { base: restBase, isPlain } = getRestBase();
976 1174 const siteBase = (window.yatraBookingData?.siteUrl || window.location.origin || '').replace(/\/$/, '');
977 1175 let createUrl;
978 1176 if (isPlain) {
@@ -986,16 +1184,28 @@
986 1184 // Show loading state
987 1185 $submitBtn.prop('disabled', true).html(
988 1186 '<svg class="animate-spin" style="display: inline-block; width: 20px; height: 20px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
989 1187 '<circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"></circle></svg>' +
990 - '<span>Processing...</span>'
1188 + '<span>' + __('Processing...', 'yatra') + '</span>'
991 1189 );
992 1190
1191 + // CSRF protection — the booking REST endpoint's
1192 + // permission_callback intentionally bypasses WP's default
1193 + // cookie/nonce check (so guests can hit it at all). The
1194 + // server validates this booking-scoped action nonce
1195 + // instead. Falls back to the hidden form field on the
1196 + // booking page when the localized var isn't present (e.g.
1197 + // older cached scripts).
1198 + const bookingNonce = window.yatraBookingData?.bookingNonce
1199 + || $('input[name="yatra_booking_nonce"]').val()
1200 + || '';
1201 +
993 1202 fetch(createUrl, {
994 1203 method: 'POST',
995 1204 headers: {
996 1205 'Content-Type': 'application/json',
997 - 'X-WP-Nonce': nonce
1206 + 'X-WP-Nonce': nonce,
1207 + 'X-Yatra-Booking-Nonce': bookingNonce
998 1208 },
999 1209 credentials: 'same-origin',
1000 1210 body: JSON.stringify(bookingData)
1001 1211 })
@@ -1002,9 +1212,9 @@
1002 1212 .then(response => response.json())
1003 1213 .then(response => handlePaymentResponse(response, originalBtnHtml))
1004 1214 .catch(error => {
1005 1215 console.error('Error:', error);
1006 - showFormError('An error occurred. Please try again.');
1216 + showFormError(__('An error occurred. Please try again.', 'yatra'));
1007 1217 $submitBtn.prop('disabled', false).html(originalBtnHtml);
1008 1218 });
1009 1219 }
1010 1220
@@ -1047,14 +1257,14 @@
1047 1257 const $btn = $('#yatra-apply-coupon');
1048 1258 const $message = $('#yatra-coupon-message');
1049 1259
1050 1260 if (!code) {
1051 - showCouponMessage('Please enter a coupon code.', 'error');
1261 + showCouponMessage(__('Please enter a coupon code.', 'yatra'), 'error');
1052 1262 return;
1053 1263 }
1054 1264
1055 1265 // Show loading state
1056 - $btn.prop('disabled', true).text('Applying...');
1266 + $btn.prop('disabled', true).text(__('Applying...', 'yatra'));
1057 1267 $message.hide();
1058 1268
1059 1269 // Carry booking_token so the REST endpoint can rehydrate the
1060 1270 // session in contexts where PHPSESSID isn't passed through —
@@ -1075,9 +1285,9 @@
1075 1285 body: JSON.stringify(couponPayload)
1076 1286 })
1077 1287 .then(response => response.json())
1078 1288 .then(response => {
1079 - $btn.prop('disabled', false).text('Apply');
1289 + $btn.prop('disabled', false).text(__('Apply', 'yatra'));
1080 1290
1081 1291 if (response.success) {
1082 1292 appliedCoupon = response.data;
1083 1293
@@ -1095,15 +1305,15 @@
1095 1305 updatePriceWithDiscount(response.data);
1096 1306
1097 1307 showCouponMessage(response.message, 'success');
1098 1308 } else {
1099 - showCouponMessage(response.message || 'Invalid coupon code.', 'error');
1309 + showCouponMessage(response.message || __('Invalid coupon code.', 'yatra'), 'error');
1100 1310 }
1101 1311 })
1102 1312 .catch(error => {
1103 1313 console.error('Coupon error:', error);
1104 - $btn.prop('disabled', false).text('Apply');
1105 - showCouponMessage('An error occurred. Please try again.', 'error');
1314 + $btn.prop('disabled', false).text(__('Apply', 'yatra'));
1315 + showCouponMessage(__('An error occurred. Please try again.', 'yatra'), 'error');
1106 1316 });
1107 1317 }
1108 1318
1109 1319 function removeCoupon() {
@@ -1533,9 +1743,9 @@
1533 1743 .then(response => response.json())
1534 1744 .then(response => {
1535 1745 if (response.success) {
1536 1746 $messageEl.removeClass('error').addClass('success');
1537 - $messageEl.text(response.message || 'Login successful! Redirecting...');
1747 + $messageEl.text(response.message || __('Login successful! Redirecting...', 'yatra'));
1538 1748 $messageEl.show();
1539 1749
1540 1750 // Reload page to show booking form
1541 1751 setTimeout(function() {
@@ -1542,16 +1752,18 @@
1542 1752 window.location.reload();
1543 1753 }, 1000);
1544 1754 } else {
1545 1755 $messageEl.removeClass('success').addClass('error');
1546 - $messageEl.html(response.message || 'Invalid credentials. Please try again.');
1756 + $messageEl.html(response.message || __('Invalid credentials. Please try again.', 'yatra'));
1547 1757 $messageEl.show();
1548 1758
1549 1759 // If email needs verification, show resend option
1550 1760 if (response.needs_verification && response.email) {
1761 + const didntReceiveText = __("Didn't receive the email?", 'yatra');
1762 + const resendLinkText = __('Resend verification link', 'yatra');
1551 1763 const $resendLink = $('<div class="yatra-resend-verification" style="margin-top: 12px; text-align: center;">' +
1552 - '<span style="color: #6b7280; font-size: 14px;">Didn\'t receive the email? </span>' +
1553 - '<button type="button" class="yatra-resend-btn" data-email="' + response.email + '" style="background: none; border: none; color: #3b82f6; font-size: 14px; font-weight: 600; cursor: pointer; text-decoration: underline;">Resend verification link</button>' +
1764 + '<span style="color: #6b7280; font-size: 14px;">' + didntReceiveText + ' </span>' +
1765 + '<button type="button" class="yatra-resend-btn" data-email="' + response.email + '" style="background: none; border: none; color: #3b82f6; font-size: 14px; font-weight: 600; cursor: pointer; text-decoration: underline;">' + resendLinkText + '</button>' +
1554 1766 '</div>');
1555 1767 $messageEl.after($resendLink);
1556 1768 }
1557 1769
@@ -1561,9 +1773,9 @@
1561 1773 }
1562 1774 })
1563 1775 .catch(function() {
1564 1776 $messageEl.removeClass('success').addClass('error');
1565 - $messageEl.text('An error occurred. Please try again.');
1777 + $messageEl.text(__('An error occurred. Please try again.', 'yatra'));
1566 1778 $messageEl.show();
1567 1779
1568 1780 $btn.prop('disabled', false);
1569 1781 $btnText.show();
@@ -1584,9 +1796,9 @@
1584 1796 return;
1585 1797 }
1586 1798
1587 1799 // Disable button and show loading
1588 - $btn.prop('disabled', true).text('Sending...');
1800 + $btn.prop('disabled', true).text(__('Sending...', 'yatra'));
1589 1801
1590 1802 fetch(apiUrl + '/auth/resend-verification', {
1591 1803 method: 'POST',
1592 1804 headers: {
@@ -1606,22 +1818,22 @@
1606 1818 } else if (response.rate_limited && response.remaining_seconds) {
1607 1819 // Start countdown timer
1608 1820 startResendCountdown($btn, response.remaining_seconds);
1609 1821 $messageEl.removeClass('success').addClass('error');
1610 - $messageEl.text('Please wait before requesting another email.');
1822 + $messageEl.text(__('Please wait before requesting another email.', 'yatra'));
1611 1823 $messageEl.show();
1612 1824 } else {
1613 1825 $messageEl.removeClass('success').addClass('error');
1614 1826 $messageEl.text(response.message);
1615 1827 $messageEl.show();
1616 - $btn.prop('disabled', false).text('Resend verification link');
1828 + $btn.prop('disabled', false).text(__('Resend verification link', 'yatra'));
1617 1829 }
1618 1830 })
1619 1831 .catch(function() {
1620 1832 $messageEl.removeClass('success').addClass('error');
1621 - $messageEl.text('An error occurred. Please try again.');
1833 + $messageEl.text(__('An error occurred. Please try again.', 'yatra'));
1622 1834 $messageEl.show();
1623 - $btn.prop('disabled', false).text('Resend verification link');
1835 + $btn.prop('disabled', false).text(__('Resend verification link', 'yatra'));
1624 1836 });
1625 1837 });
1626 1838
1627 1839 // Countdown timer for resend button
@@ -1632,9 +1844,9 @@
1632 1844 const updateCountdown = () => {
1633 1845 if (seconds <= 0) {
1634 1846 $btn.data('countdown-active', false);
1635 1847 $btn.prop('disabled', false);
1636 - $btn.html('Resend verification link');
1848 + $btn.html(__('Resend verification link', 'yatra'));
1637 1849 return;
1638 1850 }
1639 1851
1640 1852 const mins = Math.floor(seconds / 60);
@@ -1642,9 +1854,16 @@
1642 1854 const timeStr = mins > 0
1643 1855 ? `${mins}:${secs.toString().padStart(2, '0')}`
1644 1856 : `${secs}s`;
1645 1857
1646 - $btn.html(`<span style="color: #9ca3af;">Resend in </span><span style="color: #3b82f6; font-weight: 700;">${timeStr}</span>`);
1858 + // Resend-OTP countdown label. The %s placeholder is the
1859 + // remaining time as MM:SS — we wrap the literal so the
1860 + // whole sentence translates, then slot the timer in via
1861 + // replace() to avoid a dynamic msgid that gettext can't
1862 + // extract.
1863 + /* translators: %s is the remaining time (e.g. "0:45") before the verification code can be resent. */
1864 + const resendTxt = __('Resend in %s', 'yatra').replace('%s', timeStr);
1865 + $btn.html(`<span style="color: #9ca3af;">${resendTxt}</span>`);
1647 1866
1648 1867 seconds--;
1649 1868 setTimeout(updateCountdown, 1000);
1650 1869 };
@@ -1667,9 +1886,9 @@
1667 1886 const confirmPassword = $('#yatra-reg-confirm-password').val();
1668 1887
1669 1888 if (password !== confirmPassword) {
1670 1889 $messageEl.removeClass('success').addClass('error');
1671 - $messageEl.text('Passwords do not match.');
1890 + $messageEl.text(__('Passwords do not match.', 'yatra'));
1672 1891 $messageEl.show();
1673 1892 return;
1674 1893 }
1675 1894
@@ -1688,8 +1907,24 @@
1688 1907 password: password,
1689 1908 confirm_password: confirmPassword
1690 1909 };
1691 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 +
1692 1927 fetch(apiUrl + '/auth/register', {
1693 1928 method: 'POST',
1694 1929 headers: {
1695 1930 'Content-Type': 'application/json',
@@ -1701,9 +1936,9 @@
1701 1936 .then(response => response.json())
1702 1937 .then(response => {
1703 1938 if (response.success) {
1704 1939 $messageEl.removeClass('error').addClass('success');
1705 - $messageEl.text(response.message || 'Account created! Please check your email to verify.');
1940 + $messageEl.text(response.message || __('Account created! Please check your email to verify.', 'yatra'));
1706 1941 $messageEl.show();
1707 1942
1708 1943 // Reset form
1709 1944 $form[0].reset();
@@ -1725,9 +1960,9 @@
1725 1960
1726 1961 // Show info message on login tab
1727 1962 const $loginMessage = $('#yatra-login-message');
1728 1963 $loginMessage.removeClass('error').addClass('success');
1729 - $loginMessage.text('Please check your email and click the verification link, then login here.');
1964 + $loginMessage.text(__('Please check your email and click the verification link, then login here.', 'yatra'));
1730 1965 $loginMessage.show();
1731 1966 }, 3000);
1732 1967 } else {
1733 1968 // Reload page if no verification required (shouldn't happen normally)
@@ -1736,9 +1971,9 @@
1736 1971 }, 1000);
1737 1972 }
1738 1973 } else {
1739 1974 $messageEl.removeClass('success').addClass('error');
1740 - $messageEl.text(response.message || 'Registration failed. Please try again.');
1975 + $messageEl.text(response.message || __('Registration failed. Please try again.', 'yatra'));
1741 1976 $messageEl.show();
1742 1977
1743 1978 $btn.prop('disabled', false);
1744 1979 $btnText.show();
@@ -1746,9 +1981,9 @@
1746 1981 }
1747 1982 })
1748 1983 .catch(function() {
1749 1984 $messageEl.removeClass('success').addClass('error');
1750 - $messageEl.text('An error occurred. Please try again.');
1985 + $messageEl.text(__('An error occurred. Please try again.', 'yatra'));
1751 1986 $messageEl.show();
1752 1987
1753 1988 $btn.prop('disabled', false);
1754 1989 $btnText.show();
@@ -1753,8 +1988,72 @@
1753 1988 $btn.prop('disabled', false);
1754 1989 $btnText.show();
1755 1990 $btnLoading.hide();
1756 1991 });
1992 + }); // end reCAPTCHA token wrapper
1757 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 + }
1758 2057
1759 2058 // End of document ready
1760 2059 })(jQuery);