PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.9
Yatra – Travel Booking & Tour Operator Software v3.0.9
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.9, at assets/js/booking.js

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