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 +498 -109 3.0.3 → 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 /**
@@ -264,8 +378,17 @@
264 378 const pricingType = $('#yatra-summary-pricing').data('pricing-type');
265 379 if (pricingType) {
266 380 payload.pricing_type = pricingType;
267 381 }
382 + // Carry the page URL's booking_token into the summary refresh so
383 + // the REST controller can rehydrate the session from the matching
384 + // transient when PHPSESSID isn't carried into REST scope. Without
385 + // this, /booking/summary returns 400 "No active booking session
386 + // found." on every recalculation.
387 + try {
388 + const tok = new URLSearchParams(window.location.search).get('booking_token');
389 + if (tok) payload.booking_token = tok;
390 + } catch (e) { /* URLSearchParams unavailable — skip */ }
268 391 return payload;
269 392 }
270 393
271 394 /**
@@ -290,9 +413,9 @@
290 413 }
291 414 const flexDue = paymentMethod === 'deposit' || paymentMethod === 'partial';
292 415
293 416 if (isOffline) {
294 - $buttonText.text('Complete Booking');
417 + $buttonText.text(window.yatraBookingData?.i18n?.complete_booking || __('Complete Booking', 'yatra'));
295 418 const showAmount = flexDue ? due : totalSafe;
296 419 if (showAmount > 0) {
297 420 $payAmount.text(formatCurrency(showAmount, currency)).show();
298 421 } else {
@@ -298,9 +421,9 @@
298 421 } else {
299 422 $payAmount.hide();
300 423 }
301 424 } else {
302 - $buttonText.text('Pay Now');
425 + $buttonText.text(window.yatraBookingData?.i18n?.pay_now || __('Pay Now', 'yatra'));
303 426 const payVal = flexDue && due > 0 ? due : totalSafe;
304 427 if (payVal > 0) {
305 428 $payAmount.text(formatCurrency(payVal, currency)).show();
306 429 } else {
@@ -320,16 +443,30 @@
320 443 if (!$firstTraveler.length) return;
321 444
322 445 // Clone the first traveler form
323 446 const $newTraveler = $firstTraveler.clone();
324 -
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 +
325 454 // Update index references
326 455 $newTraveler.attr('data-traveler-index', index);
327 - $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 + );
328 460
329 - // 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.
330 465 if (!$newTraveler.find('.yatra-traveler-note').length) {
331 - $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 + );
332 469 }
333 470
334 471 // Update all field IDs and names
335 472 $newTraveler.find('input, select, textarea').each(function() {
@@ -379,16 +516,16 @@
379 516 * Convert FormData to nested object structure
380 517 */
381 518 function formDataToObject(formData) {
382 519 const obj = {};
383 -
520 +
384 521 for (const [key, value] of formData.entries()) {
385 522 // Handle array notation like travelers[1][first_name]
386 523 const matches = key.match(/^([^\[]+)(?:\[([^\]]*)\])?(?:\[([^\]]*)\])?$/);
387 -
524 +
388 525 if (matches) {
389 526 const [, base, index, field] = matches;
390 -
527 +
391 528 if (index !== undefined && field !== undefined) {
392 529 // Array of objects: travelers[1][first_name]
393 530 if (!obj[base]) obj[base] = {};
394 531 if (!obj[base][index]) obj[base][index] = {};
@@ -393,11 +530,23 @@
393 530 if (!obj[base]) obj[base] = {};
394 531 if (!obj[base][index]) obj[base][index] = {};
395 532 obj[base][index][field] = value;
396 533 } else if (index !== undefined) {
397 - // Simple array: items[0]
534 + // Array notation. PHP/HTML produces two flavours:
535 + // items[0], items[1] → keyed indices
536 + // items[] → push to end (multi-checkbox)
537 + // The empty-string variant is what `<input name="additional_services[]">`
538 + // sends when several checkboxes are selected. Previously
539 + // we did `obj[base][""] = value` which silently overwrote
540 + // on each entry and JSON-serialised to `[]` — so the
541 + // server saw no selected services, even when boxes were
542 + // checked.
398 543 if (!obj[base]) obj[base] = [];
399 - obj[base][index] = value;
544 + if (index === '') {
545 + obj[base].push(value);
546 + } else {
547 + obj[base][index] = value;
548 + }
400 549 } else {
401 550 // Simple key
402 551 obj[key] = value;
403 552 }
@@ -431,15 +580,30 @@
431 580
432 581 // Validate all required fields
433 582 $form.find('input[required], select[required], textarea[required]').each(function() {
434 583 const $field = $(this);
435 - const value = $field.val();
436 -
437 - 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) {
438 598 isValid = false;
439 599 $field.addClass('error');
440 600 const label = $field.closest('.yatra-form-group').find('label').text().replace('*', '').trim();
441 - errors.push(label + ' is required');
601 + errors.push(sprintf(
602 + /* translators: %s: form field label. */
603 + __('%s is required', 'yatra'),
604 + label
605 + ));
442 606 }
443 607 });
444 608
445 609 // Validate email format
@@ -449,9 +613,9 @@
449 613 const value = $field.val();
450 614 if (value && !isValidEmail(value)) {
451 615 isValid = false;
452 616 $field.addClass('error');
453 - errors.push('Please enter a valid email address');
617 + errors.push(__('Please enter a valid email address', 'yatra'));
454 618 }
455 619 });
456 620
457 621 // Validate terms checkbox
@@ -456,15 +620,15 @@
456 620
457 621 // Validate terms checkbox
458 622 if (!$('input[name="accept_terms"]').is(':checked')) {
459 623 isValid = false;
460 - errors.push('Please accept the Terms and Conditions');
624 + errors.push(__('Please accept the Terms and Conditions', 'yatra'));
461 625 }
462 626
463 627 // Validate privacy checkbox
464 628 if (!$('input[name="accept_privacy"]').is(':checked')) {
465 629 isValid = false;
466 - errors.push('Please accept the Privacy Policy');
630 + errors.push(__('Please accept the Privacy Policy', 'yatra'));
467 631 }
468 632
469 633 if (!isValid && errors.length > 0) {
470 634 showFormError(errors[0]);
@@ -500,22 +664,33 @@
500 664 * @param {{ heading?: string, footerHtml?: string, iconColor?: string }|undefined} opts
501 665 */
502 666 function showSuccessMessage(message, reference, opts) {
503 667 opts = opts || {};
504 - const heading = opts.heading || 'Booking Confirmed!';
668 + const heading = opts.heading || __('Booking Confirmed!', 'yatra');
505 669 const iconColor = opts.iconColor || '#22c55e';
670 + const confirmationEmailText = __('A confirmation email has been sent to your email address.', 'yatra');
506 671 const footerHtml = opts.footerHtml !== undefined
507 672 ? opts.footerHtml
508 - : '<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 + );
509 679 const $success = $('<div class="yatra-booking-success" style="text-align: center; padding: 60px 20px;">' +
510 680 '<svg style="width: 80px; height: 80px; color: ' + iconColor + '; margin-bottom: 24px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
511 681 '<circle cx="12" cy="12" r="10"></circle><polyline points="9,12 12,15 16,10"></polyline></svg>' +
512 682 '<h2 style="font-size: 28px; margin-bottom: 12px; color: #111827;">' + heading + '</h2>' +
513 683 '<p style="font-size: 18px; color: #6b7280; margin-bottom: 8px;">' + message + '</p>' +
514 - '<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>' +
515 685 footerHtml +
516 686 '</div>');
517 -
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();
518 693 $form.html($success);
519 694 }
520 695
521 696 // =====================
@@ -555,10 +730,10 @@
555 730
556 731 const $btn = $(this);
557 732 const $input = $('#number-of-travelers');
558 733 let currentValue = parseInt($input.val()) || 1;
559 - const min = parseInt($input.attr('min')) || 1;
560 - 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);
561 736
562 737 if ($btn.hasClass('yatra-qty-plus')) {
563 738 if (currentValue < max) {
564 739 currentValue++;
@@ -588,10 +763,10 @@
588 763 const $btn = $(this);
589 764 const $row = $btn.closest('.yatra-quantity-row');
590 765 const $input = $row.find('.yatra-qty-input');
591 766 let currentValue = parseInt($input.val()) || 0;
592 - const min = parseInt($input.attr('min')) || 0;
593 - 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);
594 769
595 770 if ($btn.hasClass('yatra-qty-plus')) {
596 771 if (currentValue < max) {
597 772 currentValue++;
@@ -721,15 +896,15 @@
721 896 const $gatewayInfo = $('#yatra-gateway-info');
722 897 const $gatewayDetails = $('#yatra-gateway-details');
723 898
724 899 const gatewayMessages = {
725 - 'pay_later': '<p>Your booking will be reserved. Full payment is required before the trip date.</p>',
726 - 'bank_transfer': '<p>After completing your booking, you will receive bank details via email. Your booking will be confirmed once payment is received.</p>',
727 - 'stripe': '<p>You will be securely redirected to complete your payment with credit or debit card.</p>',
728 - 'paypal': '<p>You will be redirected to PayPal to complete your payment securely.</p>',
729 - 'razorpay': '<p>You will be redirected to Razorpay to complete your payment.</p>',
730 - 'esewa': '<p>You will be redirected to eSewa to complete your payment.</p>',
731 - '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>',
732 907 };
733 908
734 909 if (gatewayMessages[gateway] && $gatewayDetails.length) {
735 910 $gatewayDetails.html(gatewayMessages[gateway]);
@@ -784,29 +959,56 @@
784 959 /**
785 960 * Handle payment response from server
786 961 */
787 962 function handlePaymentResponse(response, originalBtnHtml) {
788 -
789 -
790 -
963 +
964 +
965 +
791 966 if (!response.success) {
792 - const errMsg = response.message || 'An error occurred. Please try again.';
967 + const errMsg = response.message || __('An error occurred. Please try again.', 'yatra');
793 968 showFormError(errMsg);
794 969 $submitBtn.prop('disabled', false).html(originalBtnHtml);
795 970 return;
796 971 }
797 -
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 +
798 1000 const data = response.data || {};
799 1001
800 1002 if (data.waitlist) {
801 - const reference = data.reference || 'N/A';
1003 + const reference = data.reference || __('N/A', 'yatra');
802 1004 showSuccessMessage(
803 - response.message || "You're on the waitlist for this departure.",
1005 + response.message || __("You're on the waitlist for this departure.", 'yatra'),
804 1006 reference,
805 1007 {
806 - heading: "You're on the waitlist",
1008 + heading: __("You're on the waitlist", 'yatra'),
807 1009 iconColor: '#ca8a04',
808 - 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>'
809 1011 }
810 1012 );
811 1013 return;
812 1014 }
@@ -844,9 +1046,9 @@
844 1046 document.removeEventListener('yatra_payment_failed', onFailed);
845 1047 document.removeEventListener('yatra_payment_cancelled', onCancelled);
846 1048 };
847 1049 const onFailed = (e) => {
848 - showFormError(e.detail?.error || 'Payment failed. Please try again.');
1050 + showFormError(e.detail?.error || __('Payment failed. Please try again.', 'yatra'));
849 1051 $submitBtn.prop('disabled', false).html(originalBtnHtml);
850 1052 cleanup();
851 1053 };
852 1054 const onCancelled = () => {
@@ -858,10 +1060,10 @@
858 1060 return;
859 1061 }
860 1062
861 1063 // Default: show success message
862 - const reference = data.reference || 'N/A';
863 - showSuccessMessage(response.message || 'Success!', reference);
1064 + const reference = data.reference || __('N/A', 'yatra');
1065 + showSuccessMessage(response.message || __('Success!', 'yatra'), reference);
864 1066 }
865 1067
866 1068 // Form submission - using FormData API
867 1069 $form.on('submit', function(e) {
@@ -886,12 +1088,44 @@
886 1088 bookingData.accept_terms = $('input[name="accept_terms"]').is(':checked');
887 1089 bookingData.accept_privacy = $('input[name="accept_privacy"]').is(':checked');
888 1090 bookingData.subscribe_newsletter = $('input[name="subscribe_newsletter"]').is(':checked');
889 1091 bookingData.payment_due = parseFloat($form.attr('data-payment-due')) || window.yatraBookingData?.paymentDue || null;
890 - bookingData.total_amount = summary.totalAmount ?? null;
891 - bookingData.amount_due = summary.amountDue ?? null;
1092 + // The pay button uses a fallback chain (summary.totalAmount → data-summary-total
1093 + // attribute → data-payment-due) so it can show "$279" even before the async
1094 + // pricing refresh populates `summary.totalAmount`. The submit path must walk
1095 + // the same chain — otherwise the button label and the request body disagree
1096 + // and BookingService rejects with "Total amount must be greater than zero."
1097 + const $summaryEl = $('.yatra-booking-summary').first();
1098 + const resolveAmount = (preferred, attr) => {
1099 + const n = Number(preferred);
1100 + if (Number.isFinite(n) && n > 0) return n;
1101 + const a = parseFloat($summaryEl.attr(attr));
1102 + if (Number.isFinite(a) && a > 0) return a;
1103 + return null;
1104 + };
1105 + bookingData.total_amount =
1106 + resolveAmount(summary.totalAmount, 'data-summary-total') ??
1107 + resolveAmount(summary.amountDue, 'data-summary-due') ??
1108 + (parseFloat($form.attr('data-payment-due')) || null);
1109 + bookingData.amount_due =
1110 + resolveAmount(summary.amountDue, 'data-summary-due') ??
1111 + (parseFloat($form.attr('data-payment-due')) || null);
892 1112 bookingData.amount_paid = summary.amountPaid ?? null;
893 1113 bookingData.currency = window.yatraBookingData?.currency || bookingData.currency || $('input[name="currency"]').val() || 'USD';
1114 + // Carry the booking_token from the page URL into the REST request.
1115 + // Without this the server's `create_booking` cannot rehydrate the
1116 + // PHP session for the user (REST runs in a separate request and
1117 + // PHPSESSID propagation isn't guaranteed). Server falls back to
1118 + // looking up the transient by this token to recover `traveler_counts`,
1119 + // `pricing_type`, `price_types` — without those, `calculatePricing`
1120 + // returns 0 for `traveler_based` trips and the booking gets
1121 + // rejected with "Total amount must be greater than zero."
1122 + try {
1123 + const urlToken = new URLSearchParams(window.location.search).get('booking_token');
1124 + if (urlToken) {
1125 + bookingData.booking_token = urlToken;
1126 + }
1127 + } catch (e) { /* URLSearchParams missing — ignore */ }
894 1128
895 1129 // Dispatch unified booking submit event - gateways can intercept
896 1130 const submitEvent = new CustomEvent('yatra_booking_submit', {
897 1131 detail: {
@@ -918,8 +1152,25 @@
918 1152 /**
919 1153 * Submit booking to server
920 1154 */
921 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) {
922 1173 const { base: restBase, isPlain } = getRestBase();
923 1174 const siteBase = (window.yatraBookingData?.siteUrl || window.location.origin || '').replace(/\/$/, '');
924 1175 let createUrl;
925 1176 if (isPlain) {
@@ -933,16 +1184,28 @@
933 1184 // Show loading state
934 1185 $submitBtn.prop('disabled', true).html(
935 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">' +
936 1187 '<circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"></circle></svg>' +
937 - '<span>Processing...</span>'
1188 + '<span>' + __('Processing...', 'yatra') + '</span>'
938 1189 );
939 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 +
940 1202 fetch(createUrl, {
941 1203 method: 'POST',
942 1204 headers: {
943 1205 'Content-Type': 'application/json',
944 - 'X-WP-Nonce': nonce
1206 + 'X-WP-Nonce': nonce,
1207 + 'X-Yatra-Booking-Nonce': bookingNonce
945 1208 },
946 1209 credentials: 'same-origin',
947 1210 body: JSON.stringify(bookingData)
948 1211 })
@@ -949,9 +1212,9 @@
949 1212 .then(response => response.json())
950 1213 .then(response => handlePaymentResponse(response, originalBtnHtml))
951 1214 .catch(error => {
952 1215 console.error('Error:', error);
953 - showFormError('An error occurred. Please try again.');
1216 + showFormError(__('An error occurred. Please try again.', 'yatra'));
954 1217 $submitBtn.prop('disabled', false).html(originalBtnHtml);
955 1218 });
956 1219 }
957 1220
@@ -994,16 +1257,25 @@
994 1257 const $btn = $('#yatra-apply-coupon');
995 1258 const $message = $('#yatra-coupon-message');
996 1259
997 1260 if (!code) {
998 - showCouponMessage('Please enter a coupon code.', 'error');
1261 + showCouponMessage(__('Please enter a coupon code.', 'yatra'), 'error');
999 1262 return;
1000 1263 }
1001 1264
1002 1265 // Show loading state
1003 - $btn.prop('disabled', true).text('Applying...');
1266 + $btn.prop('disabled', true).text(__('Applying...', 'yatra'));
1004 1267 $message.hide();
1005 1268
1269 + // Carry booking_token so the REST endpoint can rehydrate the
1270 + // session in contexts where PHPSESSID isn't passed through —
1271 + // same pattern as the service-toggle and summary refresh.
1272 + const couponPayload = { code: code };
1273 + try {
1274 + const tok = new URLSearchParams(window.location.search).get('booking_token');
1275 + if (tok) couponPayload.booking_token = tok;
1276 + } catch (e) { /* URLSearchParams unavailable — skip */ }
1277 +
1006 1278 fetch(apiUrl + '/booking/coupon/apply', {
1007 1279 method: 'POST',
1008 1280 headers: {
1009 1281 'Content-Type': 'application/json',
@@ -1009,13 +1281,13 @@
1009 1281 'Content-Type': 'application/json',
1010 1282 'X-WP-Nonce': nonce
1011 1283 },
1012 1284 credentials: 'same-origin',
1013 - body: JSON.stringify({ code: code })
1285 + body: JSON.stringify(couponPayload)
1014 1286 })
1015 1287 .then(response => response.json())
1016 1288 .then(response => {
1017 - $btn.prop('disabled', false).text('Apply');
1289 + $btn.prop('disabled', false).text(__('Apply', 'yatra'));
1018 1290
1019 1291 if (response.success) {
1020 1292 appliedCoupon = response.data;
1021 1293
@@ -1033,15 +1305,15 @@
1033 1305 updatePriceWithDiscount(response.data);
1034 1306
1035 1307 showCouponMessage(response.message, 'success');
1036 1308 } else {
1037 - showCouponMessage(response.message || 'Invalid coupon code.', 'error');
1309 + showCouponMessage(response.message || __('Invalid coupon code.', 'yatra'), 'error');
1038 1310 }
1039 1311 })
1040 1312 .catch(error => {
1041 1313 console.error('Coupon error:', error);
1042 - $btn.prop('disabled', false).text('Apply');
1043 - 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');
1044 1316 });
1045 1317 }
1046 1318
1047 1319 function removeCoupon() {
@@ -1048,8 +1320,14 @@
1048 1320 const $btn = $('#yatra-remove-coupon');
1049 1321
1050 1322 $btn.prop('disabled', true);
1051 1323
1324 + const removePayload = {};
1325 + try {
1326 + const tok = new URLSearchParams(window.location.search).get('booking_token');
1327 + if (tok) removePayload.booking_token = tok;
1328 + } catch (e) { /* URLSearchParams unavailable — skip */ }
1329 +
1052 1330 fetch(apiUrl + '/booking/coupon/remove', {
1053 1331 method: 'POST',
1054 1332 headers: {
1055 1333 'Content-Type': 'application/json',
@@ -1055,9 +1333,9 @@
1055 1333 'Content-Type': 'application/json',
1056 1334 'X-WP-Nonce': nonce
1057 1335 },
1058 1336 credentials: 'same-origin',
1059 - body: JSON.stringify({})
1337 + body: JSON.stringify(removePayload)
1060 1338 })
1061 1339 .then(response => response.json())
1062 1340 .then(response => {
1063 1341 $btn.prop('disabled', false);
@@ -1177,8 +1455,21 @@
1177 1455 } else {
1178 1456 sessionUrl = `${restBase}/yatra/v1/booking/session`;
1179 1457 }
1180 1458
1459 + // Append booking_token from the page URL so the REST endpoint can
1460 + // rehydrate the session via transient when PHPSESSID hasn't been
1461 + // carried over — otherwise this GET returns "No active session"
1462 + // and we never reveal the applied-coupon UI (and its remove
1463 + // button) after a page refresh.
1464 + try {
1465 + const tok = new URLSearchParams(window.location.search).get('booking_token');
1466 + if (tok) {
1467 + const sep = sessionUrl.includes('?') ? '&' : '?';
1468 + sessionUrl = sessionUrl + sep + 'booking_token=' + encodeURIComponent(tok);
1469 + }
1470 + } catch (e) { /* URLSearchParams unavailable — skip */ }
1471 +
1181 1472 fetch(sessionUrl, {
1182 1473 method: 'GET',
1183 1474 credentials: 'same-origin',
1184 1475 headers: {
@@ -1322,8 +1613,19 @@
1322 1613 } else {
1323 1614 sessionUrl = `${restBase}/yatra/v1/booking/session`;
1324 1615 }
1325 1616
1617 + // Carry booking_token from URL so the REST endpoint can rehydrate
1618 + // the session even when PHPSESSID hasn't propagated to the REST
1619 + // context (root cause of the "POST /booking/session 400" we saw
1620 + // on service-toggle). Matches the same pattern the /booking/create
1621 + // submit uses.
1622 + const sessionPayload = { additional_services: serviceIds };
1623 + try {
1624 + const tok = new URLSearchParams(window.location.search).get('booking_token');
1625 + if (tok) sessionPayload.booking_token = tok;
1626 + } catch (e) { /* URLSearchParams unavailable — skip */ }
1627 +
1326 1628 fetch(sessionUrl, {
1327 1629 method: 'POST',
1328 1630 credentials: 'same-origin',
1329 1631 headers: {
@@ -1329,11 +1631,9 @@
1329 1631 headers: {
1330 1632 'Content-Type': 'application/json',
1331 1633 'X-WP-Nonce': nonce
1332 1634 },
1333 - body: JSON.stringify({
1334 - additional_services: serviceIds
1335 - })
1635 + body: JSON.stringify(sessionPayload)
1336 1636 })
1337 1637 .then(response => response.json())
1338 1638 .then(response => {
1339 1639 if (response.success) {
@@ -1443,9 +1743,9 @@
1443 1743 .then(response => response.json())
1444 1744 .then(response => {
1445 1745 if (response.success) {
1446 1746 $messageEl.removeClass('error').addClass('success');
1447 - $messageEl.text(response.message || 'Login successful! Redirecting...');
1747 + $messageEl.text(response.message || __('Login successful! Redirecting...', 'yatra'));
1448 1748 $messageEl.show();
1449 1749
1450 1750 // Reload page to show booking form
1451 1751 setTimeout(function() {
@@ -1452,16 +1752,18 @@
1452 1752 window.location.reload();
1453 1753 }, 1000);
1454 1754 } else {
1455 1755 $messageEl.removeClass('success').addClass('error');
1456 - $messageEl.html(response.message || 'Invalid credentials. Please try again.');
1756 + $messageEl.html(response.message || __('Invalid credentials. Please try again.', 'yatra'));
1457 1757 $messageEl.show();
1458 1758
1459 1759 // If email needs verification, show resend option
1460 1760 if (response.needs_verification && response.email) {
1761 + const didntReceiveText = __("Didn't receive the email?", 'yatra');
1762 + const resendLinkText = __('Resend verification link', 'yatra');
1461 1763 const $resendLink = $('<div class="yatra-resend-verification" style="margin-top: 12px; text-align: center;">' +
1462 - '<span style="color: #6b7280; font-size: 14px;">Didn\'t receive the email? </span>' +
1463 - '<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>' +
1464 1766 '</div>');
1465 1767 $messageEl.after($resendLink);
1466 1768 }
1467 1769
@@ -1471,9 +1773,9 @@
1471 1773 }
1472 1774 })
1473 1775 .catch(function() {
1474 1776 $messageEl.removeClass('success').addClass('error');
1475 - $messageEl.text('An error occurred. Please try again.');
1777 + $messageEl.text(__('An error occurred. Please try again.', 'yatra'));
1476 1778 $messageEl.show();
1477 1779
1478 1780 $btn.prop('disabled', false);
1479 1781 $btnText.show();
@@ -1494,9 +1796,9 @@
1494 1796 return;
1495 1797 }
1496 1798
1497 1799 // Disable button and show loading
1498 - $btn.prop('disabled', true).text('Sending...');
1800 + $btn.prop('disabled', true).text(__('Sending...', 'yatra'));
1499 1801
1500 1802 fetch(apiUrl + '/auth/resend-verification', {
1501 1803 method: 'POST',
1502 1804 headers: {
@@ -1516,22 +1818,22 @@
1516 1818 } else if (response.rate_limited && response.remaining_seconds) {
1517 1819 // Start countdown timer
1518 1820 startResendCountdown($btn, response.remaining_seconds);
1519 1821 $messageEl.removeClass('success').addClass('error');
1520 - $messageEl.text('Please wait before requesting another email.');
1822 + $messageEl.text(__('Please wait before requesting another email.', 'yatra'));
1521 1823 $messageEl.show();
1522 1824 } else {
1523 1825 $messageEl.removeClass('success').addClass('error');
1524 1826 $messageEl.text(response.message);
1525 1827 $messageEl.show();
1526 - $btn.prop('disabled', false).text('Resend verification link');
1828 + $btn.prop('disabled', false).text(__('Resend verification link', 'yatra'));
1527 1829 }
1528 1830 })
1529 1831 .catch(function() {
1530 1832 $messageEl.removeClass('success').addClass('error');
1531 - $messageEl.text('An error occurred. Please try again.');
1833 + $messageEl.text(__('An error occurred. Please try again.', 'yatra'));
1532 1834 $messageEl.show();
1533 - $btn.prop('disabled', false).text('Resend verification link');
1835 + $btn.prop('disabled', false).text(__('Resend verification link', 'yatra'));
1534 1836 });
1535 1837 });
1536 1838
1537 1839 // Countdown timer for resend button
@@ -1542,9 +1844,9 @@
1542 1844 const updateCountdown = () => {
1543 1845 if (seconds <= 0) {
1544 1846 $btn.data('countdown-active', false);
1545 1847 $btn.prop('disabled', false);
1546 - $btn.html('Resend verification link');
1848 + $btn.html(__('Resend verification link', 'yatra'));
1547 1849 return;
1548 1850 }
1549 1851
1550 1852 const mins = Math.floor(seconds / 60);
@@ -1552,9 +1854,16 @@
1552 1854 const timeStr = mins > 0
1553 1855 ? `${mins}:${secs.toString().padStart(2, '0')}`
1554 1856 : `${secs}s`;
1555 1857
1556 - $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>`);
1557 1866
1558 1867 seconds--;
1559 1868 setTimeout(updateCountdown, 1000);
1560 1869 };
@@ -1577,9 +1886,9 @@
1577 1886 const confirmPassword = $('#yatra-reg-confirm-password').val();
1578 1887
1579 1888 if (password !== confirmPassword) {
1580 1889 $messageEl.removeClass('success').addClass('error');
1581 - $messageEl.text('Passwords do not match.');
1890 + $messageEl.text(__('Passwords do not match.', 'yatra'));
1582 1891 $messageEl.show();
1583 1892 return;
1584 1893 }
1585 1894
@@ -1598,8 +1907,24 @@
1598 1907 password: password,
1599 1908 confirm_password: confirmPassword
1600 1909 };
1601 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 +
1602 1927 fetch(apiUrl + '/auth/register', {
1603 1928 method: 'POST',
1604 1929 headers: {
1605 1930 'Content-Type': 'application/json',
@@ -1611,9 +1936,9 @@
1611 1936 .then(response => response.json())
1612 1937 .then(response => {
1613 1938 if (response.success) {
1614 1939 $messageEl.removeClass('error').addClass('success');
1615 - $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'));
1616 1941 $messageEl.show();
1617 1942
1618 1943 // Reset form
1619 1944 $form[0].reset();
@@ -1635,9 +1960,9 @@
1635 1960
1636 1961 // Show info message on login tab
1637 1962 const $loginMessage = $('#yatra-login-message');
1638 1963 $loginMessage.removeClass('error').addClass('success');
1639 - $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'));
1640 1965 $loginMessage.show();
1641 1966 }, 3000);
1642 1967 } else {
1643 1968 // Reload page if no verification required (shouldn't happen normally)
@@ -1646,9 +1971,9 @@
1646 1971 }, 1000);
1647 1972 }
1648 1973 } else {
1649 1974 $messageEl.removeClass('success').addClass('error');
1650 - $messageEl.text(response.message || 'Registration failed. Please try again.');
1975 + $messageEl.text(response.message || __('Registration failed. Please try again.', 'yatra'));
1651 1976 $messageEl.show();
1652 1977
1653 1978 $btn.prop('disabled', false);
1654 1979 $btnText.show();
@@ -1656,9 +1981,9 @@
1656 1981 }
1657 1982 })
1658 1983 .catch(function() {
1659 1984 $messageEl.removeClass('success').addClass('error');
1660 - $messageEl.text('An error occurred. Please try again.');
1985 + $messageEl.text(__('An error occurred. Please try again.', 'yatra'));
1661 1986 $messageEl.show();
1662 1987
1663 1988 $btn.prop('disabled', false);
1664 1989 $btnText.show();
@@ -1663,8 +1988,72 @@
1663 1988 $btn.prop('disabled', false);
1664 1989 $btnText.show();
1665 1990 $btnLoading.hide();
1666 1991 });
1992 + }); // end reCAPTCHA token wrapper
1667 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 + }
1668 2057
1669 2058 // End of document ready
1670 2059 })(jQuery);