/**
* Stripe Payment Integration for Yatra
* Supports Stripe Elements (card) and Payment Request (Google Pay / Apple Pay)
*/
const __ = (window.wp && window.wp.i18n && window.wp.i18n.__) ? window.wp.i18n.__ : (text => text);
class YatraStripe {
constructor(apiUrl, nonce) {
this.apiUrl = apiUrl;
this.nonce = nonce;
this.stripe = null;
this.elements = null;
this.cardElement = null;
this.paymentRequest = null;
this.paymentRequestButton = null;
this.paymentRequestMounted = false;
this.paymentForm = document.getElementById('yatra-booking-form');
this.submitButton = document.getElementById('yatra-submit-booking');
this.originalButtonHtml = this.submitButton ? this.submitButton.innerHTML : '';
this.methodButtons = {};
this.activeMethod = null;
this.cardWrapper = null;
this.walletWrapper = null;
this.walletCapabilities = {
google_pay: false,
apple_pay: false,
};
this.walletMethodHints = {
google_pay: '',
apple_pay: '',
};
this.walletAvailabilityKnown = false;
/** Short labels for each method switcher option (tooltip base copy). */
this.methodHelpText = {};
this.stripeSettings = window.yatraBookingData?.stripe || {};
let enabledMethods = this.stripeSettings.enabledMethods || ['card'];
if (typeof enabledMethods === 'string') {
enabledMethods = enabledMethods.split(',');
}
if (!Array.isArray(enabledMethods) || enabledMethods.length === 0) {
enabledMethods = ['card'];
}
this.enabledMethods = enabledMethods.map((method) => method.trim().toLowerCase()).filter(Boolean);
if (this.enabledMethods.length === 0) {
this.enabledMethods = ['card'];
}
this.supportsCard = this.enabledMethods.includes('card');
this.supportsPaymentRequest = this.enabledMethods.some((method) => method === 'google_pay' || method === 'apple_pay');
this.companyCountry = window.yatraBookingData?.companyCountry || 'US';
this.tripTitle = window.yatraBookingData?.tripTitle || 'Trip Booking';
window.yatraStripeInstance = this;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.init());
} else {
this.init();
}
}
init() {
if (!document.querySelector('input[name="payment_gateway"][value="stripe"]')) {
return;
}
this.addStripeElementsContainer();
document.querySelectorAll('input[name="payment_gateway"]').forEach((radio) => {
radio.addEventListener('change', (e) => this.handlePaymentMethodChange(e));
});
if (typeof Stripe !== 'undefined') {
this.initializeStripe();
}
// Listen for unified booking submit event (new system)
document.addEventListener('yatra_booking_submit', (event) => this.handleBookingSubmit(event));
// Listen for payment response for client-side Stripe payments
document.addEventListener('yatra_payment_response', (event) => this.handlePaymentResponse(event));
// Legacy event support
document.addEventListener('yatraBeforeBookingSubmit', (event) => this.handleGatewayIntercept(event));
}
/**
* Handle unified booking submit event
*/
handleBookingSubmit(event) {
if (event.detail?.gateway !== 'stripe') {
return; // Let other gateways handle
}
// Stripe intercepts the form submission to handle card payment
event.preventDefault();
// Use the existing handleGatewayIntercept logic
this.handleGatewayIntercept({
preventDefault: () => {},
detail: event.detail
});
}
/**
* Handle payment response for Stripe (when server returns requires_action)
*/
handlePaymentResponse(event) {
if (event.detail?.requires_action !== 'stripe_payment') {
return;
}
this.handleStripePaymentAction(event.detail);
}
/**
* Handle Stripe payment action from server response
*/
async handleStripePaymentAction(data) {
try {
// Load Stripe if not already loaded
if (!this.stripe && data.publishable_key) {
await this.loadStripeScript();
this.stripe = Stripe(data.publishable_key);
}
if (!this.stripe) {
throw new Error('Stripe not initialized');
}
const elements = this.stripe.elements({
clientSecret: data.client_secret
});
// Create payment element in a modal
const modal = document.createElement('div');
modal.id = 'yatra-stripe-modal';
modal.innerHTML = `
${__('Complete Your Payment', 'yatra')}
`;
document.body.appendChild(modal);
const paymentElement = elements.create('payment');
paymentElement.mount('#stripe-payment-element');
document.getElementById('stripe-submit-btn').addEventListener('click', async () => {
const btn = document.getElementById('stripe-submit-btn');
btn.disabled = true;
btn.textContent = __('Processing...', 'yatra');
// Append `stripe=success` to the gateway's return URL using proper URL
// composition. The naive `+ '?stripe=success'` form breaks when
// confirmation_url already carries query params (e.g. `?balance=paid`
// from a remaining-balance flow) — two question marks produces an
// invalid URL and browsers drop the second segment, losing the flag.
let confirmReturnUrl = data.confirmation_url;
try {
const u = new URL(confirmReturnUrl, window.location.origin);
u.searchParams.set('stripe', 'success');
confirmReturnUrl = u.toString();
} catch (_e) {
// Fallback for malformed bases (shouldn't happen in practice)
const join = confirmReturnUrl.includes('?') ? '&' : '?';
confirmReturnUrl = `${confirmReturnUrl}${join}stripe=success`;
}
const { error } = await this.stripe.confirmPayment({
elements,
confirmParams: {
return_url: confirmReturnUrl,
},
});
if (error) {
document.getElementById('stripe-error-message').textContent = error.message;
document.getElementById('stripe-error-message').style.display = 'block';
btn.disabled = false;
btn.textContent = `${__('Pay', 'yatra')} ${data.currency} ${parseFloat(data.amount).toFixed(2)}`;
}
});
document.getElementById('stripe-cancel-btn').addEventListener('click', () => {
modal.remove();
document.dispatchEvent(new CustomEvent('yatra_payment_cancelled', {
detail: { gateway: 'stripe' }
}));
});
} catch (error) {
console.error('Stripe payment error:', error);
document.dispatchEvent(new CustomEvent('yatra_payment_failed', {
detail: { gateway: 'stripe', error: error.message }
}));
}
}
loadStripeScript() {
return new Promise((resolve, reject) => {
if (typeof Stripe !== 'undefined') {
resolve();
return;
}
const script = document.createElement('script');
script.src = 'https://js.stripe.com/v3/';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
addStripeElementsContainer() {
const stripeOption = document.querySelector('input[name="payment_gateway"][value="stripe"]');
if (!stripeOption || document.querySelector('.yatra-stripe-container')) {
return;
}
const container = document.createElement('div');
container.className = 'yatra-stripe-container';
const stripePreselected = !!stripeOption.checked;
container.style.display = stripePreselected ? 'block' : 'none';
this.methodSwitcher = this.createMethodSwitcher();
// Show the "Payment methods" chooser only when there's an actual choice
// (Card + Apple/Google Pay). With Card as the only enabled method the
// chooser is redundant chrome, so present the card field directly.
if (this.hasMultipleMethods) {
container.appendChild(this.methodSwitcher);
} else {
container.classList.add('yatra-stripe-container--single-method');
}
if (this.supportsPaymentRequest) {
const requestWrapper = document.createElement('div');
requestWrapper.className = 'yatra-stripe-payment-request-wrapper';
const walletNote = window.yatraTranslations?.stripeWalletNote || __('Pay instantly with Apple Pay or Google Pay.', 'yatra');
const walletTitle = __('Apple Pay & Google Pay', 'yatra');
requestWrapper.innerHTML = `
${walletNote}
${__('Checking your device for wallet support…', 'yatra')}
`;
container.appendChild(requestWrapper);
this.walletWrapper = requestWrapper;
this.walletWrapper.style.display = 'none';
}
if (this.supportsCard) {
const cardWrapper = document.createElement('div');
cardWrapper.className = 'yatra-stripe-card-wrapper';
cardWrapper.innerHTML = `
`;
container.appendChild(cardWrapper);
this.cardWrapper = cardWrapper;
this.cardWrapper.style.display = 'none';
}
const gatewayExtra = document.getElementById('yatra-gateway-extra-stripe');
if (gatewayExtra) {
gatewayExtra.appendChild(container);
} else {
let parentElement = stripeOption.closest('.yatra-payment-option')
|| stripeOption.closest('.payment_method_stripe')
|| stripeOption.closest('.payment-method')
|| stripeOption.parentElement;
if (parentElement) {
parentElement.appendChild(container);
} else {
const paymentOptions = document.querySelector('.yatra-payment-options, .payment-methods');
if (paymentOptions && paymentOptions.parentNode) {
paymentOptions.parentNode.insertBefore(container, paymentOptions.nextSibling);
}
}
}
this.setActiveMethod(this.getDefaultMethod());
this.syncMethodButtonsState();
if (stripePreselected && !this.stripe) {
this.initializeStripe();
}
}
handlePaymentMethodChange(event) {
const selectedMethod = event.target.value;
const stripeContainer = document.querySelector('.yatra-stripe-container');
if (selectedMethod === 'stripe') {
if (!this.stripe) {
this.initializeStripe();
} else if (stripeContainer) {
stripeContainer.style.display = 'block';
}
} else if (stripeContainer) {
stripeContainer.style.display = 'none';
}
}
initializeStripe() {
const publishableKey = window.yatraBookingData?.stripe?.publishableKey
|| window.yatraBookingData?.stripe?.publishable_key;
if (!publishableKey) {
this.displayError(__('Stripe publishable key is missing. Please contact support.', 'yatra'));
return;
}
if (typeof Stripe === 'undefined') {
const script = document.createElement('script');
script.src = 'https://js.stripe.com/v3/';
script.onload = () => this.setupStripeElements(publishableKey);
script.onerror = () => {
this.displayError(__('Failed to load Stripe.js. Please try again or choose another payment method.', 'yatra'));
};
document.head.appendChild(script);
} else {
this.setupStripeElements(publishableKey);
}
}
setupStripeElements(publishableKey) {
try {
this.stripe = Stripe(publishableKey, { apiVersion: '2023-10-16' });
this.elements = this.stripe.elements({
fonts: [{ cssSrc: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap' }],
locale: 'auto'
});
if (this.supportsCard) {
const style = {
base: {
color: '#32325d',
fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
fontSmoothing: 'antialiased',
fontSize: '16px',
'::placeholder': { color: '#aab7c4' }
},
invalid: {
color: '#e53935',
iconColor: '#e53935'
}
};
this.cardElement = this.elements.create('card', {
style,
hidePostalCode: true
});
const cardElement = document.getElementById('yatra-stripe-card-element');
if (cardElement) {
this.cardElement.mount(cardElement);
this.cardElement.on('change', (event) => this.displayError(event.error?.message || ''));
}
}
this.initializePaymentRequestButton();
const stripeContainer = document.querySelector('.yatra-stripe-container');
if (stripeContainer && document.querySelector('input[name="payment_gateway"]:checked')?.value === 'stripe') {
stripeContainer.style.display = 'block';
}
} catch (error) {
console.error('Error initializing Stripe:', error);
this.displayError(__('Failed to initialize payment processor. Please try again or use another payment method.', 'yatra'));
}
}
async handleGatewayIntercept(event) {
const selectedMethod = document.querySelector('input[name="payment_gateway"]:checked')?.value;
if (selectedMethod !== 'stripe') {
return true;
}
event.preventDefault();
if (this.activeMethod && this.activeMethod !== 'card') {
this.displayError(__('Please pay using the Apple Pay or Google Pay button above.', 'yatra'));
return false;
}
if (!this.supportsCard) {
this.displayError(__('Please use Apple Pay or Google Pay above to complete your payment.', 'yatra'));
return false;
}
const bookingData = event?.detail?.bookingData || this.collectBookingData();
if (!bookingData) {
this.displayError(__('Please complete the booking form before continuing.', 'yatra'));
return false;
}
if (!this.stripe || !this.cardElement) {
this.initializeStripe();
this.displayError(__('Stripe is still loading. Please wait a moment and try again.', 'yatra'));
return false;
}
if (!this.originalButtonHtml && event?.detail?.originalBtnHtml) {
this.originalButtonHtml = event.detail.originalBtnHtml;
} else if (!this.originalButtonHtml && this.submitButton) {
this.originalButtonHtml = this.submitButton.innerHTML;
}
this.displayError('');
this.setLoadingState(true);
try {
const paymentContext = await this.prepareStripePayment(bookingData, event?.detail?.form || this.paymentForm);
const { bookingInfo, clientSecret, billingDetails, effectiveAmount } = paymentContext;
const { error, paymentIntent } = await this.stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: this.cardElement,
billing_details: billingDetails
},
return_url: this.buildReturnUrl(bookingInfo)
});
if (error) {
throw error;
}
if (paymentIntent.status === 'succeeded') {
await this.notifyPaymentSuccess({
bookingId: bookingInfo.booking_id,
transactionId: paymentIntent.id,
saveCard: !!bookingData.save_card,
amount: effectiveAmount,
currency: bookingInfo.currency || bookingData.currency || 'USD'
});
window.location.href = this.buildConfirmationUrlFromBookingInfo(bookingInfo);
} else {
throw new Error(__('Payment processing was not completed. Please check your payment details and try again.', 'yatra'));
}
} catch (error) {
console.error('Stripe payment error:', error);
this.displayError(error.message || __('Payment failed. Please try again or use another payment method.', 'yatra'));
this.setLoadingState(false);
}
return false;
}
async initializePaymentRequestButton() {
if (!this.supportsPaymentRequest || !this.stripe || this.paymentRequestMounted) {
return;
}
try {
this.paymentRequest = this.stripe.paymentRequest({
country: (this.companyCountry || 'US').toUpperCase(),
currency: (window.yatraBookingData?.currency || 'USD').toLowerCase(),
total: this.getPaymentRequestTotal(),
requestPayerName: true,
requestPayerEmail: true,
});
const result = await this.paymentRequest.canMakePayment();
if (!result) {
this.setWalletStatus(
__('Apple Pay and Google Pay are not available on this browser/device.', 'yatra'),
'warning'
);
this.updateWalletCapabilities(null);
this.showWalletPlaceholder(__('Use card payment instead or switch to a supported device.', 'yatra'));
this.syncMethodButtonsState();
return;
}
this.updateWalletCapabilities(result === true ? { applePay: true, googlePay: true } : result);
this.setWalletStatus(
__('Great! Apple Pay or Google Pay is available on this device.', 'yatra'),
'success'
);
this.syncMethodButtonsState();
this.paymentRequest.on('paymentmethod', (event) => this.handlePaymentRequestPayment(event));
this.paymentRequestButton = this.elements.create('paymentRequestButton', {
paymentRequest: this.paymentRequest,
style: {
paymentRequestButton: {
type: 'default',
theme: 'dark',
height: '48px'
}
}
});
const prContainer = document.getElementById('yatra-stripe-payment-request');
if (prContainer) {
this.paymentRequestButton.mount(prContainer);
this.paymentRequestMounted = true;
prContainer.classList.remove('placeholder');
const stripeContainer = document.querySelector('.yatra-stripe-container');
if (stripeContainer && document.querySelector('input[name="payment_gateway"]:checked')?.value === 'stripe') {
stripeContainer.style.display = 'block';
}
}
} catch (error) {
console.error('Failed to initialize Stripe Payment Request button:', error);
this.setWalletStatus(
__('Unable to load Apple Pay / Google Pay. Please try again later or use card payment.', 'yatra'),
'error'
);
this.showWalletPlaceholder(__('Wallet payment failed to load.', 'yatra'));
this.updateWalletCapabilities(null);
const errHint = __('Wallet payments could not be initialized. Use your card below, or try again after refreshing the page.', 'yatra');
this.walletMethodHints.google_pay = errHint;
this.walletMethodHints.apple_pay = errHint;
this.syncMethodButtonsState();
}
}
setWalletStatus(message, variant = 'info') {
const statusEl = document.getElementById('yatra-stripe-wallet-status');
if (!statusEl) {
return;
}
statusEl.textContent = message;
statusEl.classList.remove('is-success', 'is-warning', 'is-error');
if (variant === 'success') {
statusEl.classList.add('is-success');
} else if (variant === 'warning') {
statusEl.classList.add('is-warning');
} else if (variant === 'error') {
statusEl.classList.add('is-error');
}
}
showWalletPlaceholder(message) {
const container = document.getElementById('yatra-stripe-payment-request');
if (!container) {
return;
}
container.classList.add('wallet-placeholder');
container.innerHTML = `${message}`;
}
updateWalletCapabilities(result) {
const availability = { apple_pay: false, google_pay: false };
if (result && typeof result === 'object') {
availability.apple_pay = !!(result.applePay || result.apple_pay);
availability.google_pay = !!(result.googlePay || result.google_pay);
} else if (result === true) {
availability.apple_pay = true;
availability.google_pay = true;
}
this.walletCapabilities = availability;
const hintGoogle = !availability.google_pay && this.enabledMethods.includes('google_pay')
? __('Google Pay is not available in this browser. Try Chrome or Edge on desktop or Android, use HTTPS, and ensure you have a card in your Google account.', 'yatra')
: '';
const hintApple = !availability.apple_pay && this.enabledMethods.includes('apple_pay')
? __('Apple Pay is not available here. Use Safari on iPhone, iPad, or Mac, serve this page over HTTPS, and add your domain in Stripe for Apple Pay.', 'yatra')
: '';
this.walletMethodHints = {
google_pay: hintGoogle,
apple_pay: hintApple,
};
this.walletAvailabilityKnown = true;
}
createMethodSwitcher() {
const wrapper = document.createElement('div');
wrapper.className = 'yatra-method-switcher';
const availableButtons = [];
const header = document.createElement('div');
header.className = 'yatra-method-switcher__header';
const eyebrow = document.createElement('p');
eyebrow.className = 'yatra-label-eyebrow';
eyebrow.textContent = __('Choose a payment option', 'yatra');
const title = document.createElement('div');
title.className = 'yatra-method-switcher__title';
title.textContent = __('Payment methods', 'yatra');
const helper = document.createElement('p');
helper.className = 'yatra-method-switcher__helper';
helper.textContent = __('Pick the option that works best for you.', 'yatra');
header.appendChild(eyebrow);
header.appendChild(title);
header.appendChild(helper);
if (this.supportsCard) {
availableButtons.push({
id: 'card',
label: __('Card', 'yatra'),
description: __('Use any major debit or credit card via Stripe Elements.', 'yatra'),
icon: this.getMethodIconMarkup('card')
});
}
if (this.enabledMethods.includes('google_pay')) {
availableButtons.push({
id: 'google_pay',
label: __('Google Pay', 'yatra'),
description: __('Pay instantly with the cards saved to your Google account.', 'yatra'),
icon: this.getMethodIconMarkup('google_pay')
});
}
if (this.enabledMethods.includes('apple_pay')) {
availableButtons.push({
id: 'apple_pay',
label: __('Apple Pay', 'yatra'),
description: __('Use Apple Wallet on iPhone, iPad, or Mac to pay securely.', 'yatra'),
icon: this.getMethodIconMarkup('apple_pay')
});
}
// Only a real choice (Card + a wallet) warrants the chooser. With Card
// as the only enabled method, the render step drops this switcher and
// shows the card field on its own (no redundant "Payment methods" row).
this.hasMultipleMethods = availableButtons.length > 1;
const buttons = document.createElement('div');
buttons.className = 'yatra-method-switcher__buttons';
availableButtons.forEach((method) => {
this.methodHelpText[method.id] = method.description;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'yatra-method-button';
btn.dataset.method = method.id;
btn.innerHTML = `
${method.icon}
${method.label}
`;
this.applyMethodTooltipText(btn.querySelector('.method-tooltip'), method.description);
btn.addEventListener('click', () => {
if (btn.getAttribute('aria-disabled') === 'true') {
return;
}
this.setActiveMethod(method.id);
});
this.methodButtons[method.id] = btn;
buttons.appendChild(btn);
});
wrapper.appendChild(header);
wrapper.appendChild(buttons);
return wrapper;
}
getMethodIconMarkup(type) {
const base = '';
const close = '';
const icons = {
card: `${base}
${close}`,
google_pay: `${base}
${close}`,
apple_pay: `${base}
${close}`
};
return icons[type] || `${base}`;
}
getDefaultMethod() {
if (this.supportsCard) {
return 'card';
}
if (this.enabledMethods.includes('google_pay')) {
return 'google_pay';
}
if (this.enabledMethods.includes('apple_pay')) {
return 'apple_pay';
}
return 'card';
}
setActiveMethod(method) {
if (!method) {
return;
}
// Guard against selecting unsupported methods
if (!this.isMethodSelectable(method)) {
method = this.getDefaultMethod();
}
if (this.activeMethod === method) {
return;
}
this.activeMethod = method;
if (this.cardWrapper) {
this.cardWrapper.style.display = method === 'card' ? 'block' : 'none';
}
if (this.walletWrapper) {
const shouldShowWallet = method === 'google_pay' || method === 'apple_pay';
this.walletWrapper.style.display = shouldShowWallet ? 'block' : 'none';
}
this.syncMethodButtonsState();
}
syncMethodButtonsState() {
Object.entries(this.methodButtons).forEach(([method, button]) => {
if (!button) {
return;
}
const isActive = method === this.activeMethod;
button.classList.toggle('is-active', isActive);
button.classList.remove('is-disabled', 'is-pending');
button.removeAttribute('aria-disabled');
const tooltipEl = button.querySelector('.method-tooltip');
const baseHelp = this.methodHelpText[method] || '';
if (this.isWalletMethod(method)) {
if (!this.walletAvailabilityKnown) {
button.classList.add('is-pending');
button.setAttribute('aria-disabled', 'true');
this.applyMethodTooltipText(
tooltipEl,
__('Checking if Google Pay or Apple Pay is available on this device…', 'yatra')
);
} else {
const isSupported = !!this.walletCapabilities[method];
if (!isSupported) {
button.classList.add('is-disabled');
button.setAttribute('aria-disabled', 'true');
const why = this.walletMethodHints[method] || __('This wallet is not available on this device or browser. Use card payment instead.', 'yatra');
this.applyMethodTooltipText(tooltipEl, why);
if (isActive) {
this.setActiveMethod('card');
}
} else {
this.applyMethodTooltipText(tooltipEl, baseHelp);
}
}
} else {
this.applyMethodTooltipText(tooltipEl, baseHelp);
}
});
// Ensure at least one method is visible if everything else disabled
const activeBtn = this.activeMethod ? this.methodButtons[this.activeMethod] : null;
const activeBlocked = activeBtn && activeBtn.getAttribute('aria-disabled') === 'true';
if (!this.activeMethod || activeBlocked) {
const fallback = this.supportsCard ? 'card'
: (this.walletCapabilities.google_pay ? 'google_pay'
: (this.walletCapabilities.apple_pay ? 'apple_pay' : null));
if (fallback) {
this.setActiveMethod(fallback);
}
}
}
/**
* @param {HTMLElement | null} el
* @param {string} text
*/
applyMethodTooltipText(el, text) {
if (!el) {
return;
}
const t = String(text);
el.setAttribute('data-tooltip', t);
el.setAttribute('aria-label', t);
el.setAttribute('title', t);
}
isWalletMethod(method) {
return method === 'google_pay' || method === 'apple_pay';
}
isMethodSelectable(method) {
if (method === 'card') {
return !!this.supportsCard;
}
if (this.isWalletMethod(method)) {
if (!this.walletAvailabilityKnown) {
return false;
}
return !!this.walletCapabilities[method];
}
return false;
}
async handlePaymentRequestPayment(event) {
try {
const bookingData = this.collectBookingData();
if (!bookingData) {
throw new Error(__('Please complete the booking form before using Apple Pay or Google Pay.', 'yatra'));
}
this.setLoadingState(true);
const paymentContext = await this.prepareStripePayment(bookingData, this.paymentForm);
const { bookingInfo, clientSecret, effectiveAmount } = paymentContext;
const confirmation = await this.stripe.confirmCardPayment(clientSecret, {
payment_method: event.paymentMethod.id,
return_url: this.buildReturnUrl(bookingInfo)
});
if (confirmation.error) {
event.complete('fail');
throw confirmation.error;
}
event.complete('success');
if (confirmation.paymentIntent?.status === 'requires_action') {
return;
}
if (confirmation.paymentIntent?.status === 'succeeded') {
await this.notifyPaymentSuccess({
bookingId: bookingInfo.booking_id,
transactionId: confirmation.paymentIntent.id,
saveCard: !!bookingData.save_card,
amount: effectiveAmount,
currency: bookingInfo.currency || bookingData.currency || 'USD'
});
}
window.location.href = this.buildConfirmationUrlFromBookingInfo(bookingInfo);
} catch (error) {
console.error('Stripe payment request error:', error);
if (event && typeof event.complete === 'function') {
event.complete('fail');
}
this.displayError(error.message || __('Payment failed. Please try again or use another payment method.', 'yatra'));
this.setLoadingState(false);
}
}
async prepareStripePayment(bookingData, formElement) {
// /booking/create requires BOTH the standard WP REST nonce
// (X-WP-Nonce) AND a booking-scoped action nonce
// (X-Yatra-Booking-Nonce). The booking-scoped one is what
// gates the actual write; the endpoint's public
// permission_callback intentionally bypasses WP's default
// cookie/nonce check so guest checkouts work, and falls back
// to the booking-scoped nonce as the real CSRF defence.
// Missing it returns { code: 'invalid_nonce' } 403.
const bookingNonce =
(typeof window !== 'undefined' && window.yatraBookingData && window.yatraBookingData.bookingNonce)
|| (typeof document !== 'undefined'
? (document.querySelector('input[name="yatra_booking_nonce"]') || {}).value
: '')
|| '';
// Always call the same endpoint - server decides based on session type
const bookingResponse = await fetch(`${this.apiUrl}/booking/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': this.nonce,
'X-Yatra-Booking-Nonce': bookingNonce
},
body: JSON.stringify(bookingData)
});
const bookingResult = await bookingResponse.json();
if (!bookingResult.success) {
throw new Error(bookingResult.message || __('Failed to process request. Please try again.', 'yatra'));
}
const bookingInfo = bookingResult.data;
const effectiveAmount = this.getEffectiveAmount(bookingData, bookingInfo);
if (bookingInfo.client_secret) {
const customerEmail = bookingInfo.customer_email || bookingData.contact_email || bookingData.email;
const customerName = bookingInfo.customer_name ||
[bookingData.contact_first_name, bookingData.contact_last_name]
.filter(Boolean)
.join(' ') || bookingData.full_name;
if (!customerEmail) {
throw new Error(__('Email address is required.', 'yatra'));
}
const billingDetails = this.getBillingDetails(bookingData, formElement);
return {
bookingInfo,
clientSecret: bookingInfo.client_secret,
billingDetails,
effectiveAmount,
};
}
const customerEmail = bookingInfo.customer_email || bookingData.contact_email || bookingData.email;
const customerName = bookingInfo.customer_name ||
[bookingData.contact_first_name, bookingData.contact_last_name]
.filter(Boolean)
.join(' ') || bookingData.full_name;
if (!customerEmail) {
throw new Error(__('Email address is required.', 'yatra'));
}
const paymentIntentResponse = await fetch(`${this.apiUrl}/payment/create-intent`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': this.nonce
},
body: JSON.stringify({
gateway: 'stripe',
booking_id: bookingInfo.booking_id,
amount: effectiveAmount,
currency: bookingInfo.currency,
trip_id: bookingInfo.trip_id || bookingData.trip_id || null,
trip_date: bookingInfo.trip_date || bookingData.travel_date || '',
customer_email: customerEmail,
customer_name: customerName,
return_url: this.buildReturnUrl(bookingInfo)
})
});
const intentPayload = await paymentIntentResponse.json();
if (!paymentIntentResponse.ok) {
throw new Error(intentPayload?.message || __('Unable to process payment. Please try again.', 'yatra'));
}
const clientSecret = intentPayload.client_secret;
if (!clientSecret) {
throw new Error(intentPayload?.error?.message || __('Unable to process payment. Please try again.', 'yatra'));
}
const billingDetails = this.getBillingDetails(bookingData, formElement);
return {
bookingInfo,
clientSecret,
billingDetails,
effectiveAmount,
};
}
getBillingDetails(bookingData, formElement) {
const getField = (keys) => {
const candidates = Array.isArray(keys) ? keys : [keys];
for (const key of candidates) {
if (bookingData && bookingData[key]) {
return bookingData[key];
}
if (formElement) {
const input = formElement.querySelector(`[name="${key}"]`);
if (input && input.value) {
return input.value;
}
}
}
return '';
};
const firstName = getField(['billing_first_name', 'contact_first_name', 'first_name']);
const lastName = getField(['billing_last_name', 'contact_last_name', 'last_name']);
const email = getField(['billing_email', 'contact_email', 'email']);
const phone = getField(['billing_phone', 'contact_phone', 'phone']);
const line1 = getField(['billing_address_1', 'contact_address']);
const city = getField(['billing_city', 'contact_city']);
const country = getField(['billing_country', 'contact_country']);
const postalCode = getField(['billing_postcode', 'contact_postcode', 'contact_zip']);
const billingDetails = {};
const name = `${firstName} ${lastName}`.trim();
if (name) {
billingDetails.name = name;
}
if (email) {
billingDetails.email = email;
}
if (phone) {
billingDetails.phone = phone;
}
const address = {};
if (line1) {
address.line1 = line1;
}
if (city) {
address.city = city;
}
if (country) {
const normalizedCountry = country.trim().toUpperCase();
if (normalizedCountry.length === 2) {
address.country = normalizedCountry;
}
}
if (postalCode) {
address.postal_code = postalCode;
}
if (Object.keys(address).length > 0) {
billingDetails.address = address;
}
return billingDetails;
}
collectBookingData() {
if (!this.paymentForm) {
return null;
}
const formData = new FormData(this.paymentForm);
const bookingData = {};
for (const [key, value] of formData.entries()) {
const matches = key.match(/^([^\[]+)(?:\[([^\]]*)\])?(?:\[([^\]]*)\])?$/);
if (matches) {
const [, base, index, field] = matches;
if (index !== undefined && field !== undefined) {
bookingData[base] = bookingData[base] || {};
bookingData[base][index] = bookingData[base][index] || {};
bookingData[base][index][field] = value;
} else if (index !== undefined) {
bookingData[base] = bookingData[base] || [];
bookingData[base][index] = value;
} else {
bookingData[key] = value;
}
} else {
bookingData[key] = value;
}
}
bookingData.accept_terms = this.paymentForm.querySelector('input[name="accept_terms"]')?.checked || false;
bookingData.accept_privacy = this.paymentForm.querySelector('input[name="accept_privacy"]')?.checked || false;
bookingData.subscribe_newsletter = this.paymentForm.querySelector('input[name="subscribe_newsletter"]')?.checked || false;
bookingData.payment_due = parseFloat(this.paymentForm.getAttribute('data-payment-due'))
|| window.yatraBookingData?.paymentDue
|| null;
bookingData.currency = window.yatraBookingData?.currency
|| bookingData.currency
|| this.paymentForm.querySelector('input[name="currency"]')?.value
|| 'USD';
bookingData.payment_gateway = 'stripe';
return bookingData;
}
displayError(message) {
const errorElement = document.getElementById('yatra-stripe-card-errors');
if (errorElement) {
errorElement.textContent = message || '';
errorElement.style.display = message ? 'block' : 'none';
if (message) {
errorElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
} else if (message) {
console.error(message);
}
}
setLoadingState(isLoading) {
if (!this.submitButton) {
return;
}
if (isLoading) {
this.submitButton.disabled = true;
// Spinner markup is fixed; only the user-visible text is
// wrapped so it ends up in the .pot with a clean msgid.
this.submitButton.innerHTML =
' ' + __('Processing Payment...', 'yatra');
} else {
this.submitButton.disabled = false;
this.submitButton.innerHTML = this.originalButtonHtml;
}
}
getEffectiveAmount(bookingData, bookingInfo) {
// After /booking/create, server-calculated charge (deposit / partial / full) is authoritative.
// `amount` = charge for this checkout (matches gateway). Never fall back to `total_amount` (full order).
const serverFirst = [
parseFloat(bookingInfo?.amount),
parseFloat(bookingInfo?.amount_due)
];
for (let i = 0; i < serverFirst.length; i++) {
const v = serverFirst[i];
if (!isNaN(v) && v > 0) {
return v;
}
}
const summaryDue = parseFloat(window.yatraBookingSummary?.amountDue);
if (!isNaN(summaryDue) && summaryDue > 0) {
return summaryDue;
}
const candidates = [
parseFloat(bookingData?.amount_due),
parseFloat(bookingData?.payment_due),
parseFloat(this.paymentForm?.dataset.paymentDue),
parseFloat(window.yatraBookingData?.paymentDue)
].map((val) => (isNaN(val) ? 0 : val));
const picked = candidates.find((val) => val > 0) ?? 0;
return picked > 0 ? picked : 0;
}
getCurrentPaymentDue() {
const summaryDue = parseFloat(window.yatraBookingSummary?.amountDue);
if (!isNaN(summaryDue) && summaryDue > 0) {
return summaryDue;
}
const candidates = [
parseFloat(this.paymentForm?.dataset.paymentDue),
parseFloat(window.yatraBookingData?.paymentDue)
].map((val) => (isNaN(val) ? 0 : val));
return candidates.find((val) => val > 0) ?? 0;
}
getPaymentRequestTotal() {
return {
label: this.tripTitle,
amount: this.getAmountInCents(this.getCurrentPaymentDue())
};
}
getAmountInCents(amount) {
const value = isNaN(amount) ? 0 : amount;
return Math.max(50, Math.round(value * 100));
}
/**
* Pageless confirmation URLs use the same base as Settings → Default Booking URL Base
* (e.g. book → /book/confirmation/{reference}/). Never hard-code /booking-confirmation/.
*/
buildConfirmationUrlFromBookingInfo(bookingInfo) {
if (bookingInfo.redirect_url) {
return bookingInfo.redirect_url;
}
const d = window.yatraBookingData || {};
const site = (d.siteUrl || window.location.origin || '').replace(/\/$/, '');
const ref = String(bookingInfo.reference || '').trim();
const id = bookingInfo.booking_id;
const segment = ref || (id != null && id !== '' ? String(id) : '');
if (!segment) {
return `${site}/`;
}
if (d.permalinkStructure === 'plain') {
return `${site}/?yatra_booking_confirmation=${encodeURIComponent(segment)}`;
}
const bb = String(d.bookingBase || 'book').replace(/^\/|\/$/g, '');
return `${site}/${bb}/confirmation/${encodeURIComponent(segment)}/`;
}
buildReturnUrl(bookingInfo) {
const base = this.buildConfirmationUrlFromBookingInfo(bookingInfo);
try {
const url = new URL(base, window.location.origin);
url.searchParams.set('stripe_return', '1');
return url.toString();
} catch (e) {
const join = base.includes('?') ? '&' : '?';
return `${base}${join}stripe_return=1`;
}
}
async notifyPaymentSuccess({ bookingId, transactionId, saveCard, amount, currency }) {
try {
const response = await fetch(`${this.apiUrl}/payment/confirm`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': this.nonce,
},
body: JSON.stringify({
gateway: 'stripe',
booking_id: bookingId,
transaction_id: transactionId,
save_card: !!saveCard,
amount,
currency,
}),
});
const payload = await response.json();
if (!response.ok || payload?.success === false) {
throw new Error(payload?.message || __('Failed to record payment.', 'yatra'));
}
return payload;
} catch (error) {
console.error('Failed to notify backend about payment success:', error);
throw error;
}
}
}
document.addEventListener('DOMContentLoaded', () => {
if (document.getElementById('yatra-booking-form')) {
const apiUrl = window.yatraBookingData?.apiUrl || '/wp-json/yatra/v1';
const nonce = window.yatraBookingData?.nonce || '';
new YatraStripe(apiUrl, nonce);
}
});