PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.8
Yatra – Travel Booking & Tour Operator Software v3.0.8
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / assets / js / stripe.js

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

1,270 lines 51.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Stripe Payment Integration for Yatra
3 * Supports Stripe Elements (card) and Payment Request (Google Pay / Apple Pay)
4 */
5
6 const __ = (window.wp && window.wp.i18n && window.wp.i18n.__) ? window.wp.i18n.__ : (text => text);
7
8 class YatraStripe {
9 constructor(apiUrl, nonce) {
10 this.apiUrl = apiUrl;
11 this.nonce = nonce;
12 this.stripe = null;
13 this.elements = null;
14 this.cardElement = null;
15 this.paymentRequest = null;
16 this.paymentRequestButton = null;
17 this.paymentRequestMounted = false;
18 this.paymentForm = document.getElementById('yatra-booking-form');
19 this.submitButton = document.getElementById('yatra-submit-booking');
20 this.originalButtonHtml = this.submitButton ? this.submitButton.innerHTML : '';
21 this.methodButtons = {};
22 this.activeMethod = null;
23 this.cardWrapper = null;
24 this.walletWrapper = null;
25 this.walletCapabilities = {
26 google_pay: false,
27 apple_pay: false,
28 };
29 this.walletMethodHints = {
30 google_pay: '',
31 apple_pay: '',
32 };
33 this.walletAvailabilityKnown = false;
34 /** Short labels for each method switcher option (tooltip base copy). */
35 this.methodHelpText = {};
36
37 this.stripeSettings = window.yatraBookingData?.stripe || {};
38 let enabledMethods = this.stripeSettings.enabledMethods || ['card'];
39 if (typeof enabledMethods === 'string') {
40 enabledMethods = enabledMethods.split(',');
41 }
42 if (!Array.isArray(enabledMethods) || enabledMethods.length === 0) {
43 enabledMethods = ['card'];
44 }
45 this.enabledMethods = enabledMethods.map((method) => method.trim().toLowerCase()).filter(Boolean);
46 if (this.enabledMethods.length === 0) {
47 this.enabledMethods = ['card'];
48 }
49
50 this.supportsCard = this.enabledMethods.includes('card');
51 this.supportsPaymentRequest = this.enabledMethods.some((method) => method === 'google_pay' || method === 'apple_pay');
52
53 this.companyCountry = window.yatraBookingData?.companyCountry || 'US';
54 this.tripTitle = window.yatraBookingData?.tripTitle || 'Trip Booking';
55
56 window.yatraStripeInstance = this;
57
58 if (document.readyState === 'loading') {
59 document.addEventListener('DOMContentLoaded', () => this.init());
60 } else {
61 this.init();
62 }
63 }
64
65 init() {
66 if (!document.querySelector('input[name="payment_gateway"][value="stripe"]')) {
67 return;
68 }
69
70 this.addStripeElementsContainer();
71
72 document.querySelectorAll('input[name="payment_gateway"]').forEach((radio) => {
73 radio.addEventListener('change', (e) => this.handlePaymentMethodChange(e));
74 });
75
76 if (typeof Stripe !== 'undefined') {
77 this.initializeStripe();
78 }
79
80 // Listen for unified booking submit event (new system)
81 document.addEventListener('yatra_booking_submit', (event) => this.handleBookingSubmit(event));
82
83 // Listen for payment response for client-side Stripe payments
84 document.addEventListener('yatra_payment_response', (event) => this.handlePaymentResponse(event));
85
86 // Legacy event support
87 document.addEventListener('yatraBeforeBookingSubmit', (event) => this.handleGatewayIntercept(event));
88 }
89
90 /**
91 * Handle unified booking submit event
92 */
93 handleBookingSubmit(event) {
94 if (event.detail?.gateway !== 'stripe') {
95 return; // Let other gateways handle
96 }
97
98 // Stripe intercepts the form submission to handle card payment
99 event.preventDefault();
100
101 // Use the existing handleGatewayIntercept logic
102 this.handleGatewayIntercept({
103 preventDefault: () => {},
104 detail: event.detail
105 });
106 }
107
108 /**
109 * Handle payment response for Stripe (when server returns requires_action)
110 */
111 handlePaymentResponse(event) {
112 if (event.detail?.requires_action !== 'stripe_payment') {
113 return;
114 }
115
116
117 this.handleStripePaymentAction(event.detail);
118 }
119
120 /**
121 * Handle Stripe payment action from server response
122 */
123 async handleStripePaymentAction(data) {
124 try {
125 // Load Stripe if not already loaded
126 if (!this.stripe && data.publishable_key) {
127 await this.loadStripeScript();
128 this.stripe = Stripe(data.publishable_key);
129 }
130
131 if (!this.stripe) {
132 throw new Error('Stripe not initialized');
133 }
134
135 const elements = this.stripe.elements({
136 clientSecret: data.client_secret
137 });
138
139 // Create payment element in a modal
140 const modal = document.createElement('div');
141 modal.id = 'yatra-stripe-modal';
142 modal.innerHTML = `
143 <div style="position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 9999; display: flex; align-items: center; justify-content: center;">
144 <div style="background: white; padding: 32px; border-radius: 12px; max-width: 500px; width: 90%; box-shadow: 0 4px 20px rgba(0,0,0,0.2);">
145 <h2 style="margin: 0 0 24px; text-align: center;">${__('Complete Your Payment', 'yatra')}</h2>
146 <div id="stripe-payment-element"></div>
147 <button id="stripe-submit-btn" style="width: 100%; padding: 16px; background: #635bff; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; margin-top: 24px; cursor: pointer;">
148 ${__('Pay', 'yatra')} ${data.currency} ${parseFloat(data.amount).toFixed(2)}
149 </button>
150 <button id="stripe-cancel-btn" style="width: 100%; padding: 12px; background: transparent; color: #666; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; margin-top: 12px; cursor: pointer;">
151 ${__('Cancel', 'yatra')}
152 </button>
153 <div id="stripe-error-message" style="margin-top: 16px; color: #ef4444; display: none;"></div>
154 </div>
155 </div>
156 `;
157 document.body.appendChild(modal);
158
159 const paymentElement = elements.create('payment');
160 paymentElement.mount('#stripe-payment-element');
161
162 document.getElementById('stripe-submit-btn').addEventListener('click', async () => {
163 const btn = document.getElementById('stripe-submit-btn');
164 btn.disabled = true;
165 btn.textContent = __('Processing...', 'yatra');
166
167 // Append `stripe=success` to the gateway's return URL using proper URL
168 // composition. The naive `+ '?stripe=success'` form breaks when
169 // confirmation_url already carries query params (e.g. `?balance=paid`
170 // from a remaining-balance flow) — two question marks produces an
171 // invalid URL and browsers drop the second segment, losing the flag.
172 let confirmReturnUrl = data.confirmation_url;
173 try {
174 const u = new URL(confirmReturnUrl, window.location.origin);
175 u.searchParams.set('stripe', 'success');
176 confirmReturnUrl = u.toString();
177 } catch (_e) {
178 // Fallback for malformed bases (shouldn't happen in practice)
179 const join = confirmReturnUrl.includes('?') ? '&' : '?';
180 confirmReturnUrl = `${confirmReturnUrl}${join}stripe=success`;
181 }
182
183 const { error } = await this.stripe.confirmPayment({
184 elements,
185 confirmParams: {
186 return_url: confirmReturnUrl,
187 },
188 });
189
190 if (error) {
191 document.getElementById('stripe-error-message').textContent = error.message;
192 document.getElementById('stripe-error-message').style.display = 'block';
193 btn.disabled = false;
194 btn.textContent = `${__('Pay', 'yatra')} ${data.currency} ${parseFloat(data.amount).toFixed(2)}`;
195 }
196 });
197
198 document.getElementById('stripe-cancel-btn').addEventListener('click', () => {
199 modal.remove();
200 document.dispatchEvent(new CustomEvent('yatra_payment_cancelled', {
201 detail: { gateway: 'stripe' }
202 }));
203 });
204
205 } catch (error) {
206 console.error('Stripe payment error:', error);
207 document.dispatchEvent(new CustomEvent('yatra_payment_failed', {
208 detail: { gateway: 'stripe', error: error.message }
209 }));
210 }
211 }
212
213 loadStripeScript() {
214 return new Promise((resolve, reject) => {
215 if (typeof Stripe !== 'undefined') {
216 resolve();
217 return;
218 }
219 const script = document.createElement('script');
220 script.src = 'https://js.stripe.com/v3/';
221 script.onload = resolve;
222 script.onerror = reject;
223 document.head.appendChild(script);
224 });
225 }
226
227 addStripeElementsContainer() {
228 const stripeOption = document.querySelector('input[name="payment_gateway"][value="stripe"]');
229 if (!stripeOption || document.querySelector('.yatra-stripe-container')) {
230 return;
231 }
232
233 const container = document.createElement('div');
234 container.className = 'yatra-stripe-container';
235 const stripePreselected = !!stripeOption.checked;
236 container.style.display = stripePreselected ? 'block' : 'none';
237
238 this.methodSwitcher = this.createMethodSwitcher();
239 container.appendChild(this.methodSwitcher);
240
241 if (this.supportsPaymentRequest) {
242 const requestWrapper = document.createElement('div');
243 requestWrapper.className = 'yatra-stripe-payment-request-wrapper';
244 const walletNote = window.yatraTranslations?.stripeWalletNote || __('Pay instantly with Apple Pay or Google Pay.', 'yatra');
245 const walletTitle = __('Apple Pay & Google Pay', 'yatra');
246 requestWrapper.innerHTML = `
247 <div class="yatra-wallet-header">
248 <div>
249 <p class="yatra-wallet-eyebrow">${__('One-tap checkout', 'yatra')}</p>
250 <h4 class="yatra-wallet-title">${walletTitle}</h4>
251 </div>
252 <div class="yatra-wallet-badges">
253 <span class="wallet-badge wallet-badge--apple"> Pay</span>
254 <span class="wallet-badge wallet-badge--google">G Pay</span>
255 </div>
256 </div>
257 <p class="yatra-wallet-desc">${walletNote}</p>
258 <div id="yatra-stripe-payment-request" class="yatra-stripe-payment-request placeholder"></div>
259 <div id="yatra-stripe-wallet-status" class="yatra-wallet-status">
260 ${__('Checking your device for wallet support…', 'yatra')}
261 </div>
262 `;
263 container.appendChild(requestWrapper);
264 this.walletWrapper = requestWrapper;
265 this.walletWrapper.style.display = 'none';
266 }
267
268 if (this.supportsCard) {
269 const cardWrapper = document.createElement('div');
270 cardWrapper.className = 'yatra-stripe-card-wrapper';
271 cardWrapper.innerHTML = `
272 <div id="yatra-stripe-card-element" class="yatra-stripe-card-element"></div>
273 <div id="yatra-stripe-card-errors" class="yatra-stripe-errors" role="alert"></div>
274 `;
275 container.appendChild(cardWrapper);
276 this.cardWrapper = cardWrapper;
277 this.cardWrapper.style.display = 'none';
278 }
279
280 const gatewayExtra = document.getElementById('yatra-gateway-extra-stripe');
281 if (gatewayExtra) {
282 gatewayExtra.appendChild(container);
283 } else {
284 let parentElement = stripeOption.closest('.yatra-payment-option')
285 || stripeOption.closest('.payment_method_stripe')
286 || stripeOption.closest('.payment-method')
287 || stripeOption.parentElement;
288
289 if (parentElement) {
290 parentElement.appendChild(container);
291 } else {
292 const paymentOptions = document.querySelector('.yatra-payment-options, .payment-methods');
293 if (paymentOptions && paymentOptions.parentNode) {
294 paymentOptions.parentNode.insertBefore(container, paymentOptions.nextSibling);
295 }
296 }
297 }
298
299 this.setActiveMethod(this.getDefaultMethod());
300 this.syncMethodButtonsState();
301
302 if (stripePreselected && !this.stripe) {
303 this.initializeStripe();
304 }
305 }
306
307 handlePaymentMethodChange(event) {
308 const selectedMethod = event.target.value;
309 const stripeContainer = document.querySelector('.yatra-stripe-container');
310
311 if (selectedMethod === 'stripe') {
312 if (!this.stripe) {
313 this.initializeStripe();
314 } else if (stripeContainer) {
315 stripeContainer.style.display = 'block';
316 }
317 } else if (stripeContainer) {
318 stripeContainer.style.display = 'none';
319 }
320 }
321
322 initializeStripe() {
323 const publishableKey = window.yatraBookingData?.stripe?.publishableKey
324 || window.yatraBookingData?.stripe?.publishable_key;
325 if (!publishableKey) {
326 this.displayError(__('Stripe publishable key is missing. Please contact support.', 'yatra'));
327 return;
328 }
329
330 if (typeof Stripe === 'undefined') {
331 const script = document.createElement('script');
332 script.src = 'https://js.stripe.com/v3/';
333 script.onload = () => this.setupStripeElements(publishableKey);
334 script.onerror = () => {
335 this.displayError(__('Failed to load Stripe.js. Please try again or choose another payment method.', 'yatra'));
336 };
337 document.head.appendChild(script);
338 } else {
339 this.setupStripeElements(publishableKey);
340 }
341 }
342
343 setupStripeElements(publishableKey) {
344 try {
345 this.stripe = Stripe(publishableKey, { apiVersion: '2023-10-16' });
346 this.elements = this.stripe.elements({
347 fonts: [{ cssSrc: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap' }],
348 locale: 'auto'
349 });
350
351 if (this.supportsCard) {
352 const style = {
353 base: {
354 color: '#32325d',
355 fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
356 fontSmoothing: 'antialiased',
357 fontSize: '16px',
358 '::placeholder': { color: '#aab7c4' }
359 },
360 invalid: {
361 color: '#e53935',
362 iconColor: '#e53935'
363 }
364 };
365
366 this.cardElement = this.elements.create('card', {
367 style,
368 hidePostalCode: true
369 });
370
371 const cardElement = document.getElementById('yatra-stripe-card-element');
372 if (cardElement) {
373 this.cardElement.mount(cardElement);
374 this.cardElement.on('change', (event) => this.displayError(event.error?.message || ''));
375 }
376 }
377
378 this.initializePaymentRequestButton();
379
380 const stripeContainer = document.querySelector('.yatra-stripe-container');
381 if (stripeContainer && document.querySelector('input[name="payment_gateway"]:checked')?.value === 'stripe') {
382 stripeContainer.style.display = 'block';
383 }
384 } catch (error) {
385 console.error('Error initializing Stripe:', error);
386 this.displayError(__('Failed to initialize payment processor. Please try again or use another payment method.', 'yatra'));
387 }
388 }
389
390 async handleGatewayIntercept(event) {
391 const selectedMethod = document.querySelector('input[name="payment_gateway"]:checked')?.value;
392 if (selectedMethod !== 'stripe') {
393 return true;
394 }
395
396 event.preventDefault();
397
398 if (this.activeMethod && this.activeMethod !== 'card') {
399 this.displayError(__('Please pay using the Apple Pay or Google Pay button above.', 'yatra'));
400 return false;
401 }
402
403 if (!this.supportsCard) {
404 this.displayError(__('Please use Apple Pay or Google Pay above to complete your payment.', 'yatra'));
405 return false;
406 }
407
408 const bookingData = event?.detail?.bookingData || this.collectBookingData();
409 if (!bookingData) {
410 this.displayError(__('Please complete the booking form before continuing.', 'yatra'));
411 return false;
412 }
413
414 if (!this.stripe || !this.cardElement) {
415 this.initializeStripe();
416 this.displayError(__('Stripe is still loading. Please wait a moment and try again.', 'yatra'));
417 return false;
418 }
419
420 if (!this.originalButtonHtml && event?.detail?.originalBtnHtml) {
421 this.originalButtonHtml = event.detail.originalBtnHtml;
422 } else if (!this.originalButtonHtml && this.submitButton) {
423 this.originalButtonHtml = this.submitButton.innerHTML;
424 }
425
426 this.displayError('');
427 this.setLoadingState(true);
428
429 try {
430 const paymentContext = await this.prepareStripePayment(bookingData, event?.detail?.form || this.paymentForm);
431 const { bookingInfo, clientSecret, billingDetails, effectiveAmount } = paymentContext;
432
433 const { error, paymentIntent } = await this.stripe.confirmCardPayment(clientSecret, {
434 payment_method: {
435 card: this.cardElement,
436 billing_details: billingDetails
437 },
438 return_url: this.buildReturnUrl(bookingInfo)
439 });
440
441 if (error) {
442 throw error;
443 }
444
445 if (paymentIntent.status === 'succeeded') {
446 await this.notifyPaymentSuccess({
447 bookingId: bookingInfo.booking_id,
448 transactionId: paymentIntent.id,
449 saveCard: !!bookingData.save_card,
450 amount: effectiveAmount,
451 currency: bookingInfo.currency || bookingData.currency || 'USD'
452 });
453 window.location.href = this.buildConfirmationUrlFromBookingInfo(bookingInfo);
454 } else {
455 throw new Error(__('Payment processing was not completed. Please check your payment details and try again.', 'yatra'));
456 }
457 } catch (error) {
458 console.error('Stripe payment error:', error);
459 this.displayError(error.message || __('Payment failed. Please try again or use another payment method.', 'yatra'));
460 this.setLoadingState(false);
461 }
462
463 return false;
464 }
465
466 async initializePaymentRequestButton() {
467 if (!this.supportsPaymentRequest || !this.stripe || this.paymentRequestMounted) {
468 return;
469 }
470
471 try {
472 this.paymentRequest = this.stripe.paymentRequest({
473 country: (this.companyCountry || 'US').toUpperCase(),
474 currency: (window.yatraBookingData?.currency || 'USD').toLowerCase(),
475 total: this.getPaymentRequestTotal(),
476 requestPayerName: true,
477 requestPayerEmail: true,
478 });
479
480 const result = await this.paymentRequest.canMakePayment();
481 if (!result) {
482 this.setWalletStatus(
483 __('Apple Pay and Google Pay are not available on this browser/device.', 'yatra'),
484 'warning'
485 );
486 this.updateWalletCapabilities(null);
487 this.showWalletPlaceholder(__('Use card payment instead or switch to a supported device.', 'yatra'));
488 this.syncMethodButtonsState();
489 return;
490 }
491
492 this.updateWalletCapabilities(result === true ? { applePay: true, googlePay: true } : result);
493 this.setWalletStatus(
494 __('Great! Apple Pay or Google Pay is available on this device.', 'yatra'),
495 'success'
496 );
497 this.syncMethodButtonsState();
498
499 this.paymentRequest.on('paymentmethod', (event) => this.handlePaymentRequestPayment(event));
500
501 this.paymentRequestButton = this.elements.create('paymentRequestButton', {
502 paymentRequest: this.paymentRequest,
503 style: {
504 paymentRequestButton: {
505 type: 'default',
506 theme: 'dark',
507 height: '48px'
508 }
509 }
510 });
511
512 const prContainer = document.getElementById('yatra-stripe-payment-request');
513 if (prContainer) {
514 this.paymentRequestButton.mount(prContainer);
515 this.paymentRequestMounted = true;
516 prContainer.classList.remove('placeholder');
517 const stripeContainer = document.querySelector('.yatra-stripe-container');
518 if (stripeContainer && document.querySelector('input[name="payment_gateway"]:checked')?.value === 'stripe') {
519 stripeContainer.style.display = 'block';
520 }
521 }
522 } catch (error) {
523 console.error('Failed to initialize Stripe Payment Request button:', error);
524 this.setWalletStatus(
525 __('Unable to load Apple Pay / Google Pay. Please try again later or use card payment.', 'yatra'),
526 'error'
527 );
528 this.showWalletPlaceholder(__('Wallet payment failed to load.', 'yatra'));
529 this.updateWalletCapabilities(null);
530 const errHint = __('Wallet payments could not be initialized. Use your card below, or try again after refreshing the page.', 'yatra');
531 this.walletMethodHints.google_pay = errHint;
532 this.walletMethodHints.apple_pay = errHint;
533 this.syncMethodButtonsState();
534 }
535 }
536
537 setWalletStatus(message, variant = 'info') {
538 const statusEl = document.getElementById('yatra-stripe-wallet-status');
539 if (!statusEl) {
540 return;
541 }
542 statusEl.textContent = message;
543 statusEl.classList.remove('is-success', 'is-warning', 'is-error');
544 if (variant === 'success') {
545 statusEl.classList.add('is-success');
546 } else if (variant === 'warning') {
547 statusEl.classList.add('is-warning');
548 } else if (variant === 'error') {
549 statusEl.classList.add('is-error');
550 }
551 }
552
553 showWalletPlaceholder(message) {
554 const container = document.getElementById('yatra-stripe-payment-request');
555 if (!container) {
556 return;
557 }
558 container.classList.add('wallet-placeholder');
559 container.innerHTML = `<span>${message}</span>`;
560 }
561
562 updateWalletCapabilities(result) {
563 const availability = { apple_pay: false, google_pay: false };
564
565 if (result && typeof result === 'object') {
566 availability.apple_pay = !!(result.applePay || result.apple_pay);
567 availability.google_pay = !!(result.googlePay || result.google_pay);
568 } else if (result === true) {
569 availability.apple_pay = true;
570 availability.google_pay = true;
571 }
572
573 this.walletCapabilities = availability;
574
575 const hintGoogle = !availability.google_pay && this.enabledMethods.includes('google_pay')
576 ? __('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')
577 : '';
578 const hintApple = !availability.apple_pay && this.enabledMethods.includes('apple_pay')
579 ? __('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')
580 : '';
581
582 this.walletMethodHints = {
583 google_pay: hintGoogle,
584 apple_pay: hintApple,
585 };
586 this.walletAvailabilityKnown = true;
587 }
588
589 createMethodSwitcher() {
590 const wrapper = document.createElement('div');
591 wrapper.className = 'yatra-method-switcher';
592
593 const availableButtons = [];
594
595 const header = document.createElement('div');
596 header.className = 'yatra-method-switcher__header';
597
598 const eyebrow = document.createElement('p');
599 eyebrow.className = 'yatra-label-eyebrow';
600 eyebrow.textContent = __('Choose a payment option', 'yatra');
601
602 const title = document.createElement('div');
603 title.className = 'yatra-method-switcher__title';
604 title.textContent = __('Payment methods', 'yatra');
605
606 const helper = document.createElement('p');
607 helper.className = 'yatra-method-switcher__helper';
608 helper.textContent = __('Pick the option that works best for you.', 'yatra');
609
610 header.appendChild(eyebrow);
611 header.appendChild(title);
612 header.appendChild(helper);
613
614 if (this.supportsCard) {
615 availableButtons.push({
616 id: 'card',
617 label: __('Card', 'yatra'),
618 description: __('Use any major debit or credit card via Stripe Elements.', 'yatra'),
619 icon: this.getMethodIconMarkup('card')
620 });
621 }
622
623 if (this.enabledMethods.includes('google_pay')) {
624 availableButtons.push({
625 id: 'google_pay',
626 label: __('Google Pay', 'yatra'),
627 description: __('Pay instantly with the cards saved to your Google account.', 'yatra'),
628 icon: this.getMethodIconMarkup('google_pay')
629 });
630 }
631
632 if (this.enabledMethods.includes('apple_pay')) {
633 availableButtons.push({
634 id: 'apple_pay',
635 label: __('Apple Pay', 'yatra'),
636 description: __('Use Apple Wallet on iPhone, iPad, or Mac to pay securely.', 'yatra'),
637 icon: this.getMethodIconMarkup('apple_pay')
638 });
639 }
640
641 const buttons = document.createElement('div');
642 buttons.className = 'yatra-method-switcher__buttons';
643
644 availableButtons.forEach((method) => {
645 this.methodHelpText[method.id] = method.description;
646 const btn = document.createElement('button');
647 btn.type = 'button';
648 btn.className = 'yatra-method-button';
649 btn.dataset.method = method.id;
650 btn.innerHTML = `
651 ${method.icon}
652 <div class="method-content">
653 <span class="method-label">${method.label}</span>
654 </div>
655 <span class="method-tooltip">
656 <svg viewBox="0 0 20 20" aria-hidden="true" focusable="false">
657 <circle cx="10" cy="10" r="9" stroke="currentColor" stroke-width="1.3" fill="none"></circle>
658 <circle cx="10" cy="6" r="1.2" fill="currentColor"></circle>
659 <path d="M10 9v6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"></path>
660 </svg>
661 </span>
662 `;
663 this.applyMethodTooltipText(btn.querySelector('.method-tooltip'), method.description);
664 btn.addEventListener('click', () => {
665 if (btn.getAttribute('aria-disabled') === 'true') {
666 return;
667 }
668 this.setActiveMethod(method.id);
669 });
670 this.methodButtons[method.id] = btn;
671 buttons.appendChild(btn);
672 });
673
674 wrapper.appendChild(header);
675 wrapper.appendChild(buttons);
676 return wrapper;
677 }
678
679 getMethodIconMarkup(type) {
680 const base = '<span class="method-icon method-icon--' + type + '" aria-hidden="true">';
681 const close = '</span>';
682
683 const icons = {
684 card: `${base}
685 <svg viewBox="0 0 40 24" role="presentation" focusable="false">
686 <rect x="1" y="1" width="38" height="22" rx="6" fill="#111827" stroke="rgba(255,255,255,0.2)" stroke-width="1" />
687 <rect x="4" y="8" width="32" height="3" fill="#4b5563"></rect>
688 <rect x="4" y="14" width="15" height="4" fill="#f3f4f6"></rect>
689 <circle cx="28" cy="16" r="3.5" fill="#f97316"></circle>
690 <circle cx="33" cy="16" r="3.5" fill="#facc15"></circle>
691 </svg>
692 ${close}`,
693 google_pay: `${base}
694 <svg viewBox="0 0 40 24" role="presentation" focusable="false">
695 <g transform="translate(8 4)">
696 <path fill="#ea4335" d="M6.4 7.6a3.6 3.6 0 0 1 3.4-3.6c1 0 1.7.4 2.2.8L14 3.8A6 6 0 0 0 9.8 2C6.5 2 3.7 4.7 3.7 8s2.8 6 6.1 6c1.8 0 3.3-.6 4.3-1.8 1.1-1.3 1.4-3.1 1.2-4.6H9.8v2.5h3.6c-.2.9-.7 1.7-1.5 2.3-.7.6-1.7.9-2.8.9-2.3 0-4.2-1.9-4.2-4.2z"></path>
697 <path fill="#4285f4" d="M20.5 4.8a2.8 2.8 0 0 1 2.6 1.6V5H25v9.2h-1.9V9.6a1.6 1.6 0 0 0-1.7-1.7c-1.1 0-1.7.7-1.7 1.9v4.4h-1.9V5h1.9v1.3c.4-.8 1.3-1.5 2.8-1.5z"></path>
698 <path fill="#34a853" d="M16.7 9.2c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2 2-.9 2-2zm3.9 0c0 3-2.3 5.2-5.9 5.2a5.9 5.9 0 1 1 0-11.8 5.8 5.8 0 0 1 4.1 1.7l-1.6 1.6A3.5 3.5 0 0 0 14.7 4C12.3 4 10.4 6 10.4 8.4s1.9 4.4 4.3 4.4c2 0 3.2-1.1 3.6-2.6h-3.6V8.7h5.8c.1.2.1.4.1.5z"></path>
699 </g>
700 </svg>
701 ${close}`,
702 apple_pay: `${base}
703 <svg viewBox="0 0 40 24" role="presentation" focusable="false">
704 <g transform="translate(7 3)" fill="currentColor">
705 <path d="M15.1 4.8c-.7.9-1.8 1.4-2.8 1.3-.1-1 .3-2 1-2.7.7-.8 2.1-1.5 3.2-1.5.1 1.1-.3 2.2-1.4 2.9z"></path>
706 <path d="M18.3 12.7c-.5 1.1-1.2 2.2-2.1 3.2-.9 1-1.8 1.9-3 1.9s-1.5-.6-2.9-.6-1.9.6-3 .6c-1.3 0-2.3-1.1-3.2-2.1C2.3 13.7 1 10 2.7 7.5c.8-1.2 2.2-2 3.6-2 1.4 0 2.4.9 3 .9.7 0 2-.9 3.6-.9.6 0 2.5.2 3.6 1.9.1.1 2 .7 1.8 5.3z"></path>
707 <rect x="20.5" y="4.3" width="1.9" height="9.4" rx="0.5"></rect>
708 <path d="M26.8 4.1c2.2 0 3.8 1.4 3.8 3.4 0 2.1-1.6 3.4-3.9 3.4h-2V14h-2V4.1zm-2 5.4h1.7c1.2 0 2-.7 2-1.9s-.8-1.9-2-1.9h-1.7z"></path>
709 </g>
710 </svg>
711 ${close}`
712 };
713
714 return icons[type] || `${base}</span>`;
715 }
716
717 getDefaultMethod() {
718 if (this.supportsCard) {
719 return 'card';
720 }
721
722 if (this.enabledMethods.includes('google_pay')) {
723 return 'google_pay';
724 }
725
726 if (this.enabledMethods.includes('apple_pay')) {
727 return 'apple_pay';
728 }
729
730 return 'card';
731 }
732
733 setActiveMethod(method) {
734 if (!method) {
735 return;
736 }
737
738 // Guard against selecting unsupported methods
739 if (!this.isMethodSelectable(method)) {
740 method = this.getDefaultMethod();
741 }
742
743 if (this.activeMethod === method) {
744 return;
745 }
746
747 this.activeMethod = method;
748
749 if (this.cardWrapper) {
750 this.cardWrapper.style.display = method === 'card' ? 'block' : 'none';
751 }
752
753 if (this.walletWrapper) {
754 const shouldShowWallet = method === 'google_pay' || method === 'apple_pay';
755 this.walletWrapper.style.display = shouldShowWallet ? 'block' : 'none';
756 }
757
758 this.syncMethodButtonsState();
759 }
760
761 syncMethodButtonsState() {
762 Object.entries(this.methodButtons).forEach(([method, button]) => {
763 if (!button) {
764 return;
765 }
766
767 const isActive = method === this.activeMethod;
768 button.classList.toggle('is-active', isActive);
769 button.classList.remove('is-disabled', 'is-pending');
770 button.removeAttribute('aria-disabled');
771
772 const tooltipEl = button.querySelector('.method-tooltip');
773 const baseHelp = this.methodHelpText[method] || '';
774
775 if (this.isWalletMethod(method)) {
776 if (!this.walletAvailabilityKnown) {
777 button.classList.add('is-pending');
778 button.setAttribute('aria-disabled', 'true');
779 this.applyMethodTooltipText(
780 tooltipEl,
781 __('Checking if Google Pay or Apple Pay is available on this device…', 'yatra')
782 );
783 } else {
784 const isSupported = !!this.walletCapabilities[method];
785 if (!isSupported) {
786 button.classList.add('is-disabled');
787 button.setAttribute('aria-disabled', 'true');
788 const why = this.walletMethodHints[method] || __('This wallet is not available on this device or browser. Use card payment instead.', 'yatra');
789 this.applyMethodTooltipText(tooltipEl, why);
790 if (isActive) {
791 this.setActiveMethod('card');
792 }
793 } else {
794 this.applyMethodTooltipText(tooltipEl, baseHelp);
795 }
796 }
797 } else {
798 this.applyMethodTooltipText(tooltipEl, baseHelp);
799 }
800 });
801
802 // Ensure at least one method is visible if everything else disabled
803 const activeBtn = this.activeMethod ? this.methodButtons[this.activeMethod] : null;
804 const activeBlocked = activeBtn && activeBtn.getAttribute('aria-disabled') === 'true';
805 if (!this.activeMethod || activeBlocked) {
806 const fallback = this.supportsCard ? 'card'
807 : (this.walletCapabilities.google_pay ? 'google_pay'
808 : (this.walletCapabilities.apple_pay ? 'apple_pay' : null));
809 if (fallback) {
810 this.setActiveMethod(fallback);
811 }
812 }
813 }
814
815 /**
816 * @param {HTMLElement | null} el
817 * @param {string} text
818 */
819 applyMethodTooltipText(el, text) {
820 if (!el) {
821 return;
822 }
823 const t = String(text);
824 el.setAttribute('data-tooltip', t);
825 el.setAttribute('aria-label', t);
826 el.setAttribute('title', t);
827 }
828
829 isWalletMethod(method) {
830 return method === 'google_pay' || method === 'apple_pay';
831 }
832
833 isMethodSelectable(method) {
834 if (method === 'card') {
835 return !!this.supportsCard;
836 }
837
838 if (this.isWalletMethod(method)) {
839 if (!this.walletAvailabilityKnown) {
840 return false;
841 }
842 return !!this.walletCapabilities[method];
843 }
844
845 return false;
846 }
847
848 async handlePaymentRequestPayment(event) {
849 try {
850 const bookingData = this.collectBookingData();
851 if (!bookingData) {
852 throw new Error(__('Please complete the booking form before using Apple Pay or Google Pay.', 'yatra'));
853 }
854
855 this.setLoadingState(true);
856 const paymentContext = await this.prepareStripePayment(bookingData, this.paymentForm);
857 const { bookingInfo, clientSecret, effectiveAmount } = paymentContext;
858
859 const confirmation = await this.stripe.confirmCardPayment(clientSecret, {
860 payment_method: event.paymentMethod.id,
861 return_url: this.buildReturnUrl(bookingInfo)
862 });
863
864 if (confirmation.error) {
865 event.complete('fail');
866 throw confirmation.error;
867 }
868
869 event.complete('success');
870
871 if (confirmation.paymentIntent?.status === 'requires_action') {
872 return;
873 }
874
875 if (confirmation.paymentIntent?.status === 'succeeded') {
876 await this.notifyPaymentSuccess({
877 bookingId: bookingInfo.booking_id,
878 transactionId: confirmation.paymentIntent.id,
879 saveCard: !!bookingData.save_card,
880 amount: effectiveAmount,
881 currency: bookingInfo.currency || bookingData.currency || 'USD'
882 });
883 }
884
885 window.location.href = this.buildConfirmationUrlFromBookingInfo(bookingInfo);
886 } catch (error) {
887 console.error('Stripe payment request error:', error);
888 if (event && typeof event.complete === 'function') {
889 event.complete('fail');
890 }
891 this.displayError(error.message || __('Payment failed. Please try again or use another payment method.', 'yatra'));
892 this.setLoadingState(false);
893 }
894 }
895
896 async prepareStripePayment(bookingData, formElement) {
897 // /booking/create requires BOTH the standard WP REST nonce
898 // (X-WP-Nonce) AND a booking-scoped action nonce
899 // (X-Yatra-Booking-Nonce). The booking-scoped one is what
900 // gates the actual write; the endpoint's public
901 // permission_callback intentionally bypasses WP's default
902 // cookie/nonce check so guest checkouts work, and falls back
903 // to the booking-scoped nonce as the real CSRF defence.
904 // Missing it returns { code: 'invalid_nonce' } 403.
905 const bookingNonce =
906 (typeof window !== 'undefined' && window.yatraBookingData && window.yatraBookingData.bookingNonce)
907 || (typeof document !== 'undefined'
908 ? (document.querySelector('input[name="yatra_booking_nonce"]') || {}).value
909 : '')
910 || '';
911
912 // Always call the same endpoint - server decides based on session type
913 const bookingResponse = await fetch(`${this.apiUrl}/booking/create`, {
914 method: 'POST',
915 headers: {
916 'Content-Type': 'application/json',
917 'X-WP-Nonce': this.nonce,
918 'X-Yatra-Booking-Nonce': bookingNonce
919 },
920 body: JSON.stringify(bookingData)
921 });
922
923 const bookingResult = await bookingResponse.json();
924
925 if (!bookingResult.success) {
926 throw new Error(bookingResult.message || __('Failed to process request. Please try again.', 'yatra'));
927 }
928
929 const bookingInfo = bookingResult.data;
930 const effectiveAmount = this.getEffectiveAmount(bookingData, bookingInfo);
931
932 if (bookingInfo.client_secret) {
933 const customerEmail = bookingInfo.customer_email || bookingData.contact_email || bookingData.email;
934 const customerName = bookingInfo.customer_name ||
935 [bookingData.contact_first_name, bookingData.contact_last_name]
936 .filter(Boolean)
937 .join(' ') || bookingData.full_name;
938
939 if (!customerEmail) {
940 throw new Error(__('Email address is required.', 'yatra'));
941 }
942
943 const billingDetails = this.getBillingDetails(bookingData, formElement);
944
945 return {
946 bookingInfo,
947 clientSecret: bookingInfo.client_secret,
948 billingDetails,
949 effectiveAmount,
950 };
951 }
952
953 const customerEmail = bookingInfo.customer_email || bookingData.contact_email || bookingData.email;
954 const customerName = bookingInfo.customer_name ||
955 [bookingData.contact_first_name, bookingData.contact_last_name]
956 .filter(Boolean)
957 .join(' ') || bookingData.full_name;
958
959 if (!customerEmail) {
960 throw new Error(__('Email address is required.', 'yatra'));
961 }
962
963 const paymentIntentResponse = await fetch(`${this.apiUrl}/payment/create-intent`, {
964 method: 'POST',
965 headers: {
966 'Content-Type': 'application/json',
967 'X-WP-Nonce': this.nonce
968 },
969 body: JSON.stringify({
970 gateway: 'stripe',
971 booking_id: bookingInfo.booking_id,
972 amount: effectiveAmount,
973 currency: bookingInfo.currency,
974 trip_id: bookingInfo.trip_id || bookingData.trip_id || null,
975 trip_date: bookingInfo.trip_date || bookingData.travel_date || '',
976 customer_email: customerEmail,
977 customer_name: customerName,
978 return_url: this.buildReturnUrl(bookingInfo)
979 })
980 });
981
982 const intentPayload = await paymentIntentResponse.json();
983 if (!paymentIntentResponse.ok) {
984 throw new Error(intentPayload?.message || __('Unable to process payment. Please try again.', 'yatra'));
985 }
986 const clientSecret = intentPayload.client_secret;
987
988 if (!clientSecret) {
989 throw new Error(intentPayload?.error?.message || __('Unable to process payment. Please try again.', 'yatra'));
990 }
991
992 const billingDetails = this.getBillingDetails(bookingData, formElement);
993
994 return {
995 bookingInfo,
996 clientSecret,
997 billingDetails,
998 effectiveAmount,
999 };
1000 }
1001
1002 getBillingDetails(bookingData, formElement) {
1003 const getField = (keys) => {
1004 const candidates = Array.isArray(keys) ? keys : [keys];
1005 for (const key of candidates) {
1006 if (bookingData && bookingData[key]) {
1007 return bookingData[key];
1008 }
1009 if (formElement) {
1010 const input = formElement.querySelector(`[name="${key}"]`);
1011 if (input && input.value) {
1012 return input.value;
1013 }
1014 }
1015 }
1016 return '';
1017 };
1018
1019 const firstName = getField(['billing_first_name', 'contact_first_name', 'first_name']);
1020 const lastName = getField(['billing_last_name', 'contact_last_name', 'last_name']);
1021 const email = getField(['billing_email', 'contact_email', 'email']);
1022 const phone = getField(['billing_phone', 'contact_phone', 'phone']);
1023 const line1 = getField(['billing_address_1', 'contact_address']);
1024 const city = getField(['billing_city', 'contact_city']);
1025 const country = getField(['billing_country', 'contact_country']);
1026 const postalCode = getField(['billing_postcode', 'contact_postcode', 'contact_zip']);
1027
1028 const billingDetails = {};
1029 const name = `${firstName} ${lastName}`.trim();
1030 if (name) {
1031 billingDetails.name = name;
1032 }
1033 if (email) {
1034 billingDetails.email = email;
1035 }
1036 if (phone) {
1037 billingDetails.phone = phone;
1038 }
1039
1040 const address = {};
1041 if (line1) {
1042 address.line1 = line1;
1043 }
1044 if (city) {
1045 address.city = city;
1046 }
1047 if (country) {
1048 const normalizedCountry = country.trim().toUpperCase();
1049 if (normalizedCountry.length === 2) {
1050 address.country = normalizedCountry;
1051 }
1052 }
1053 if (postalCode) {
1054 address.postal_code = postalCode;
1055 }
1056
1057 if (Object.keys(address).length > 0) {
1058 billingDetails.address = address;
1059 }
1060
1061 return billingDetails;
1062 }
1063
1064 collectBookingData() {
1065 if (!this.paymentForm) {
1066 return null;
1067 }
1068
1069 const formData = new FormData(this.paymentForm);
1070 const bookingData = {};
1071
1072 for (const [key, value] of formData.entries()) {
1073 const matches = key.match(/^([^\[]+)(?:\[([^\]]*)\])?(?:\[([^\]]*)\])?$/);
1074
1075 if (matches) {
1076 const [, base, index, field] = matches;
1077
1078 if (index !== undefined && field !== undefined) {
1079 bookingData[base] = bookingData[base] || {};
1080 bookingData[base][index] = bookingData[base][index] || {};
1081 bookingData[base][index][field] = value;
1082 } else if (index !== undefined) {
1083 bookingData[base] = bookingData[base] || [];
1084 bookingData[base][index] = value;
1085 } else {
1086 bookingData[key] = value;
1087 }
1088 } else {
1089 bookingData[key] = value;
1090 }
1091 }
1092
1093 bookingData.accept_terms = this.paymentForm.querySelector('input[name="accept_terms"]')?.checked || false;
1094 bookingData.accept_privacy = this.paymentForm.querySelector('input[name="accept_privacy"]')?.checked || false;
1095 bookingData.subscribe_newsletter = this.paymentForm.querySelector('input[name="subscribe_newsletter"]')?.checked || false;
1096 bookingData.payment_due = parseFloat(this.paymentForm.getAttribute('data-payment-due'))
1097 || window.yatraBookingData?.paymentDue
1098 || null;
1099 bookingData.currency = window.yatraBookingData?.currency
1100 || bookingData.currency
1101 || this.paymentForm.querySelector('input[name="currency"]')?.value
1102 || 'USD';
1103 bookingData.payment_gateway = 'stripe';
1104
1105 return bookingData;
1106 }
1107
1108 displayError(message) {
1109 const errorElement = document.getElementById('yatra-stripe-card-errors');
1110 if (errorElement) {
1111 errorElement.textContent = message || '';
1112 errorElement.style.display = message ? 'block' : 'none';
1113 if (message) {
1114 errorElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
1115 }
1116 } else if (message) {
1117 console.error(message);
1118 }
1119 }
1120
1121 setLoadingState(isLoading) {
1122 if (!this.submitButton) {
1123 return;
1124 }
1125
1126 if (isLoading) {
1127 this.submitButton.disabled = true;
1128 // Spinner markup is fixed; only the user-visible text is
1129 // wrapped so it ends up in the .pot with a clean msgid.
1130 this.submitButton.innerHTML =
1131 '<span class="yatra-spinner"></span> ' + __('Processing Payment...', 'yatra');
1132 } else {
1133 this.submitButton.disabled = false;
1134 this.submitButton.innerHTML = this.originalButtonHtml;
1135 }
1136 }
1137
1138 getEffectiveAmount(bookingData, bookingInfo) {
1139 // After /booking/create, server-calculated charge (deposit / partial / full) is authoritative.
1140 // `amount` = charge for this checkout (matches gateway). Never fall back to `total_amount` (full order).
1141 const serverFirst = [
1142 parseFloat(bookingInfo?.amount),
1143 parseFloat(bookingInfo?.amount_due)
1144 ];
1145 for (let i = 0; i < serverFirst.length; i++) {
1146 const v = serverFirst[i];
1147 if (!isNaN(v) && v > 0) {
1148 return v;
1149 }
1150 }
1151
1152 const summaryDue = parseFloat(window.yatraBookingSummary?.amountDue);
1153 if (!isNaN(summaryDue) && summaryDue > 0) {
1154 return summaryDue;
1155 }
1156
1157 const candidates = [
1158 parseFloat(bookingData?.amount_due),
1159 parseFloat(bookingData?.payment_due),
1160 parseFloat(this.paymentForm?.dataset.paymentDue),
1161 parseFloat(window.yatraBookingData?.paymentDue)
1162 ].map((val) => (isNaN(val) ? 0 : val));
1163
1164 const picked = candidates.find((val) => val > 0) ?? 0;
1165 return picked > 0 ? picked : 0;
1166 }
1167
1168 getCurrentPaymentDue() {
1169 const summaryDue = parseFloat(window.yatraBookingSummary?.amountDue);
1170 if (!isNaN(summaryDue) && summaryDue > 0) {
1171 return summaryDue;
1172 }
1173
1174 const candidates = [
1175 parseFloat(this.paymentForm?.dataset.paymentDue),
1176 parseFloat(window.yatraBookingData?.paymentDue)
1177 ].map((val) => (isNaN(val) ? 0 : val));
1178
1179 return candidates.find((val) => val > 0) ?? 0;
1180 }
1181
1182 getPaymentRequestTotal() {
1183 return {
1184 label: this.tripTitle,
1185 amount: this.getAmountInCents(this.getCurrentPaymentDue())
1186 };
1187 }
1188
1189 getAmountInCents(amount) {
1190 const value = isNaN(amount) ? 0 : amount;
1191 return Math.max(50, Math.round(value * 100));
1192 }
1193
1194 /**
1195 * Pageless confirmation URLs use the same base as Settings → Default Booking URL Base
1196 * (e.g. book → /book/confirmation/{reference}/). Never hard-code /booking-confirmation/.
1197 */
1198 buildConfirmationUrlFromBookingInfo(bookingInfo) {
1199 if (bookingInfo.redirect_url) {
1200 return bookingInfo.redirect_url;
1201 }
1202 const d = window.yatraBookingData || {};
1203 const site = (d.siteUrl || window.location.origin || '').replace(/\/$/, '');
1204 const ref = String(bookingInfo.reference || '').trim();
1205 const id = bookingInfo.booking_id;
1206 const segment = ref || (id != null && id !== '' ? String(id) : '');
1207
1208 if (!segment) {
1209 return `${site}/`;
1210 }
1211
1212 if (d.permalinkStructure === 'plain') {
1213 return `${site}/?yatra_booking_confirmation=${encodeURIComponent(segment)}`;
1214 }
1215
1216 const bb = String(d.bookingBase || 'book').replace(/^\/|\/$/g, '');
1217 return `${site}/${bb}/confirmation/${encodeURIComponent(segment)}/`;
1218 }
1219
1220 buildReturnUrl(bookingInfo) {
1221 const base = this.buildConfirmationUrlFromBookingInfo(bookingInfo);
1222 try {
1223 const url = new URL(base, window.location.origin);
1224 url.searchParams.set('stripe_return', '1');
1225 return url.toString();
1226 } catch (e) {
1227 const join = base.includes('?') ? '&' : '?';
1228 return `${base}${join}stripe_return=1`;
1229 }
1230 }
1231
1232 async notifyPaymentSuccess({ bookingId, transactionId, saveCard, amount, currency }) {
1233 try {
1234 const response = await fetch(`${this.apiUrl}/payment/confirm`, {
1235 method: 'POST',
1236 headers: {
1237 'Content-Type': 'application/json',
1238 'X-WP-Nonce': this.nonce,
1239 },
1240 body: JSON.stringify({
1241 gateway: 'stripe',
1242 booking_id: bookingId,
1243 transaction_id: transactionId,
1244 save_card: !!saveCard,
1245 amount,
1246 currency,
1247 }),
1248 });
1249
1250 const payload = await response.json();
1251 if (!response.ok || payload?.success === false) {
1252 throw new Error(payload?.message || __('Failed to record payment.', 'yatra'));
1253 }
1254
1255 return payload;
1256 } catch (error) {
1257 console.error('Failed to notify backend about payment success:', error);
1258 throw error;
1259 }
1260 }
1261 }
1262
1263 document.addEventListener('DOMContentLoaded', () => {
1264 if (document.getElementById('yatra-booking-form')) {
1265 const apiUrl = window.yatraBookingData?.apiUrl || '/wp-json/yatra/v1';
1266 const nonce = window.yatraBookingData?.nonce || '';
1267 new YatraStripe(apiUrl, nonce);
1268 }
1269 });
1270