| 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 |
|