PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.8
Yatra – Travel Booking & Tour Operator Software v3.0.8
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
yatra / assets / js / booking.js

booking.js in Yatra – Travel Booking & Tour Operator Software 3.0.8, at assets/js/booking.js

1,963 lines 85.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Booking Page JavaScript
3 *
4 * Uses FormData API to automatically collect all form fields
5 * and submit via REST API
6 *
7 * @package Yatra
8 */
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 if (typeof window.__ === 'function') return;
16 if (window.wp && window.wp.i18n && typeof window.wp.i18n.__ === 'function') {
17 window.__ = function (text, domain) { return window.wp.i18n.__(text, domain || 'yatra'); };
18 window._n = function (s, p, n, domain) { return window.wp.i18n._n(s, p, n, domain || 'yatra'); };
19 window._x = function (text, ctx, domain) { return window.wp.i18n._x(text, ctx, domain || 'yatra'); };
20 window.sprintf = window.sprintf || (window.wp.i18n.sprintf || function (fmt) { return fmt; });
21 } else {
22 window.__ = function (text) { return text; };
23 window._n = function (s, p, n) { return n === 1 ? s : p; };
24 window._x = function (text) { return text; };
25 window.sprintf = window.sprintf || function (fmt) { return fmt; };
26 }
27 })();
28
29 (function($) {
30 'use strict';
31
32 // API configuration - available throughout the module
33 const apiUrl = window.yatraBookingData?.apiUrl || '/wp-json/yatra/v1';
34 const nonce = window.yatraBookingData?.nonce || '';
35
36 // Unified REST base resolver with plain-permalink support
37 const getRestBase = () => {
38 const siteUrl = window.yatraBookingData?.siteUrl || window.location.origin || '';
39 let base =
40 window.yatraBookingData?.apiUrl ||
41 window.yatraBookingData?.restUrl ||
42 (window.wpApiSettings && window.wpApiSettings.root) ||
43 `${siteUrl.replace(/\/$/, '')}/wp-json`;
44 base = base.replace(/\/$/, '');
45 const permalinkStructure =
46 window.yatraBookingData?.permalinkStructure ||
47 window.yatraTripData?.permalinkStructure ||
48 window.yatraAdmin?.permalinkStructure ||
49 '';
50 // Default to plain when structure is unknown to avoid 404s under Plain permalinks
51 const isPlain = permalinkStructure === 'plain' || !permalinkStructure;
52 return { base, isPlain };
53 };
54
55 $(document).ready(function() {
56 const $form = $('#yatra-booking-form');
57 const $submitBtn = $('#yatra-submit-booking');
58 const isRemainingPayment = Boolean(window.yatraBookingData?.isRemainingPayment) || ($form.data('is-remaining-payment') === 'yes');
59 const remainingAmount = isRemainingPayment
60 ? parseFloat(window.yatraBookingData?.remainingAmount ?? $form.data('payment-due')) || 0
61 : 0;
62 const totalRemainingAmount = isRemainingPayment
63 ? parseFloat(window.yatraBookingData?.totalAmount ?? (remainingAmount + (window.yatraBookingData?.amountPaid || 0))) || remainingAmount
64 : null;
65
66 // Get pricing data
67 const pricePerPerson = parseFloat($('input[name="trip_price"]').val()) || window.yatraBookingData?.tripPrice || 0;
68 // Use global currency from settings (prioritize yatraBookingData which uses global currency)
69 const currency = window.yatraBookingData?.currency || $('input[name="currency"]').val() || 'USD';
70 const depositPercentage = window.yatraBookingData?.depositPercentage || 20;
71 const partialPercentage = window.yatraBookingData?.partialPercentage || 30;
72
73 window.yatraBookingSummary = window.yatraBookingSummary || {};
74
75 // Coupon state
76 let appliedCoupon = null;
77
78 // Group discount state (used for dynamic updates when traveler count changes)
79 let appliedGroupDiscount = null;
80 const groupDiscounts = window.yatraBookingData?.groupDiscounts || [];
81
82 // Country options for dynamically added travelers
83 const countryOptions = generateCountryOptions();
84
85 /**
86 * Generate country select options HTML.
87 *
88 * Each English country name is wrapped in `__()` so it's
89 * extractable and translatable per WordPress conventions.
90 * Keys (ISO 3166-1 alpha-2 codes) are untranslated, as they
91 * should be — they're identifiers, not user-facing text.
92 */
93 function generateCountryOptions() {
94 const countries = {
95 'AF': __('Afghanistan', 'yatra'), 'AL': __('Albania', 'yatra'), 'DZ': __('Algeria', 'yatra'), 'AR': __('Argentina', 'yatra'),
96 'AU': __('Australia', 'yatra'), 'AT': __('Austria', 'yatra'), 'BD': __('Bangladesh', 'yatra'), 'BE': __('Belgium', 'yatra'),
97 'BR': __('Brazil', 'yatra'), 'BT': __('Bhutan', 'yatra'), 'CA': __('Canada', 'yatra'), 'CN': __('China', 'yatra'),
98 'CO': __('Colombia', 'yatra'), 'CZ': __('Czech Republic', 'yatra'), 'DK': __('Denmark', 'yatra'), 'EG': __('Egypt', 'yatra'),
99 'FI': __('Finland', 'yatra'), 'FR': __('France', 'yatra'), 'DE': __('Germany', 'yatra'), 'GR': __('Greece', 'yatra'),
100 'HK': __('Hong Kong', 'yatra'), 'HU': __('Hungary', 'yatra'), 'IS': __('Iceland', 'yatra'), 'IN': __('India', 'yatra'),
101 'ID': __('Indonesia', 'yatra'), 'IE': __('Ireland', 'yatra'), 'IL': __('Israel', 'yatra'), 'IT': __('Italy', 'yatra'),
102 'JP': __('Japan', 'yatra'), 'KE': __('Kenya', 'yatra'), 'KR': __('South Korea', 'yatra'), 'MY': __('Malaysia', 'yatra'),
103 'MV': __('Maldives', 'yatra'), 'MX': __('Mexico', 'yatra'), 'NL': __('Netherlands', 'yatra'), 'NZ': __('New Zealand', 'yatra'),
104 'NP': __('Nepal', 'yatra'), 'NO': __('Norway', 'yatra'), 'PK': __('Pakistan', 'yatra'), 'PE': __('Peru', 'yatra'),
105 'PH': __('Philippines', 'yatra'), 'PL': __('Poland', 'yatra'), 'PT': __('Portugal', 'yatra'), 'RO': __('Romania', 'yatra'),
106 'RU': __('Russia', 'yatra'), 'SA': __('Saudi Arabia', 'yatra'), 'SG': __('Singapore', 'yatra'), 'ZA': __('South Africa', 'yatra'),
107 'ES': __('Spain', 'yatra'), 'LK': __('Sri Lanka', 'yatra'), 'SE': __('Sweden', 'yatra'), 'CH': __('Switzerland', 'yatra'),
108 'TW': __('Taiwan', 'yatra'), 'TH': __('Thailand', 'yatra'), 'TR': __('Turkey', 'yatra'), 'AE': __('United Arab Emirates', 'yatra'),
109 'GB': __('United Kingdom', 'yatra'), 'US': __('United States', 'yatra'), 'VN': __('Vietnam', 'yatra')
110 };
111
112 let options = '<option value="">' + __('Select Country', 'yatra') + '</option>';
113 for (const [code, name] of Object.entries(countries)) {
114 options += `<option value="${code}">${name}</option>`;
115 }
116 return options;
117 }
118
119 /**
120 * Get current traveler count
121 */
122 function getTravelerCount() {
123 // Check if we have category-based pricing (dropdown style)
124 const categoryInputs = $('.yatra-qty-input[data-category-id]');
125 if (categoryInputs.length > 0) {
126 let total = 0;
127 categoryInputs.each(function() {
128 total += parseInt($(this).val()) || 0;
129 });
130 return total || 1;
131 }
132
133 // Fallback to simple number input or traveler forms
134 const simpleInput = $('#number-of-travelers').val();
135 if (simpleInput) {
136 return parseInt(simpleInput) || 1;
137 }
138
139 return $('.yatra-traveler-form').length || 1;
140 }
141
142 /**
143 * Check if using traveler-based pricing
144 */
145 function isTravelerBasedPricing() {
146 return $('.yatra-summary-pricing').data('pricing-type') === 'traveler_based';
147 }
148
149 /**
150 * Calculate total for traveler-based pricing
151 */
152 function calculateTravelerBasedTotal() {
153 let total = 0;
154 $('.yatra-qty-input[data-category-id]').each(function() {
155 const count = parseInt($(this).val()) || 0;
156 const price = parseFloat($(this).data('price')) || 0;
157 const $row = $(this).closest('.yatra-quantity-row');
158 const pricingMode = $(this).data('pricing-mode') || $row.data('pricing-mode') || 'per_person';
159 const groupOverflow = $(this).data('group-overflow') || $row.data('group-overflow') || 'block';
160 const maxPax = parseInt($(this).data('max-pax') || $row.data('max-pax')) || 0;
161
162 if (pricingMode === 'per_group') {
163 // Per group: one flat price for the whole group, or one flat
164 // price per block of maxPax people when overflow is "per_block".
165 if (count > 0) {
166 total += (groupOverflow === 'per_block' && maxPax > 0)
167 ? price * Math.ceil(count / maxPax)
168 : price;
169 }
170 } else {
171 // Per person: charge per traveler
172 total += count * price;
173 }
174 });
175 return total;
176 }
177
178 /**
179 * Debounce timer for AJAX calls
180 */
181 let summaryDebounceTimer = null;
182
183 /**
184 * Update the dropdown display text for traveler-based pricing
185 */
186 function updateTravelersDisplayText() {
187 const parts = [];
188 $('.yatra-qty-input[data-category-id]').each(function() {
189 const count = parseInt($(this).val()) || 0;
190 const categoryLabel = $(this).closest('.yatra-quantity-row').find('.yatra-quantity-title').text();
191 if (count > 0) {
192 parts.push(sprintf(
193 /* translators: 1: traveler category label, 2: count. */
194 __('%1$s x %2$d', 'yatra'),
195 categoryLabel,
196 count
197 ));
198 }
199 });
200 const displayText = parts.length > 0 ? parts.join(', ') : __('Select travelers', 'yatra');
201 $('#yatra-travelers-display').text(displayText);
202 }
203
204 /**
205 * Update display text for regular pricing
206 */
207 function updateRegularTravelersDisplayText(count) {
208 $('#yatra-travelers-display-regular').text(sprintf(
209 /* translators: %d: number of travelers on this booking. */
210 _n('%d traveler', '%d travelers', count, 'yatra'),
211 count
212 ));
213 }
214
215
216 /**
217 * Format currency
218 */
219 function formatCurrency(amount, currencyCode) {
220 // Get currency formatting settings from global settings
221 const currencyPosition = window.yatraBookingData?.currencyPosition || 'before';
222 let decimalPlaces = parseInt(String(window.yatraBookingData?.decimalPlaces ?? '2'), 10);
223 if (Number.isNaN(decimalPlaces)) {
224 decimalPlaces = 2;
225 }
226 decimalPlaces = Math.max(0, Math.min(4, decimalPlaces));
227 const thousandSeparator = window.yatraBookingData?.thousandSeparator || ',';
228 const decimalSeparator = window.yatraBookingData?.decimalSeparator || '.';
229
230 // Currency symbols matching PHP yatra_get_currency_symbol function
231 const symbols = {
232 'USD': '$', 'EUR': '€', 'GBP': '£', 'JPY': '¥', 'CNY': '¥',
233 'INR': '₹', 'NPR': 'Rs', 'AUD': 'A$', 'CAD': 'C$', 'CHF': 'CHF',
234 'NZD': 'NZ$', 'SGD': 'S$', 'HKD': 'HK$', 'KRW': '₩', 'THB': '฿',
235 'MYR': 'RM', 'PHP': '₱', 'IDR': 'Rp', 'VND': '₫', 'BRL': 'R$',
236 'MXN': 'MX$', 'RUB': '₽', 'ZAR': 'R', 'AED': 'د.إ', 'SAR': '﷼',
237 'TRY': '₺', 'SEK': 'kr', 'NOK': 'kr', 'DKK': 'kr', 'PLN': 'zł',
238 'CZK': 'Kč', 'HUF': 'Ft', 'ILS': '₪', 'TWD': 'NT$', 'PKR': '₨',
239 'BDT': '৳', 'LKR': 'Rs', 'EGP': 'E£', 'NGN': '₦', 'KES': 'KSh',
240 'GHS': 'GH₵', 'ARS': 'AR$', 'CLP': 'CL$', 'COP': 'CO$', 'PEN': 'S/'
241 };
242
243 const symbol = symbols[currencyCode?.toUpperCase()] || (currencyCode ? currencyCode + ' ' : '');
244
245 // Format amount with proper separators
246 // First format with standard separators, then replace with custom ones
247 const tempFormatted = amount.toLocaleString('en-US', {
248 minimumFractionDigits: decimalPlaces,
249 maximumFractionDigits: decimalPlaces
250 });
251 // Replace thousand separator
252 const formattedAmount = tempFormatted
253 .split(',').join('__THOUSAND__')
254 .split('.').join('__DECIMAL__')
255 .replace(/__THOUSAND__/g, thousandSeparator)
256 .replace(/__DECIMAL__/g, decimalSeparator);
257
258 // Placement must match PHP yatra_format_price() exactly: the Settings
259 // UI uses left / right / left_space / right_space; legacy values are
260 // before (= left_space) and after (= right_space). Only *_space adds a
261 // space, so a "left" setting renders "$208.35", not "$ 208.35".
262 let pos = String(currencyPosition || '').toLowerCase().trim();
263 if (pos === 'before') {
264 pos = 'left_space';
265 } else if (pos === 'after') {
266 pos = 'right_space';
267 }
268 if (['left', 'right', 'left_space', 'right_space'].indexOf(pos) === -1) {
269 pos = 'left_space';
270 }
271
272 if (pos === 'right') {
273 return formattedAmount + symbol;
274 }
275 if (pos === 'right_space') {
276 return formattedAmount + ' ' + symbol;
277 }
278 if (pos === 'left') {
279 return symbol + formattedAmount;
280 }
281 return symbol + ' ' + formattedAmount;
282 }
283
284 /**
285 * Build JSON body for /booking/summary (payment method, travelers, services).
286 * Keeps sidebar + window.yatraBookingSummary aligned with Pro flexible payments.
287 */
288 function buildPricingSummaryPayload() {
289 const payload = {};
290 const pm = $('input[name="payment_method"]:checked').val();
291 if (pm) {
292 payload.payment_method = pm;
293 }
294 const travelDate = $('#travel-date').val() || $('input[name="travel_date"]').first().val();
295 if (travelDate) {
296 payload.travel_date = travelDate;
297 }
298 const depTime = $('input[name="departure_time"]').first().val();
299 if (depTime) {
300 payload.departure_time = depTime;
301 }
302 if ($('.yatra-qty-input[data-category-id]').length) {
303 const travelerCounts = {};
304 $('.yatra-qty-input[data-category-id]').each(function() {
305 const catId = $(this).data('category-id');
306 travelerCounts[catId] = parseInt($(this).val(), 10) || 0;
307 });
308 payload.traveler_counts = travelerCounts;
309 }
310 const serviceIds = [];
311 $('#yatra-additional-services input[type="checkbox"]:checked').each(function() {
312 const id = parseInt($(this).closest('.yatra-service-item').data('service-id'), 10);
313 if (!Number.isNaN(id)) {
314 serviceIds.push(id);
315 }
316 });
317 if (serviceIds.length) {
318 payload.additional_services = serviceIds;
319 }
320 const pricingType = $('#yatra-summary-pricing').data('pricing-type');
321 if (pricingType) {
322 payload.pricing_type = pricingType;
323 }
324 // Carry the page URL's booking_token into the summary refresh so
325 // the REST controller can rehydrate the session from the matching
326 // transient when PHPSESSID isn't carried into REST scope. Without
327 // this, /booking/summary returns 400 "No active booking session
328 // found." on every recalculation.
329 try {
330 const tok = new URLSearchParams(window.location.search).get('booking_token');
331 if (tok) payload.booking_token = tok;
332 } catch (e) { /* URLSearchParams unavailable — skip */ }
333 return payload;
334 }
335
336 /**
337 * Update pay button label and amount (offline gateways still show amount due for deposit/partial).
338 */
339 function updateCheckoutButtonState() {
340 const $buttonText = $('#pay-button-text');
341 const $payAmount = $('#pay-amount');
342 const gateway = $('input[name="payment_gateway"]:checked').val() || 'pay_later';
343 const offlineGateways = ['pay_later', 'bank_transfer'];
344 const isOffline = offlineGateways.includes(gateway);
345 const paymentMethod = $('input[name="payment_method"]:checked').val() || 'full';
346 const summary = window.yatraBookingSummary || {};
347 let due = parseFloat(summary.amountDue);
348 const total = parseFloat(summary.totalAmount);
349 if (Number.isNaN(due)) {
350 due = parseFloat($form.attr('data-payment-due')) || 0;
351 }
352 let totalSafe = total;
353 if (Number.isNaN(totalSafe)) {
354 totalSafe = parseFloat($form.attr('data-payment-due')) || 0;
355 }
356 const flexDue = paymentMethod === 'deposit' || paymentMethod === 'partial';
357
358 if (isOffline) {
359 $buttonText.text(window.yatraBookingData?.i18n?.complete_booking || __('Complete Booking', 'yatra'));
360 const showAmount = flexDue ? due : totalSafe;
361 if (showAmount > 0) {
362 $payAmount.text(formatCurrency(showAmount, currency)).show();
363 } else {
364 $payAmount.hide();
365 }
366 } else {
367 $buttonText.text(window.yatraBookingData?.i18n?.pay_now || __('Pay Now', 'yatra'));
368 const payVal = flexDue && due > 0 ? due : totalSafe;
369 if (payVal > 0) {
370 $payAmount.text(formatCurrency(payVal, currency)).show();
371 } else {
372 $payAmount.hide();
373 }
374 }
375 }
376
377 /**
378 * Add a new traveler form dynamically
379 */
380 function addTravelerForm(index) {
381 const $container = $('#yatra-travelers-container');
382
383 // Get field configuration from the first traveler form
384 const $firstTraveler = $container.find('.yatra-traveler-form').first();
385 if (!$firstTraveler.length) return;
386
387 // Clone the first traveler form
388 const $newTraveler = $firstTraveler.clone();
389
390 // Additional travelers are never the lead, so drop any fields the
391 // operator marked "Lead traveler only" (data-applies-to="lead").
392 // Traveler 1 is rendered server-side with these; clones must not
393 // carry them. ('all'/unset fields stay.)
394 $newTraveler.find('[data-applies-to="lead"]').remove();
395
396 // Update index references
397 $newTraveler.attr('data-traveler-index', index);
398 $newTraveler.find('.yatra-traveler-title').text(
399 /* translators: %d: traveler sequence number. */
400 sprintf(__('Traveler %d', 'yatra'), index)
401 );
402
403 // Add "Additional traveler" note. The class wrapper is
404 // markup (not translatable); only the visible label runs
405 // through __() so translators see a clean msgid in the
406 // pot rather than HTML noise.
407 if (!$newTraveler.find('.yatra-traveler-note').length) {
408 $newTraveler.find('.yatra-traveler-header').append(
409 '<span class="yatra-traveler-note">' + __('Additional traveler', 'yatra') + '</span>'
410 );
411 }
412
413 // Update all field IDs and names
414 $newTraveler.find('input, select, textarea').each(function() {
415 const $field = $(this);
416 const oldId = $field.attr('id');
417 const oldName = $field.attr('name');
418
419 if (oldId) {
420 // Replace traveler-1- with traveler-{index}-
421 const newId = oldId.replace(/traveler-\d+-/, 'traveler-' + index + '-');
422 $field.attr('id', newId);
423 }
424
425 if (oldName) {
426 // Replace travelers[1] with travelers[{index}]
427 const newName = oldName.replace(/travelers\[\d+\]/, 'travelers[' + index + ']');
428 $field.attr('name', newName);
429 }
430
431 // Clear values
432 $field.val('');
433 $field.removeClass('error');
434 });
435
436 // Update associated labels
437 $newTraveler.find('label').each(function() {
438 const $label = $(this);
439 const forAttr = $label.attr('for');
440 if (forAttr) {
441 const newFor = forAttr.replace(/traveler-\d+-/, 'traveler-' + index + '-');
442 $label.attr('for', newFor);
443 }
444 });
445
446 $container.append($newTraveler);
447 }
448
449 /**
450 * Remove the last traveler form
451 */
452 function removeTravelerForm(index) {
453 const $container = $('#yatra-travelers-container');
454 $container.find(`.yatra-traveler-form[data-traveler-index="${index}"]`).remove();
455 }
456
457 /**
458 * Convert FormData to nested object structure
459 */
460 function formDataToObject(formData) {
461 const obj = {};
462
463 for (const [key, value] of formData.entries()) {
464 // Handle array notation like travelers[1][first_name]
465 const matches = key.match(/^([^\[]+)(?:\[([^\]]*)\])?(?:\[([^\]]*)\])?$/);
466
467 if (matches) {
468 const [, base, index, field] = matches;
469
470 if (index !== undefined && field !== undefined) {
471 // Array of objects: travelers[1][first_name]
472 if (!obj[base]) obj[base] = {};
473 if (!obj[base][index]) obj[base][index] = {};
474 obj[base][index][field] = value;
475 } else if (index !== undefined) {
476 // Array notation. PHP/HTML produces two flavours:
477 // items[0], items[1] → keyed indices
478 // items[] → push to end (multi-checkbox)
479 // The empty-string variant is what `<input name="additional_services[]">`
480 // sends when several checkboxes are selected. Previously
481 // we did `obj[base][""] = value` which silently overwrote
482 // on each entry and JSON-serialised to `[]` — so the
483 // server saw no selected services, even when boxes were
484 // checked.
485 if (!obj[base]) obj[base] = [];
486 if (index === '') {
487 obj[base].push(value);
488 } else {
489 obj[base][index] = value;
490 }
491 } else {
492 // Simple key
493 obj[key] = value;
494 }
495 } else {
496 obj[key] = value;
497 }
498 }
499
500 // Convert travelers object to array
501 if (obj.travelers && typeof obj.travelers === 'object') {
502 obj.travelers = Object.values(obj.travelers);
503 }
504
505 return obj;
506 }
507
508 /**
509 * Validate form before submission
510 */
511 function validateForm() {
512 if (isRemainingPayment) {
513 return true;
514 }
515
516 let isValid = true;
517 const errors = [];
518
519 // Remove previous error states
520 $form.find('.error').removeClass('error');
521 $('.yatra-form-error').remove();
522
523 // Validate all required fields
524 $form.find('input[required], select[required], textarea[required]').each(function() {
525 const $field = $(this);
526 const type = ($field.attr('type') || '').toLowerCase();
527
528 // Checkbox/radio: .val() returns the value attribute regardless of
529 // checked state, so an unchecked required box would wrongly pass.
530 // Test the checked state instead.
531 let invalid;
532 if (type === 'checkbox' || type === 'radio') {
533 invalid = !$field.is(':checked');
534 } else {
535 const value = $field.val();
536 invalid = !value || value.trim() === '';
537 }
538
539 if (invalid) {
540 isValid = false;
541 $field.addClass('error');
542 const label = $field.closest('.yatra-form-group').find('label').text().replace('*', '').trim();
543 errors.push(sprintf(
544 /* translators: %s: form field label. */
545 __('%s is required', 'yatra'),
546 label
547 ));
548 }
549 });
550
551 // Validate email format
552 const emailFields = $form.find('input[type="email"]');
553 emailFields.each(function() {
554 const $field = $(this);
555 const value = $field.val();
556 if (value && !isValidEmail(value)) {
557 isValid = false;
558 $field.addClass('error');
559 errors.push(__('Please enter a valid email address', 'yatra'));
560 }
561 });
562
563 // Validate terms checkbox
564 if (!$('input[name="accept_terms"]').is(':checked')) {
565 isValid = false;
566 errors.push(__('Please accept the Terms and Conditions', 'yatra'));
567 }
568
569 // Validate privacy checkbox
570 if (!$('input[name="accept_privacy"]').is(':checked')) {
571 isValid = false;
572 errors.push(__('Please accept the Privacy Policy', 'yatra'));
573 }
574
575 if (!isValid && errors.length > 0) {
576 showFormError(errors[0]);
577 }
578
579 return isValid;
580 }
581
582 /**
583 * Validate email format
584 */
585 function isValidEmail(email) {
586 return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
587 }
588
589 /**
590 * Show form error message
591 */
592 function showFormError(message) {
593 const $errorContainer = $('#yatra-booking-form-errors');
594 if ($errorContainer.length) {
595 $errorContainer.html('<div class="yatra-form-error" role="alert">' + message + '</div>');
596 $('html, body').animate({ scrollTop: $errorContainer.offset().top - 100 }, 300);
597 } else {
598 alert(message);
599 }
600 }
601
602 /**
603 * Show success message
604 * @param {string} message
605 * @param {string} reference
606 * @param {{ heading?: string, footerHtml?: string, iconColor?: string }|undefined} opts
607 */
608 function showSuccessMessage(message, reference, opts) {
609 opts = opts || {};
610 const heading = opts.heading || __('Booking Confirmed!', 'yatra');
611 const iconColor = opts.iconColor || '#22c55e';
612 const confirmationEmailText = __('A confirmation email has been sent to your email address.', 'yatra');
613 const footerHtml = opts.footerHtml !== undefined
614 ? opts.footerHtml
615 : '<p style="margin-top: 24px; color: #6b7280;">' + confirmationEmailText + '</p>';
616 const referenceText = sprintf(
617 /* translators: %s: booking reference number. */
618 __('Reference: %s', 'yatra'),
619 reference
620 );
621 const $success = $('<div class="yatra-booking-success" style="text-align: center; padding: 60px 20px;">' +
622 '<svg style="width: 80px; height: 80px; color: ' + iconColor + '; margin-bottom: 24px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
623 '<circle cx="12" cy="12" r="10"></circle><polyline points="9,12 12,15 16,10"></polyline></svg>' +
624 '<h2 style="font-size: 28px; margin-bottom: 12px; color: #111827;">' + heading + '</h2>' +
625 '<p style="font-size: 18px; color: #6b7280; margin-bottom: 8px;">' + message + '</p>' +
626 '<p style="font-size: 16px; color: #111827; font-weight: 600;">' + referenceText + '</p>' +
627 footerHtml +
628 '</div>');
629
630 $form.html($success);
631 }
632
633 // =====================
634 // Event Handlers
635 // =====================
636
637 // Dropdown toggle for traveler selector
638 $(document).on('click', '.yatra-booking-participants-select', function(e) {
639 // Don't toggle if clicking on buttons or inputs inside
640 if ($(e.target).closest('.yatra-qty-btn, .yatra-qty-input, .yatra-quantity-controls').length) {
641 return;
642 }
643
644 const $select = $(this);
645 const wasActive = $select.hasClass('active');
646
647 // Close all dropdowns first
648 $('.yatra-booking-participants-select').removeClass('active');
649
650 // Toggle this one
651 if (!wasActive) {
652 $select.addClass('active');
653 }
654 });
655
656 // Close dropdown when clicking outside
657 $(document).on('click', function(e) {
658 if (!$(e.target).closest('.yatra-booking-participants-select').length) {
659 $('.yatra-booking-participants-select').removeClass('active');
660 }
661 });
662
663 // Quantity selector for regular pricing (dropdown style)
664 $(document).on('click', '.yatra-qty-btn[data-field="travelers"]', function(e) {
665 e.preventDefault();
666 e.stopPropagation();
667
668 const $btn = $(this);
669 const $input = $('#number-of-travelers');
670 let currentValue = parseInt($input.val()) || 1;
671 const min = parseInt($input.attr('min')) || 1;
672 const max = parseInt($input.attr('max')) || 20;
673
674 if ($btn.hasClass('yatra-qty-plus')) {
675 if (currentValue < max) {
676 currentValue++;
677 addTravelerForm(currentValue);
678 }
679 } else if ($btn.hasClass('yatra-qty-minus')) {
680 if (currentValue > min) {
681 removeTravelerForm(currentValue);
682 currentValue--;
683 }
684 }
685
686 $input.val(currentValue);
687
688 // Update button disabled states
689 $btn.closest('.yatra-quantity-controls').find('.yatra-qty-minus').prop('disabled', currentValue <= min);
690 $btn.closest('.yatra-quantity-controls').find('.yatra-qty-plus').prop('disabled', currentValue >= max);
691
692 updateBookingSession();
693 });
694
695 // Quantity selector for traveler-based category pricing (dropdown style)
696 $(document).on('click', '.yatra-qty-btn[data-category]', function(e) {
697 e.preventDefault();
698 e.stopPropagation();
699
700 const $btn = $(this);
701 const $row = $btn.closest('.yatra-quantity-row');
702 const $input = $row.find('.yatra-qty-input');
703 let currentValue = parseInt($input.val()) || 0;
704 const min = parseInt($input.attr('min')) || 0;
705 const max = parseInt($input.attr('max')) || 20;
706
707 if ($btn.hasClass('yatra-qty-plus')) {
708 if (currentValue < max) {
709 currentValue++;
710 }
711 } else if ($btn.hasClass('yatra-qty-minus')) {
712 if (currentValue > min) {
713 currentValue--;
714 }
715 }
716
717 $input.val(currentValue);
718
719 // Update button disabled states
720 $row.find('.yatra-qty-minus').prop('disabled', currentValue <= min);
721 $row.find('.yatra-qty-plus').prop('disabled', currentValue >= max);
722
723 // Update traveler forms based on total count
724 updateTravelerFormsForCategories();
725
726 // Update travelers display text immediately
727 updateTravelersDisplayText();
728
729
730 updateBookingSession();
731 });
732
733 /**
734 * Update traveler forms based on category selections
735 */
736 function updateTravelerFormsForCategories() {
737 const targetCount = getTravelerCount();
738 const currentFormCount = $('.yatra-traveler-form').length;
739
740 if (targetCount > currentFormCount) {
741 // Add forms
742 for (let i = currentFormCount + 1; i <= targetCount; i++) {
743 addTravelerForm(i);
744 }
745 } else if (targetCount < currentFormCount && targetCount > 0) {
746 // Remove forms
747 for (let i = currentFormCount; i > targetCount; i--) {
748 removeTravelerForm(i);
749 }
750 }
751 }
752
753 /**
754 * Update booking session with new traveler counts
755 * This ensures checkout/confirmation reflects the updated travelers
756 */
757 function updateBookingSession() {
758 const sessionPayload = {
759 travelers: getTravelerCount()
760 };
761
762 // Add traveler_counts for category-based pricing
763 if (isTravelerBasedPricing()) {
764 const travelerCounts = {};
765 $('.yatra-qty-input[data-category-id]').each(function() {
766 const categoryId = $(this).data('category-id');
767 const count = parseInt($(this).val()) || 0;
768 if (categoryId !== undefined) {
769 travelerCounts[categoryId] = count;
770 }
771 });
772 sessionPayload.traveler_counts = travelerCounts;
773 }
774
775 // Update session via REST API (needs session cookie, but no nonce)
776 const { base: restBase, isPlain } = getRestBase();
777 const siteBase = (window.yatraBookingData?.siteUrl || window.location.origin || '').replace(/\/$/, '');
778 let sessionUrl;
779 if (isPlain) {
780 sessionUrl = `${siteBase}/?rest_route=/yatra/v1/booking/session`;
781 } else if (restBase.includes('/yatra/v1')) {
782 sessionUrl = `${restBase}/booking/session`;
783 } else {
784 sessionUrl = `${restBase}/yatra/v1/booking/session`;
785 }
786
787 fetch(sessionUrl, {
788 method: 'POST',
789 credentials: 'same-origin',
790 headers: {
791 'Content-Type': 'application/json',
792 'X-WP-Nonce': nonce
793 },
794 body: JSON.stringify(sessionPayload)
795 })
796 .then(response => response.json())
797 .then(data => {
798 if (!data.success) {
799 console.warn('Failed to update booking session:', data.message);
800 return;
801 }
802 schedulePricingSummaryRefresh();
803 })
804 .catch(error => {
805 console.error('Error updating booking session:', error);
806 });
807 }
808
809 // Initial summary figures from server-rendered sidebar (before any AJAX)
810 if (!isRemainingPayment) {
811 const $sbInit = $('.yatra-booking-summary').first();
812 if ($sbInit.length) {
813 const t = parseFloat($sbInit.attr('data-summary-total'));
814 const du = parseFloat($sbInit.attr('data-summary-due'));
815 window.yatraBookingSummary = {
816 totalAmount: Number.isNaN(t) ? null : t,
817 amountDue: Number.isNaN(du) ? null : du,
818 amountPaid: 0
819 };
820 }
821 updateCheckoutButtonState();
822 }
823
824 $(document).on('change', 'input[name="payment_method"]', function() {
825 schedulePricingSummaryRefresh();
826 });
827
828 // Update when gateway changes
829 $('input[name="payment_gateway"]').on('change', function() {
830
831 // Show gateway-specific info
832 const gateway = $(this).val();
833 const $gatewayInfo = $('#yatra-gateway-info');
834 const $gatewayDetails = $('#yatra-gateway-details');
835
836 const gatewayMessages = {
837 'pay_later': '<p>' + __('Your booking will be reserved. Full payment is required before the trip date.', 'yatra') + '</p>',
838 '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>',
839 'stripe': '<p>' + __('You will be securely redirected to complete your payment with credit or debit card.', 'yatra') + '</p>',
840 'paypal': '<p>' + __('You will be redirected to PayPal to complete your payment securely.', 'yatra') + '</p>',
841 'razorpay': '<p>' + __('You will be redirected to Razorpay to complete your payment.', 'yatra') + '</p>',
842 'esewa': '<p>' + __('You will be redirected to eSewa to complete your payment.', 'yatra') + '</p>',
843 'khalti': '<p>' + __('You will be redirected to Khalti to complete your payment.', 'yatra') + '</p>',
844 };
845
846 if (gatewayMessages[gateway] && $gatewayDetails.length) {
847 $gatewayDetails.html(gatewayMessages[gateway]);
848 $gatewayInfo.show();
849 } else if ($gatewayInfo.length) {
850 $gatewayInfo.hide();
851 }
852
853 updateCheckoutButtonState();
854
855 // Toggle gateway-specific content containers
856 $('.yatra-gateway-extra').removeClass('active');
857 const $gatewayExtra = $('#yatra-gateway-extra-' + gateway);
858 // Only show if it has content (children elements)
859 if ($gatewayExtra.length && $gatewayExtra.children().length > 0) {
860 $gatewayExtra.addClass('active');
861 }
862 });
863
864 // Ensure default-selected gateway shows its content on load
865 const $defaultGateway = $('input[name="payment_gateway"]:checked');
866 if ($defaultGateway.length) {
867 $defaultGateway.trigger('change');
868 }
869
870 /**
871 * ============================================
872 * CENTRALIZED PAYMENT GATEWAY HOOK SYSTEM
873 * ============================================
874 *
875 * Events:
876 * - yatra_booking_submit: Fired when form is submitted, gateways can intercept
877 * - yatra_payment_response: Fired when server responds with payment data
878 * - yatra_payment_success: Fired when payment completes successfully
879 * - yatra_payment_failed: Fired when payment fails
880 * - yatra_payment_cancelled: Fired when user cancels payment
881 */
882
883 // Store for registered gateway handlers
884 window.yatraPaymentGateways = window.yatraPaymentGateways || {};
885
886 /**
887 * Register a payment gateway handler
888 * @param {string} gatewayId - Gateway identifier (e.g., 'stripe', 'razorpay')
889 * @param {object} handler - Handler object with methods: canHandle, handlePayment
890 */
891 window.yatraRegisterPaymentGateway = function(gatewayId, handler) {
892
893 window.yatraPaymentGateways[gatewayId] = handler;
894 };
895
896 /**
897 * Handle payment response from server
898 */
899 function handlePaymentResponse(response, originalBtnHtml) {
900
901
902
903 if (!response.success) {
904 const errMsg = response.message || __('An error occurred. Please try again.', 'yatra');
905 showFormError(errMsg);
906 $submitBtn.prop('disabled', false).html(originalBtnHtml);
907 return;
908 }
909
910 // Guest email verification path — booking is held in
911 // pending_verification until the customer clicks the
912 // magic link in their inbox. Render a "check your email"
913 // success screen and DO NOT redirect to payment.
914 if (response.code === 'email_verification_required') {
915 const data = response.data || {};
916 const reference = data.reference || __('N/A', 'yatra');
917 const email = data.email || '';
918 showSuccessMessage(
919 response.message || __("We've sent a verification email. Click the link to complete your booking.", 'yatra'),
920 reference,
921 {
922 heading: __('Check your email', 'yatra'),
923 iconColor: '#2563eb',
924 footerHtml:
925 '<p style="margin-top: 20px; color: #374151;">' +
926 __('We sent a verification link to:', 'yatra') +
927 ' <strong>' + (email || __('your email', 'yatra')) + '</strong>' +
928 '</p>' +
929 '<p style="margin-top: 12px; color: #6b7280; font-size: 14px;">' +
930 __("Didn't see it? Check your spam folder. The link expires in 48 hours.", 'yatra') +
931 '</p>'
932 }
933 );
934 return;
935 }
936
937 const data = response.data || {};
938
939 if (data.waitlist) {
940 const reference = data.reference || __('N/A', 'yatra');
941 showSuccessMessage(
942 response.message || __("You're on the waitlist for this departure.", 'yatra'),
943 reference,
944 {
945 heading: __("You're on the waitlist", 'yatra'),
946 iconColor: '#ca8a04',
947 footerHtml: '<p style="margin-top: 24px; color: #6b7280;">' + __('We will contact you if a space opens up.', 'yatra') + '</p>'
948 }
949 );
950 return;
951 }
952
953
954 // Check for redirect URLs first (PayPal, eSewa, Khalti, etc.)
955 if (data.payment_url) {
956
957 window.location.href = data.payment_url;
958 return;
959 }
960
961 if (data.redirect_url && !data.requires_action) {
962
963 window.location.href = data.redirect_url;
964 return;
965 }
966
967 // Check for client-side payment actions (Stripe, Razorpay, Square, etc.)
968 if (data.requires_action) {
969
970
971 // Dispatch unified payment event
972 const paymentEvent = new CustomEvent('yatra_payment_response', {
973 detail: {
974 ...data,
975 originalBtnHtml,
976 resetButton: () => $submitBtn.prop('disabled', false).html(originalBtnHtml)
977 }
978 });
979 document.dispatchEvent(paymentEvent);
980
981 // Setup failure/cancel listeners
982 const cleanup = () => {
983 document.removeEventListener('yatra_payment_failed', onFailed);
984 document.removeEventListener('yatra_payment_cancelled', onCancelled);
985 };
986 const onFailed = (e) => {
987 showFormError(e.detail?.error || __('Payment failed. Please try again.', 'yatra'));
988 $submitBtn.prop('disabled', false).html(originalBtnHtml);
989 cleanup();
990 };
991 const onCancelled = () => {
992 $submitBtn.prop('disabled', false).html(originalBtnHtml);
993 cleanup();
994 };
995 document.addEventListener('yatra_payment_failed', onFailed, { once: true });
996 document.addEventListener('yatra_payment_cancelled', onCancelled, { once: true });
997 return;
998 }
999
1000 // Default: show success message
1001 const reference = data.reference || __('N/A', 'yatra');
1002 showSuccessMessage(response.message || __('Success!', 'yatra'), reference);
1003 }
1004
1005 // Form submission - using FormData API
1006 $form.on('submit', function(e) {
1007 e.preventDefault();
1008
1009 // Validate form
1010 if (!validateForm()) {
1011 return;
1012 }
1013
1014 const originalBtnHtml = $submitBtn.html();
1015 const selectedGateway = $('input[name="payment_gateway"]:checked').val();
1016
1017 // Collect form data using FormData API
1018 const formData = new FormData(this);
1019
1020 // Convert to structured object
1021 const bookingData = formDataToObject(formData);
1022 const summary = window.yatraBookingSummary || {};
1023
1024 // Add checkbox values explicitly (unchecked checkboxes aren't in FormData)
1025 bookingData.accept_terms = $('input[name="accept_terms"]').is(':checked');
1026 bookingData.accept_privacy = $('input[name="accept_privacy"]').is(':checked');
1027 bookingData.subscribe_newsletter = $('input[name="subscribe_newsletter"]').is(':checked');
1028 bookingData.payment_due = parseFloat($form.attr('data-payment-due')) || window.yatraBookingData?.paymentDue || null;
1029 // The pay button uses a fallback chain (summary.totalAmount → data-summary-total
1030 // attribute → data-payment-due) so it can show "$279" even before the async
1031 // pricing refresh populates `summary.totalAmount`. The submit path must walk
1032 // the same chain — otherwise the button label and the request body disagree
1033 // and BookingService rejects with "Total amount must be greater than zero."
1034 const $summaryEl = $('.yatra-booking-summary').first();
1035 const resolveAmount = (preferred, attr) => {
1036 const n = Number(preferred);
1037 if (Number.isFinite(n) && n > 0) return n;
1038 const a = parseFloat($summaryEl.attr(attr));
1039 if (Number.isFinite(a) && a > 0) return a;
1040 return null;
1041 };
1042 bookingData.total_amount =
1043 resolveAmount(summary.totalAmount, 'data-summary-total') ??
1044 resolveAmount(summary.amountDue, 'data-summary-due') ??
1045 (parseFloat($form.attr('data-payment-due')) || null);
1046 bookingData.amount_due =
1047 resolveAmount(summary.amountDue, 'data-summary-due') ??
1048 (parseFloat($form.attr('data-payment-due')) || null);
1049 bookingData.amount_paid = summary.amountPaid ?? null;
1050 bookingData.currency = window.yatraBookingData?.currency || bookingData.currency || $('input[name="currency"]').val() || 'USD';
1051 // Carry the booking_token from the page URL into the REST request.
1052 // Without this the server's `create_booking` cannot rehydrate the
1053 // PHP session for the user (REST runs in a separate request and
1054 // PHPSESSID propagation isn't guaranteed). Server falls back to
1055 // looking up the transient by this token to recover `traveler_counts`,
1056 // `pricing_type`, `price_types` — without those, `calculatePricing`
1057 // returns 0 for `traveler_based` trips and the booking gets
1058 // rejected with "Total amount must be greater than zero."
1059 try {
1060 const urlToken = new URLSearchParams(window.location.search).get('booking_token');
1061 if (urlToken) {
1062 bookingData.booking_token = urlToken;
1063 }
1064 } catch (e) { /* URLSearchParams missing — ignore */ }
1065
1066 // Dispatch unified booking submit event - gateways can intercept
1067 const submitEvent = new CustomEvent('yatra_booking_submit', {
1068 detail: {
1069 gateway: selectedGateway,
1070 form: this,
1071 bookingData,
1072 submitButton: $submitBtn[0],
1073 originalBtnHtml,
1074 // Helper to proceed with default submission
1075 proceedWithSubmission: () => submitBookingToServer(bookingData, originalBtnHtml)
1076 },
1077 cancelable: true
1078 });
1079
1080
1081 const shouldProceed = document.dispatchEvent(submitEvent);
1082
1083 // If event was not cancelled, proceed with server submission
1084 if (shouldProceed) {
1085 submitBookingToServer(bookingData, originalBtnHtml);
1086 }
1087 });
1088
1089 /**
1090 * Submit booking to server
1091 */
1092 function submitBookingToServer(bookingData, originalBtnHtml) {
1093 const { base: restBase, isPlain } = getRestBase();
1094 const siteBase = (window.yatraBookingData?.siteUrl || window.location.origin || '').replace(/\/$/, '');
1095 let createUrl;
1096 if (isPlain) {
1097 createUrl = `${siteBase}/?rest_route=/yatra/v1/booking/create`;
1098 } else if (restBase.includes('/yatra/v1')) {
1099 createUrl = `${restBase}/booking/create`;
1100 } else {
1101 createUrl = `${restBase}/yatra/v1/booking/create`;
1102 }
1103
1104 // Show loading state
1105 $submitBtn.prop('disabled', true).html(
1106 '<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">' +
1107 '<circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"></circle></svg>' +
1108 '<span>' + __('Processing...', 'yatra') + '</span>'
1109 );
1110
1111 // CSRF protection — the booking REST endpoint's
1112 // permission_callback intentionally bypasses WP's default
1113 // cookie/nonce check (so guests can hit it at all). The
1114 // server validates this booking-scoped action nonce
1115 // instead. Falls back to the hidden form field on the
1116 // booking page when the localized var isn't present (e.g.
1117 // older cached scripts).
1118 const bookingNonce = window.yatraBookingData?.bookingNonce
1119 || $('input[name="yatra_booking_nonce"]').val()
1120 || '';
1121
1122 fetch(createUrl, {
1123 method: 'POST',
1124 headers: {
1125 'Content-Type': 'application/json',
1126 'X-WP-Nonce': nonce,
1127 'X-Yatra-Booking-Nonce': bookingNonce
1128 },
1129 credentials: 'same-origin',
1130 body: JSON.stringify(bookingData)
1131 })
1132 .then(response => response.json())
1133 .then(response => handlePaymentResponse(response, originalBtnHtml))
1134 .catch(error => {
1135 console.error('Error:', error);
1136 showFormError(__('An error occurred. Please try again.', 'yatra'));
1137 $submitBtn.prop('disabled', false).html(originalBtnHtml);
1138 });
1139 }
1140
1141 // Toggle coupon form visibility
1142 $('#yatra-coupon-toggle-btn').on('click', function() {
1143 const $form = $('#yatra-coupon-form');
1144 const $chevron = $(this).find('.yatra-coupon-chevron');
1145
1146 if ($form.is(':visible')) {
1147 $form.slideUp(200);
1148 $chevron.css('transform', 'rotate(0deg)');
1149 } else {
1150 $form.slideDown(200);
1151 $chevron.css('transform', 'rotate(180deg)');
1152 // Focus the input field
1153 setTimeout(() => $('#yatra-coupon-code').focus(), 200);
1154 }
1155 });
1156
1157 // Apply coupon
1158 $('#yatra-apply-coupon').on('click', function() {
1159 applyCoupon();
1160 });
1161
1162 // Apply coupon on Enter key
1163 $('#yatra-coupon-code').on('keypress', function(e) {
1164 if (e.which === 13) {
1165 e.preventDefault();
1166 applyCoupon();
1167 }
1168 });
1169
1170 // Remove coupon
1171 $('#yatra-remove-coupon').on('click', function() {
1172 removeCoupon();
1173 });
1174
1175 function applyCoupon() {
1176 const code = $('#yatra-coupon-code').val().trim().toUpperCase();
1177 const $btn = $('#yatra-apply-coupon');
1178 const $message = $('#yatra-coupon-message');
1179
1180 if (!code) {
1181 showCouponMessage(__('Please enter a coupon code.', 'yatra'), 'error');
1182 return;
1183 }
1184
1185 // Show loading state
1186 $btn.prop('disabled', true).text(__('Applying...', 'yatra'));
1187 $message.hide();
1188
1189 // Carry booking_token so the REST endpoint can rehydrate the
1190 // session in contexts where PHPSESSID isn't passed through —
1191 // same pattern as the service-toggle and summary refresh.
1192 const couponPayload = { code: code };
1193 try {
1194 const tok = new URLSearchParams(window.location.search).get('booking_token');
1195 if (tok) couponPayload.booking_token = tok;
1196 } catch (e) { /* URLSearchParams unavailable — skip */ }
1197
1198 fetch(apiUrl + '/booking/coupon/apply', {
1199 method: 'POST',
1200 headers: {
1201 'Content-Type': 'application/json',
1202 'X-WP-Nonce': nonce
1203 },
1204 credentials: 'same-origin',
1205 body: JSON.stringify(couponPayload)
1206 })
1207 .then(response => response.json())
1208 .then(response => {
1209 $btn.prop('disabled', false).text(__('Apply', 'yatra'));
1210
1211 if (response.success) {
1212 appliedCoupon = response.data;
1213
1214 // Hide the form and show applied coupon
1215 $('#yatra-coupon-form').hide();
1216 $('#yatra-coupon-toggle-btn').hide();
1217
1218 // Show applied coupon display
1219 const $applied = $('#yatra-applied-coupon');
1220 $applied.find('.yatra-coupon-code-display').text(response.data.code);
1221 $applied.find('.yatra-coupon-discount').text('-' + response.data.discount_formatted);
1222 $applied.show();
1223
1224 // Update price display
1225 updatePriceWithDiscount(response.data);
1226
1227 showCouponMessage(response.message, 'success');
1228 } else {
1229 showCouponMessage(response.message || __('Invalid coupon code.', 'yatra'), 'error');
1230 }
1231 })
1232 .catch(error => {
1233 console.error('Coupon error:', error);
1234 $btn.prop('disabled', false).text(__('Apply', 'yatra'));
1235 showCouponMessage(__('An error occurred. Please try again.', 'yatra'), 'error');
1236 });
1237 }
1238
1239 function removeCoupon() {
1240 const $btn = $('#yatra-remove-coupon');
1241
1242 $btn.prop('disabled', true);
1243
1244 const removePayload = {};
1245 try {
1246 const tok = new URLSearchParams(window.location.search).get('booking_token');
1247 if (tok) removePayload.booking_token = tok;
1248 } catch (e) { /* URLSearchParams unavailable — skip */ }
1249
1250 fetch(apiUrl + '/booking/coupon/remove', {
1251 method: 'POST',
1252 headers: {
1253 'Content-Type': 'application/json',
1254 'X-WP-Nonce': nonce
1255 },
1256 credentials: 'same-origin',
1257 body: JSON.stringify(removePayload)
1258 })
1259 .then(response => response.json())
1260 .then(response => {
1261 $btn.prop('disabled', false);
1262
1263 if (response.success) {
1264 appliedCoupon = null;
1265
1266 // Hide applied coupon and show toggle button
1267 $('#yatra-applied-coupon').hide();
1268 $('#yatra-coupon-toggle-btn').show();
1269 $('#yatra-coupon-code').val('');
1270 $('#yatra-coupon-message').hide();
1271
1272 // Refresh pricing summary via AJAX
1273 refreshPricingSummary();
1274 }
1275 })
1276 .catch(error => {
1277 console.error('Remove coupon error:', error);
1278 $btn.prop('disabled', false);
1279 });
1280 }
1281
1282 function showCouponMessage(message, type) {
1283 const $message = $('#yatra-coupon-message');
1284 $message.removeClass('success error').addClass(type);
1285 $message.text(message).show();
1286
1287 // Auto-hide success messages
1288 if (type === 'success') {
1289 setTimeout(() => $message.fadeOut(), 3000);
1290 }
1291 }
1292
1293 function applySummaryResponse(response) {
1294 if (!response.success || !response.data) {
1295 return;
1296 }
1297 const d = response.data;
1298 if (d.pricing_html) {
1299 $('#yatra-summary-pricing').html(d.pricing_html);
1300 }
1301 window.yatraBookingSummary = {
1302 totalAmount: typeof d.total_amount === 'number' ? d.total_amount : parseFloat(d.total_amount),
1303 amountDue: typeof d.amount_due === 'number' ? d.amount_due : parseFloat(d.amount_due),
1304 amountPaid: typeof d.amount_paid === 'number' ? d.amount_paid : parseFloat(d.amount_paid || 0) || 0
1305 };
1306 const $sb = $('.yatra-booking-summary').first();
1307 if ($sb.length && !Number.isNaN(window.yatraBookingSummary.totalAmount)) {
1308 $sb.attr('data-summary-total', String(window.yatraBookingSummary.totalAmount));
1309 }
1310 if ($sb.length && !Number.isNaN(window.yatraBookingSummary.amountDue)) {
1311 $sb.attr('data-summary-due', String(window.yatraBookingSummary.amountDue));
1312 $form.attr('data-payment-due', String(window.yatraBookingSummary.amountDue));
1313 }
1314 updateCheckoutButtonState();
1315 }
1316
1317 function refreshPricingSummary(extraPayload) {
1318 const { base: restBase, isPlain } = getRestBase();
1319 const siteBase = (window.yatraBookingData?.siteUrl || window.location.origin || '').replace(/\/$/, '');
1320 let summaryUrl;
1321 if (isPlain) {
1322 summaryUrl = `${siteBase}/?rest_route=/yatra/v1/booking/summary`;
1323 } else if (restBase.includes('/yatra/v1')) {
1324 summaryUrl = `${restBase}/booking/summary`;
1325 } else {
1326 summaryUrl = `${restBase}/yatra/v1/booking/summary`;
1327 }
1328
1329 const body = Object.assign(buildPricingSummaryPayload(), extraPayload || {});
1330
1331 fetch(summaryUrl, {
1332 method: 'POST',
1333 headers: {
1334 'Content-Type': 'application/json',
1335 'X-WP-Nonce': nonce
1336 },
1337 credentials: 'same-origin',
1338 body: JSON.stringify(body)
1339 })
1340 .then(response => response.json())
1341 .then(response => {
1342 applySummaryResponse(response);
1343 })
1344 .catch(error => {
1345 console.error('Error refreshing pricing summary:', error);
1346 });
1347 }
1348
1349 function schedulePricingSummaryRefresh() {
1350 if (isRemainingPayment) {
1351 return;
1352 }
1353 clearTimeout(summaryDebounceTimer);
1354 summaryDebounceTimer = setTimeout(function() {
1355 refreshPricingSummary();
1356 }, 250);
1357 }
1358
1359 function updatePriceWithDiscount(couponData) {
1360 // Refresh pricing summary after coupon applied
1361 refreshPricingSummary();
1362 }
1363
1364 /**
1365 * Load existing coupon from session and display it
1366 */
1367 function loadCouponFromSession() {
1368 const { base: restBase, isPlain } = getRestBase();
1369 const siteBase = (window.yatraBookingData?.siteUrl || window.location.origin || '').replace(/\/$/, '');
1370 let sessionUrl;
1371 if (isPlain) {
1372 sessionUrl = `${siteBase}/?rest_route=/yatra/v1/booking/session`;
1373 } else if (restBase.includes('/yatra/v1')) {
1374 sessionUrl = `${restBase}/booking/session`;
1375 } else {
1376 sessionUrl = `${restBase}/yatra/v1/booking/session`;
1377 }
1378
1379 // Append booking_token from the page URL so the REST endpoint can
1380 // rehydrate the session via transient when PHPSESSID hasn't been
1381 // carried over — otherwise this GET returns "No active session"
1382 // and we never reveal the applied-coupon UI (and its remove
1383 // button) after a page refresh.
1384 try {
1385 const tok = new URLSearchParams(window.location.search).get('booking_token');
1386 if (tok) {
1387 const sep = sessionUrl.includes('?') ? '&' : '?';
1388 sessionUrl = sessionUrl + sep + 'booking_token=' + encodeURIComponent(tok);
1389 }
1390 } catch (e) { /* URLSearchParams unavailable — skip */ }
1391
1392 fetch(sessionUrl, {
1393 method: 'GET',
1394 credentials: 'same-origin',
1395 headers: {
1396 'Content-Type': 'application/json',
1397 'X-WP-Nonce': nonce
1398 },
1399 })
1400 .then(response => response.json())
1401 .then(response => {
1402 if (response.success && response.data && response.data.coupon) {
1403 const coupon = response.data.coupon;
1404 // Calculate discount formatted if not present
1405 const discountFormatted = coupon.discount_formatted || formatCurrency(coupon.discount_amount || 0, currency);
1406
1407 appliedCoupon = {
1408 code: coupon.code,
1409 type: coupon.type,
1410 discount_amount: parseFloat(coupon.discount_amount) || 0,
1411 discount_formatted: discountFormatted,
1412 new_total: coupon.new_total || 0,
1413 new_total_formatted: coupon.new_total_formatted || ''
1414 };
1415
1416 // Hide the form and show applied coupon
1417 $('#yatra-coupon-form').hide();
1418 $('#yatra-coupon-toggle-btn').hide();
1419
1420 // Show applied coupon display
1421 const $applied = $('#yatra-applied-coupon');
1422 $applied.find('.yatra-coupon-code-display').text(coupon.code);
1423 $applied.find('.yatra-coupon-discount').text('-' + appliedCoupon.discount_formatted);
1424 $applied.show();
1425
1426 // Update price display
1427 updatePriceWithDiscount(appliedCoupon);
1428 }
1429 })
1430 .catch(error => {
1431 console.error('Error loading coupon from session:', error);
1432 });
1433 }
1434
1435 /**
1436 * Check URL for coupon parameter and auto-apply
1437 */
1438 function checkUrlForCoupon() {
1439 const urlParams = new URLSearchParams(window.location.search);
1440 const couponCode = urlParams.get('coupon');
1441
1442 if (couponCode && couponCode.trim()) {
1443 // Set the coupon code in the input
1444 $('#yatra-coupon-code').val(couponCode.trim().toUpperCase());
1445
1446 // Auto-apply the coupon
1447 applyCoupon();
1448
1449 // Remove coupon parameter from URL without page reload
1450 urlParams.delete('coupon');
1451 const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
1452 window.history.replaceState({}, '', newUrl);
1453 }
1454 }
1455
1456 // Check URL for coupon parameter first (takes priority)
1457 // If URL has coupon, it will be applied and session updated
1458 // If no URL coupon, load existing coupon from session
1459 const urlParams = new URLSearchParams(window.location.search);
1460 const urlCoupon = urlParams.get('coupon');
1461
1462 if (urlCoupon && urlCoupon.trim()) {
1463 // URL coupon takes priority - apply it
1464 checkUrlForCoupon();
1465 } else {
1466 // No URL coupon - load from session
1467 loadCouponFromSession();
1468 }
1469
1470 // Trigger gateway change to show initial info
1471 $('input[name="payment_gateway"]:checked').trigger('change');
1472
1473 // =====================
1474 // Additional Services Selection
1475 // =====================
1476
1477 // Handle additional service checkbox changes
1478 $(document).on('change', '#yatra-additional-services input[type="checkbox"]', function() {
1479 const $checkbox = $(this);
1480 const $serviceItem = $checkbox.closest('.yatra-service-item');
1481 const serviceId = $serviceItem.data('service-id');
1482 const servicePrice = parseFloat($serviceItem.data('service-price')) || 0;
1483 const servicePricePer = $serviceItem.data('service-price-per') || 'booking';
1484 const isRequired = $serviceItem.data('is-required') === 1 || $serviceItem.data('is-required') === '1';
1485 const isChecked = $checkbox.is(':checked');
1486
1487 // Prevent unchecking required services
1488 if (isRequired && !isChecked) {
1489 $checkbox.prop('checked', true);
1490 return;
1491 }
1492
1493 // Update visual state
1494 if (isChecked) {
1495 $serviceItem.addClass('yatra-service-selected');
1496 } else {
1497 $serviceItem.removeClass('yatra-service-selected');
1498 }
1499
1500 // Update booking session with selected services
1501 updateServicesInSession();
1502 });
1503
1504 /**
1505 * Get selected additional services
1506 */
1507 function getSelectedServices() {
1508 const services = [];
1509 $('#yatra-additional-services input[type="checkbox"]:checked').each(function() {
1510 const $serviceItem = $(this).closest('.yatra-service-item');
1511 services.push({
1512 id: parseInt($serviceItem.data('service-id')),
1513 price: parseFloat($serviceItem.data('service-price')) || 0,
1514 price_per: $serviceItem.data('service-price-per') || 'booking'
1515 });
1516 });
1517 return services;
1518 }
1519
1520 /**
1521 * Update services in booking session
1522 */
1523 function updateServicesInSession() {
1524 const selectedServices = getSelectedServices();
1525 const serviceIds = selectedServices.map(s => s.id);
1526
1527 const { base: restBase, isPlain } = getRestBase();
1528 let sessionUrl;
1529 if (isPlain && !restBase.includes('/yatra/v1')) {
1530 sessionUrl = `${restBase}/?rest_route=/yatra/v1/booking/session`;
1531 } else if (restBase.includes('/yatra/v1')) {
1532 sessionUrl = `${restBase}/booking/session`;
1533 } else {
1534 sessionUrl = `${restBase}/yatra/v1/booking/session`;
1535 }
1536
1537 // Carry booking_token from URL so the REST endpoint can rehydrate
1538 // the session even when PHPSESSID hasn't propagated to the REST
1539 // context (root cause of the "POST /booking/session 400" we saw
1540 // on service-toggle). Matches the same pattern the /booking/create
1541 // submit uses.
1542 const sessionPayload = { additional_services: serviceIds };
1543 try {
1544 const tok = new URLSearchParams(window.location.search).get('booking_token');
1545 if (tok) sessionPayload.booking_token = tok;
1546 } catch (e) { /* URLSearchParams unavailable — skip */ }
1547
1548 fetch(sessionUrl, {
1549 method: 'POST',
1550 credentials: 'same-origin',
1551 headers: {
1552 'Content-Type': 'application/json',
1553 'X-WP-Nonce': nonce
1554 },
1555 body: JSON.stringify(sessionPayload)
1556 })
1557 .then(response => response.json())
1558 .then(response => {
1559 if (response.success) {
1560 schedulePricingSummaryRefresh();
1561 }
1562 })
1563 .catch(error => {
1564 console.error('Error updating services in session:', error);
1565 });
1566 }
1567 });
1568
1569 // Add CSS for spinner animation
1570 const style = document.createElement('style');
1571 style.textContent = '@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
1572 document.head.appendChild(style);
1573
1574 // =====================
1575 // Auth Forms (Login/Register)
1576 // =====================
1577
1578 // Tab switching
1579 $(document).on('click', '.yatra-auth-tab, .yatra-switch-tab', function() {
1580 const tab = $(this).data('tab');
1581
1582 // Update tabs
1583 $('.yatra-auth-tab').removeClass('active');
1584 $(`.yatra-auth-tab[data-tab="${tab}"]`).addClass('active');
1585
1586 // Update content
1587 $('.yatra-auth-content').hide();
1588 $(`#yatra-auth-${tab}`).show();
1589 });
1590
1591 // Toggle password visibility
1592 $(document).on('click', '.yatra-toggle-password', function() {
1593 const $input = $(this).parent().find('input');
1594 const $eyeOpen = $(this).find('.eye-open');
1595 const $eyeClosed = $(this).find('.eye-closed');
1596
1597 if ($input.attr('type') === 'password') {
1598 $input.attr('type', 'text');
1599 $eyeOpen.hide();
1600 $eyeClosed.show();
1601 } else {
1602 $input.attr('type', 'password');
1603 $eyeOpen.show();
1604 $eyeClosed.hide();
1605 }
1606 });
1607
1608 // Password strength indicator
1609 $(document).on('input', '#yatra-reg-password', function() {
1610 const password = $(this).val();
1611 const $strengthMeter = $('#yatra-password-strength');
1612 let strength = 0;
1613
1614 if (password.length >= 8) strength++;
1615 if (password.match(/[a-z]/) && password.match(/[A-Z]/)) strength++;
1616 if (password.match(/[0-9]/)) strength++;
1617 if (password.match(/[^a-zA-Z0-9]/)) strength++;
1618
1619 $strengthMeter.removeClass('weak fair good strong');
1620 if (password.length > 0) {
1621 if (strength <= 1) $strengthMeter.addClass('weak');
1622 else if (strength === 2) $strengthMeter.addClass('fair');
1623 else if (strength === 3) $strengthMeter.addClass('good');
1624 else $strengthMeter.addClass('strong');
1625 }
1626 });
1627
1628 // Login form submission via REST API
1629 $(document).on('submit', '#yatra-login-form', function(e) {
1630 e.preventDefault();
1631
1632 const $form = $(this);
1633 const $btn = $form.find('.yatra-auth-submit');
1634 const $btnText = $btn.find('.btn-text');
1635 const $btnLoading = $btn.find('.btn-loading');
1636 const $messageEl = $('#yatra-login-message');
1637
1638 // Remove any existing resend link
1639 $('.yatra-resend-verification').remove();
1640
1641 // Show loading
1642 $btn.prop('disabled', true);
1643 $btnText.hide();
1644 $btnLoading.css('display', 'flex');
1645 $messageEl.hide();
1646
1647 // Prepare data for REST API
1648 const loginData = {
1649 username: $('#yatra-login-email').val(),
1650 password: $('#yatra-login-password').val(),
1651 remember: $('input[name="rememberme"]').is(':checked')
1652 };
1653
1654 fetch(apiUrl + '/auth/login', {
1655 method: 'POST',
1656 headers: {
1657 'Content-Type': 'application/json',
1658 'X-WP-Nonce': nonce
1659 },
1660 credentials: 'same-origin',
1661 body: JSON.stringify(loginData)
1662 })
1663 .then(response => response.json())
1664 .then(response => {
1665 if (response.success) {
1666 $messageEl.removeClass('error').addClass('success');
1667 $messageEl.text(response.message || __('Login successful! Redirecting...', 'yatra'));
1668 $messageEl.show();
1669
1670 // Reload page to show booking form
1671 setTimeout(function() {
1672 window.location.reload();
1673 }, 1000);
1674 } else {
1675 $messageEl.removeClass('success').addClass('error');
1676 $messageEl.html(response.message || __('Invalid credentials. Please try again.', 'yatra'));
1677 $messageEl.show();
1678
1679 // If email needs verification, show resend option
1680 if (response.needs_verification && response.email) {
1681 const didntReceiveText = __("Didn't receive the email?", 'yatra');
1682 const resendLinkText = __('Resend verification link', 'yatra');
1683 const $resendLink = $('<div class="yatra-resend-verification" style="margin-top: 12px; text-align: center;">' +
1684 '<span style="color: #6b7280; font-size: 14px;">' + didntReceiveText + ' </span>' +
1685 '<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>' +
1686 '</div>');
1687 $messageEl.after($resendLink);
1688 }
1689
1690 $btn.prop('disabled', false);
1691 $btnText.show();
1692 $btnLoading.hide();
1693 }
1694 })
1695 .catch(function() {
1696 $messageEl.removeClass('success').addClass('error');
1697 $messageEl.text(__('An error occurred. Please try again.', 'yatra'));
1698 $messageEl.show();
1699
1700 $btn.prop('disabled', false);
1701 $btnText.show();
1702 $btnLoading.hide();
1703 });
1704 });
1705
1706 // Resend verification email
1707 $(document).on('click', '.yatra-resend-btn', function(e) {
1708 e.preventDefault();
1709
1710 const $btn = $(this);
1711 const email = $btn.data('email');
1712 const $messageEl = $('#yatra-login-message');
1713
1714 // Don't allow click if countdown is active
1715 if ($btn.data('countdown-active')) {
1716 return;
1717 }
1718
1719 // Disable button and show loading
1720 $btn.prop('disabled', true).text(__('Sending...', 'yatra'));
1721
1722 fetch(apiUrl + '/auth/resend-verification', {
1723 method: 'POST',
1724 headers: {
1725 'Content-Type': 'application/json',
1726 'X-WP-Nonce': nonce
1727 },
1728 credentials: 'same-origin',
1729 body: JSON.stringify({ email: email })
1730 })
1731 .then(response => response.json())
1732 .then(response => {
1733 if (response.success) {
1734 $messageEl.removeClass('error').addClass('success');
1735 $messageEl.text(response.message);
1736 $messageEl.show();
1737 $('.yatra-resend-verification').remove();
1738 } else if (response.rate_limited && response.remaining_seconds) {
1739 // Start countdown timer
1740 startResendCountdown($btn, response.remaining_seconds);
1741 $messageEl.removeClass('success').addClass('error');
1742 $messageEl.text(__('Please wait before requesting another email.', 'yatra'));
1743 $messageEl.show();
1744 } else {
1745 $messageEl.removeClass('success').addClass('error');
1746 $messageEl.text(response.message);
1747 $messageEl.show();
1748 $btn.prop('disabled', false).text(__('Resend verification link', 'yatra'));
1749 }
1750 })
1751 .catch(function() {
1752 $messageEl.removeClass('success').addClass('error');
1753 $messageEl.text(__('An error occurred. Please try again.', 'yatra'));
1754 $messageEl.show();
1755 $btn.prop('disabled', false).text(__('Resend verification link', 'yatra'));
1756 });
1757 });
1758
1759 // Countdown timer for resend button
1760 function startResendCountdown($btn, seconds) {
1761 $btn.data('countdown-active', true);
1762 $btn.prop('disabled', true);
1763
1764 const updateCountdown = () => {
1765 if (seconds <= 0) {
1766 $btn.data('countdown-active', false);
1767 $btn.prop('disabled', false);
1768 $btn.html(__('Resend verification link', 'yatra'));
1769 return;
1770 }
1771
1772 const mins = Math.floor(seconds / 60);
1773 const secs = seconds % 60;
1774 const timeStr = mins > 0
1775 ? `${mins}:${secs.toString().padStart(2, '0')}`
1776 : `${secs}s`;
1777
1778 // Resend-OTP countdown label. The %s placeholder is the
1779 // remaining time as MM:SS — we wrap the literal so the
1780 // whole sentence translates, then slot the timer in via
1781 // replace() to avoid a dynamic msgid that gettext can't
1782 // extract.
1783 /* translators: %s is the remaining time (e.g. "0:45") before the verification code can be resent. */
1784 const resendTxt = __('Resend in %s', 'yatra').replace('%s', timeStr);
1785 $btn.html(`<span style="color: #9ca3af;">${resendTxt}</span>`);
1786
1787 seconds--;
1788 setTimeout(updateCountdown, 1000);
1789 };
1790
1791 updateCountdown();
1792 }
1793
1794 // Registration form submission via REST API
1795 $(document).on('submit', '#yatra-register-form', function(e) {
1796 e.preventDefault();
1797
1798 const $form = $(this);
1799 const $btn = $form.find('.yatra-auth-submit');
1800 const $btnText = $btn.find('.btn-text');
1801 const $btnLoading = $btn.find('.btn-loading');
1802 const $messageEl = $('#yatra-register-message');
1803
1804 // Validate passwords match
1805 const password = $('#yatra-reg-password').val();
1806 const confirmPassword = $('#yatra-reg-confirm-password').val();
1807
1808 if (password !== confirmPassword) {
1809 $messageEl.removeClass('success').addClass('error');
1810 $messageEl.text(__('Passwords do not match.', 'yatra'));
1811 $messageEl.show();
1812 return;
1813 }
1814
1815 // Show loading
1816 $btn.prop('disabled', true);
1817 $btnText.hide();
1818 $btnLoading.css('display', 'flex');
1819 $messageEl.hide();
1820
1821 // Prepare data for REST API
1822 const registerData = {
1823 first_name: $('#yatra-reg-first-name').val(),
1824 last_name: $('#yatra-reg-last-name').val(),
1825 email: $('#yatra-reg-email').val(),
1826 phone: $('#yatra-reg-phone').val(),
1827 password: password,
1828 confirm_password: confirmPassword
1829 };
1830
1831 fetch(apiUrl + '/auth/register', {
1832 method: 'POST',
1833 headers: {
1834 'Content-Type': 'application/json',
1835 'X-WP-Nonce': nonce
1836 },
1837 credentials: 'same-origin',
1838 body: JSON.stringify(registerData)
1839 })
1840 .then(response => response.json())
1841 .then(response => {
1842 if (response.success) {
1843 $messageEl.removeClass('error').addClass('success');
1844 $messageEl.text(response.message || __('Account created! Please check your email to verify.', 'yatra'));
1845 $messageEl.show();
1846
1847 // Reset form
1848 $form[0].reset();
1849 $('#yatra-password-strength').removeClass('weak fair good strong');
1850
1851 // If verification is required, switch to login tab after a delay
1852 if (response.require_verification) {
1853 $btn.prop('disabled', false);
1854 $btnText.show();
1855 $btnLoading.hide();
1856
1857 // Show message and switch to login tab after 3 seconds
1858 setTimeout(function() {
1859 // Switch to login tab
1860 $('.yatra-auth-tab').removeClass('active');
1861 $('.yatra-auth-tab[data-tab="login"]').addClass('active');
1862 $('.yatra-auth-content').hide();
1863 $('#yatra-auth-login').show();
1864
1865 // Show info message on login tab
1866 const $loginMessage = $('#yatra-login-message');
1867 $loginMessage.removeClass('error').addClass('success');
1868 $loginMessage.text(__('Please check your email and click the verification link, then login here.', 'yatra'));
1869 $loginMessage.show();
1870 }, 3000);
1871 } else {
1872 // Reload page if no verification required (shouldn't happen normally)
1873 setTimeout(function() {
1874 window.location.reload();
1875 }, 1000);
1876 }
1877 } else {
1878 $messageEl.removeClass('success').addClass('error');
1879 $messageEl.text(response.message || __('Registration failed. Please try again.', 'yatra'));
1880 $messageEl.show();
1881
1882 $btn.prop('disabled', false);
1883 $btnText.show();
1884 $btnLoading.hide();
1885 }
1886 })
1887 .catch(function() {
1888 $messageEl.removeClass('success').addClass('error');
1889 $messageEl.text(__('An error occurred. Please try again.', 'yatra'));
1890 $messageEl.show();
1891
1892 $btn.prop('disabled', false);
1893 $btnText.show();
1894 $btnLoading.hide();
1895 });
1896 });
1897
1898 // ---------------------------------------------------------------------
1899 // Date field picker upgrade (fast year navigation for Date of Birth)
1900 // ---------------------------------------------------------------------
1901 // A native <input type="date"> popup only steps one month at a time, so
1902 // reaching a birth year like 1990 takes many clicks. Where flatpickr is
1903 // available (and on non-touch devices, since mobile native pickers already
1904 // have good year wheels) we upgrade `.yatra-js-date` inputs to a flatpickr
1905 // whose header year is directly typeable/scrollable. Fully guarded: with no
1906 // flatpickr, or on mobile, the native input is left untouched and working.
1907 var yatraIsMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent || '');
1908
1909 function yatraUpgradeDateField(input) {
1910 if (!input || input.dataset.yatraFp === '1') return;
1911 if (typeof window.flatpickr === 'undefined') return;
1912 if (yatraIsMobile) return; // native mobile date wheels already navigate years well
1913 input.dataset.yatraFp = '1';
1914 var isDob = input.getAttribute('data-yatra-dob') === '1';
1915 // Turn off the native date UI so flatpickr is the only picker; the
1916 // submitted value stays ISO (Y-m-d) so FormData/back-end are unchanged.
1917 try { input.type = 'text'; } catch (e) {}
1918 window.flatpickr(input, {
1919 dateFormat: 'Y-m-d',
1920 allowInput: true,
1921 maxDate: isDob ? 'today' : null,
1922 defaultDate: input.value || null
1923 });
1924 }
1925
1926 function yatraUpgradeDateFields(root) {
1927 var scope = (root && root.querySelectorAll) ? root : document;
1928 var list = scope.querySelectorAll('input.yatra-js-date');
1929 for (var i = 0; i < list.length; i++) {
1930 yatraUpgradeDateField(list[i]);
1931 }
1932 }
1933
1934 function yatraInitDatePickers() {
1935 yatraUpgradeDateFields(document);
1936 // The traveler form is injected via AJAX, so watch for inserted inputs.
1937 if (typeof MutationObserver === 'undefined' || !document.body) return;
1938 var obs = new MutationObserver(function (mutations) {
1939 for (var m = 0; m < mutations.length; m++) {
1940 var added = mutations[m].addedNodes;
1941 for (var n = 0; n < added.length; n++) {
1942 var node = added[n];
1943 if (!node || node.nodeType !== 1) continue;
1944 if (node.matches && node.matches('input.yatra-js-date')) {
1945 yatraUpgradeDateField(node);
1946 } else if (node.querySelectorAll) {
1947 yatraUpgradeDateFields(node);
1948 }
1949 }
1950 }
1951 });
1952 obs.observe(document.body, { childList: true, subtree: true });
1953 }
1954
1955 if (document.readyState === 'loading') {
1956 document.addEventListener('DOMContentLoaded', yatraInitDatePickers);
1957 } else {
1958 yatraInitDatePickers();
1959 }
1960
1961 // End of document ready
1962 })(jQuery);
1963