PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.13
Yatra – Travel Booking & Tour Operator Software v3.0.13
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.13, at assets/js/stripe.js

1,282 lines 51.8 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 // Show the "Payment methods" chooser only when there's an actual choice
240 // (Card + Apple/Google Pay). With Card as the only enabled method the
241 // chooser is redundant chrome, so present the card field directly.
242 if (this.hasMultipleMethods) {
243 container.appendChild(this.methodSwitcher);
244 } else {
245 container.classList.add('yatra-stripe-container--single-method');
246 }
247
248 if (this.supportsPaymentRequest) {
249 const requestWrapper = document.createElement('div');
250 requestWrapper.className = 'yatra-stripe-payment-request-wrapper';
251 const walletNote = window.yatraTranslations?.stripeWalletNote || __('Pay instantly with Apple Pay or Google Pay.', 'yatra');
252 const walletTitle = __('Apple Pay & Google Pay', 'yatra');
253 requestWrapper.innerHTML = `
254 <div class="yatra-wallet-header">
255 <div>
256 <p class="yatra-wallet-eyebrow">${__('One-tap checkout', 'yatra')}</p>
257 <h4 class="yatra-wallet-title">${walletTitle}</h4>
258 </div>
259 <div class="yatra-wallet-badges">
260 <span class="wallet-badge wallet-badge--apple"> Pay</span>
261 <span class="wallet-badge wallet-badge--google">G Pay</span>
262 </div>
263 </div>
264 <p class="yatra-wallet-desc">${walletNote}</p>
265 <div id="yatra-stripe-payment-request" class="yatra-stripe-payment-request placeholder"></div>
266 <div id="yatra-stripe-wallet-status" class="yatra-wallet-status">
267 ${__('Checking your device for wallet support…', 'yatra')}
268 </div>
269 `;
270 container.appendChild(requestWrapper);
271 this.walletWrapper = requestWrapper;
272 this.walletWrapper.style.display = 'none';
273 }
274
275 if (this.supportsCard) {
276 const cardWrapper = document.createElement('div');
277 cardWrapper.className = 'yatra-stripe-card-wrapper';
278 cardWrapper.innerHTML = `
279 <div id="yatra-stripe-card-element" class="yatra-stripe-card-element"></div>
280 <div id="yatra-stripe-card-errors" class="yatra-stripe-errors" role="alert"></div>
281 `;
282 container.appendChild(cardWrapper);
283 this.cardWrapper = cardWrapper;
284 this.cardWrapper.style.display = 'none';
285 }
286
287 const gatewayExtra = document.getElementById('yatra-gateway-extra-stripe');
288 if (gatewayExtra) {
289 gatewayExtra.appendChild(container);
290 } else {
291 let parentElement = stripeOption.closest('.yatra-payment-option')
292 || stripeOption.closest('.payment_method_stripe')
293 || stripeOption.closest('.payment-method')
294 || stripeOption.parentElement;
295
296 if (parentElement) {
297 parentElement.appendChild(container);
298 } else {
299 const paymentOptions = document.querySelector('.yatra-payment-options, .payment-methods');
300 if (paymentOptions && paymentOptions.parentNode) {
301 paymentOptions.parentNode.insertBefore(container, paymentOptions.nextSibling);
302 }
303 }
304 }
305
306 this.setActiveMethod(this.getDefaultMethod());
307 this.syncMethodButtonsState();
308
309 if (stripePreselected && !this.stripe) {
310 this.initializeStripe();
311 }
312 }
313
314 handlePaymentMethodChange(event) {
315 const selectedMethod = event.target.value;
316 const stripeContainer = document.querySelector('.yatra-stripe-container');
317
318 if (selectedMethod === 'stripe') {
319 if (!this.stripe) {
320 this.initializeStripe();
321 } else if (stripeContainer) {
322 stripeContainer.style.display = 'block';
323 }
324 } else if (stripeContainer) {
325 stripeContainer.style.display = 'none';
326 }
327 }
328
329 initializeStripe() {
330 const publishableKey = window.yatraBookingData?.stripe?.publishableKey
331 || window.yatraBookingData?.stripe?.publishable_key;
332 if (!publishableKey) {
333 this.displayError(__('Stripe publishable key is missing. Please contact support.', 'yatra'));
334 return;
335 }
336
337 if (typeof Stripe === 'undefined') {
338 const script = document.createElement('script');
339 script.src = 'https://js.stripe.com/v3/';
340 script.onload = () => this.setupStripeElements(publishableKey);
341 script.onerror = () => {
342 this.displayError(__('Failed to load Stripe.js. Please try again or choose another payment method.', 'yatra'));
343 };
344 document.head.appendChild(script);
345 } else {
346 this.setupStripeElements(publishableKey);
347 }
348 }
349
350 setupStripeElements(publishableKey) {
351 try {
352 this.stripe = Stripe(publishableKey, { apiVersion: '2023-10-16' });
353 this.elements = this.stripe.elements({
354 fonts: [{ cssSrc: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap' }],
355 locale: 'auto'
356 });
357
358 if (this.supportsCard) {
359 const style = {
360 base: {
361 color: '#32325d',
362 fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
363 fontSmoothing: 'antialiased',
364 fontSize: '16px',
365 '::placeholder': { color: '#aab7c4' }
366 },
367 invalid: {
368 color: '#e53935',
369 iconColor: '#e53935'
370 }
371 };
372
373 this.cardElement = this.elements.create('card', {
374 style,
375 hidePostalCode: true
376 });
377
378 const cardElement = document.getElementById('yatra-stripe-card-element');
379 if (cardElement) {
380 this.cardElement.mount(cardElement);
381 this.cardElement.on('change', (event) => this.displayError(event.error?.message || ''));
382 }
383 }
384
385 this.initializePaymentRequestButton();
386
387 const stripeContainer = document.querySelector('.yatra-stripe-container');
388 if (stripeContainer && document.querySelector('input[name="payment_gateway"]:checked')?.value === 'stripe') {
389 stripeContainer.style.display = 'block';
390 }
391 } catch (error) {
392 console.error('Error initializing Stripe:', error);
393 this.displayError(__('Failed to initialize payment processor. Please try again or use another payment method.', 'yatra'));
394 }
395 }
396
397 async handleGatewayIntercept(event) {
398 const selectedMethod = document.querySelector('input[name="payment_gateway"]:checked')?.value;
399 if (selectedMethod !== 'stripe') {
400 return true;
401 }
402
403 event.preventDefault();
404
405 if (this.activeMethod && this.activeMethod !== 'card') {
406 this.displayError(__('Please pay using the Apple Pay or Google Pay button above.', 'yatra'));
407 return false;
408 }
409
410 if (!this.supportsCard) {
411 this.displayError(__('Please use Apple Pay or Google Pay above to complete your payment.', 'yatra'));
412 return false;
413 }
414
415 const bookingData = event?.detail?.bookingData || this.collectBookingData();
416 if (!bookingData) {
417 this.displayError(__('Please complete the booking form before continuing.', 'yatra'));
418 return false;
419 }
420
421 if (!this.stripe || !this.cardElement) {
422 this.initializeStripe();
423 this.displayError(__('Stripe is still loading. Please wait a moment and try again.', 'yatra'));
424 return false;
425 }
426
427 if (!this.originalButtonHtml && event?.detail?.originalBtnHtml) {
428 this.originalButtonHtml = event.detail.originalBtnHtml;
429 } else if (!this.originalButtonHtml && this.submitButton) {
430 this.originalButtonHtml = this.submitButton.innerHTML;
431 }
432
433 this.displayError('');
434 this.setLoadingState(true);
435
436 try {
437 const paymentContext = await this.prepareStripePayment(bookingData, event?.detail?.form || this.paymentForm);
438 const { bookingInfo, clientSecret, billingDetails, effectiveAmount } = paymentContext;
439
440 const { error, paymentIntent } = await this.stripe.confirmCardPayment(clientSecret, {
441 payment_method: {
442 card: this.cardElement,
443 billing_details: billingDetails
444 },
445 return_url: this.buildReturnUrl(bookingInfo)
446 });
447
448 if (error) {
449 throw error;
450 }
451
452 if (paymentIntent.status === 'succeeded') {
453 await this.notifyPaymentSuccess({
454 bookingId: bookingInfo.booking_id,
455 transactionId: paymentIntent.id,
456 saveCard: !!bookingData.save_card,
457 amount: effectiveAmount,
458 currency: bookingInfo.currency || bookingData.currency || 'USD'
459 });
460 window.location.href = this.buildConfirmationUrlFromBookingInfo(bookingInfo);
461 } else {
462 throw new Error(__('Payment processing was not completed. Please check your payment details and try again.', 'yatra'));
463 }
464 } catch (error) {
465 console.error('Stripe payment error:', error);
466 this.displayError(error.message || __('Payment failed. Please try again or use another payment method.', 'yatra'));
467 this.setLoadingState(false);
468 }
469
470 return false;
471 }
472
473 async initializePaymentRequestButton() {
474 if (!this.supportsPaymentRequest || !this.stripe || this.paymentRequestMounted) {
475 return;
476 }
477
478 try {
479 this.paymentRequest = this.stripe.paymentRequest({
480 country: (this.companyCountry || 'US').toUpperCase(),
481 currency: (window.yatraBookingData?.currency || 'USD').toLowerCase(),
482 total: this.getPaymentRequestTotal(),
483 requestPayerName: true,
484 requestPayerEmail: true,
485 });
486
487 const result = await this.paymentRequest.canMakePayment();
488 if (!result) {
489 this.setWalletStatus(
490 __('Apple Pay and Google Pay are not available on this browser/device.', 'yatra'),
491 'warning'
492 );
493 this.updateWalletCapabilities(null);
494 this.showWalletPlaceholder(__('Use card payment instead or switch to a supported device.', 'yatra'));
495 this.syncMethodButtonsState();
496 return;
497 }
498
499 this.updateWalletCapabilities(result === true ? { applePay: true, googlePay: true } : result);
500 this.setWalletStatus(
501 __('Great! Apple Pay or Google Pay is available on this device.', 'yatra'),
502 'success'
503 );
504 this.syncMethodButtonsState();
505
506 this.paymentRequest.on('paymentmethod', (event) => this.handlePaymentRequestPayment(event));
507
508 this.paymentRequestButton = this.elements.create('paymentRequestButton', {
509 paymentRequest: this.paymentRequest,
510 style: {
511 paymentRequestButton: {
512 type: 'default',
513 theme: 'dark',
514 height: '48px'
515 }
516 }
517 });
518
519 const prContainer = document.getElementById('yatra-stripe-payment-request');
520 if (prContainer) {
521 this.paymentRequestButton.mount(prContainer);
522 this.paymentRequestMounted = true;
523 prContainer.classList.remove('placeholder');
524 const stripeContainer = document.querySelector('.yatra-stripe-container');
525 if (stripeContainer && document.querySelector('input[name="payment_gateway"]:checked')?.value === 'stripe') {
526 stripeContainer.style.display = 'block';
527 }
528 }
529 } catch (error) {
530 console.error('Failed to initialize Stripe Payment Request button:', error);
531 this.setWalletStatus(
532 __('Unable to load Apple Pay / Google Pay. Please try again later or use card payment.', 'yatra'),
533 'error'
534 );
535 this.showWalletPlaceholder(__('Wallet payment failed to load.', 'yatra'));
536 this.updateWalletCapabilities(null);
537 const errHint = __('Wallet payments could not be initialized. Use your card below, or try again after refreshing the page.', 'yatra');
538 this.walletMethodHints.google_pay = errHint;
539 this.walletMethodHints.apple_pay = errHint;
540 this.syncMethodButtonsState();
541 }
542 }
543
544 setWalletStatus(message, variant = 'info') {
545 const statusEl = document.getElementById('yatra-stripe-wallet-status');
546 if (!statusEl) {
547 return;
548 }
549 statusEl.textContent = message;
550 statusEl.classList.remove('is-success', 'is-warning', 'is-error');
551 if (variant === 'success') {
552 statusEl.classList.add('is-success');
553 } else if (variant === 'warning') {
554 statusEl.classList.add('is-warning');
555 } else if (variant === 'error') {
556 statusEl.classList.add('is-error');
557 }
558 }
559
560 showWalletPlaceholder(message) {
561 const container = document.getElementById('yatra-stripe-payment-request');
562 if (!container) {
563 return;
564 }
565 container.classList.add('wallet-placeholder');
566 container.innerHTML = `<span>${message}</span>`;
567 }
568
569 updateWalletCapabilities(result) {
570 const availability = { apple_pay: false, google_pay: false };
571
572 if (result && typeof result === 'object') {
573 availability.apple_pay = !!(result.applePay || result.apple_pay);
574 availability.google_pay = !!(result.googlePay || result.google_pay);
575 } else if (result === true) {
576 availability.apple_pay = true;
577 availability.google_pay = true;
578 }
579
580 this.walletCapabilities = availability;
581
582 const hintGoogle = !availability.google_pay && this.enabledMethods.includes('google_pay')
583 ? __('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')
584 : '';
585 const hintApple = !availability.apple_pay && this.enabledMethods.includes('apple_pay')
586 ? __('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')
587 : '';
588
589 this.walletMethodHints = {
590 google_pay: hintGoogle,
591 apple_pay: hintApple,
592 };
593 this.walletAvailabilityKnown = true;
594 }
595
596 createMethodSwitcher() {
597 const wrapper = document.createElement('div');
598 wrapper.className = 'yatra-method-switcher';
599
600 const availableButtons = [];
601
602 const header = document.createElement('div');
603 header.className = 'yatra-method-switcher__header';
604
605 const eyebrow = document.createElement('p');
606 eyebrow.className = 'yatra-label-eyebrow';
607 eyebrow.textContent = __('Choose a payment option', 'yatra');
608
609 const title = document.createElement('div');
610 title.className = 'yatra-method-switcher__title';
611 title.textContent = __('Payment methods', 'yatra');
612
613 const helper = document.createElement('p');
614 helper.className = 'yatra-method-switcher__helper';
615 helper.textContent = __('Pick the option that works best for you.', 'yatra');
616
617 header.appendChild(eyebrow);
618 header.appendChild(title);
619 header.appendChild(helper);
620
621 if (this.supportsCard) {
622 availableButtons.push({
623 id: 'card',
624 label: __('Card', 'yatra'),
625 description: __('Use any major debit or credit card via Stripe Elements.', 'yatra'),
626 icon: this.getMethodIconMarkup('card')
627 });
628 }
629
630 if (this.enabledMethods.includes('google_pay')) {
631 availableButtons.push({
632 id: 'google_pay',
633 label: __('Google Pay', 'yatra'),
634 description: __('Pay instantly with the cards saved to your Google account.', 'yatra'),
635 icon: this.getMethodIconMarkup('google_pay')
636 });
637 }
638
639 if (this.enabledMethods.includes('apple_pay')) {
640 availableButtons.push({
641 id: 'apple_pay',
642 label: __('Apple Pay', 'yatra'),
643 description: __('Use Apple Wallet on iPhone, iPad, or Mac to pay securely.', 'yatra'),
644 icon: this.getMethodIconMarkup('apple_pay')
645 });
646 }
647
648 // Only a real choice (Card + a wallet) warrants the chooser. With Card
649 // as the only enabled method, the render step drops this switcher and
650 // shows the card field on its own (no redundant "Payment methods" row).
651 this.hasMultipleMethods = availableButtons.length > 1;
652
653 const buttons = document.createElement('div');
654 buttons.className = 'yatra-method-switcher__buttons';
655
656 availableButtons.forEach((method) => {
657 this.methodHelpText[method.id] = method.description;
658 const btn = document.createElement('button');
659 btn.type = 'button';
660 btn.className = 'yatra-method-button';
661 btn.dataset.method = method.id;
662 btn.innerHTML = `
663 ${method.icon}
664 <div class="method-content">
665 <span class="method-label">${method.label}</span>
666 </div>
667 <span class="method-tooltip">
668 <svg viewBox="0 0 20 20" aria-hidden="true" focusable="false">
669 <circle cx="10" cy="10" r="9" stroke="currentColor" stroke-width="1.3" fill="none"></circle>
670 <circle cx="10" cy="6" r="1.2" fill="currentColor"></circle>
671 <path d="M10 9v6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"></path>
672 </svg>
673 </span>
674 `;
675 this.applyMethodTooltipText(btn.querySelector('.method-tooltip'), method.description);
676 btn.addEventListener('click', () => {
677 if (btn.getAttribute('aria-disabled') === 'true') {
678 return;
679 }
680 this.setActiveMethod(method.id);
681 });
682 this.methodButtons[method.id] = btn;
683 buttons.appendChild(btn);
684 });
685
686 wrapper.appendChild(header);
687 wrapper.appendChild(buttons);
688 return wrapper;
689 }
690
691 getMethodIconMarkup(type) {
692 const base = '<span class="method-icon method-icon--' + type + '" aria-hidden="true">';
693 const close = '</span>';
694
695 const icons = {
696 card: `${base}
697 <svg viewBox="0 0 40 24" role="presentation" focusable="false">
698 <rect x="1" y="1" width="38" height="22" rx="6" fill="#111827" stroke="rgba(255,255,255,0.2)" stroke-width="1" />
699 <rect x="4" y="8" width="32" height="3" fill="#4b5563"></rect>
700 <rect x="4" y="14" width="15" height="4" fill="#f3f4f6"></rect>
701 <circle cx="28" cy="16" r="3.5" fill="#f97316"></circle>
702 <circle cx="33" cy="16" r="3.5" fill="#facc15"></circle>
703 </svg>
704 ${close}`,
705 google_pay: `${base}
706 <svg viewBox="0 0 40 24" role="presentation" focusable="false">
707 <g transform="translate(8 4)">
708 <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>
709 <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>
710 <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>
711 </g>
712 </svg>
713 ${close}`,
714 apple_pay: `${base}
715 <svg viewBox="0 0 40 24" role="presentation" focusable="false">
716 <g transform="translate(7 3)" fill="currentColor">
717 <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>
718 <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>
719 <rect x="20.5" y="4.3" width="1.9" height="9.4" rx="0.5"></rect>
720 <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>
721 </g>
722 </svg>
723 ${close}`
724 };
725
726 return icons[type] || `${base}</span>`;
727 }
728
729 getDefaultMethod() {
730 if (this.supportsCard) {
731 return 'card';
732 }
733
734 if (this.enabledMethods.includes('google_pay')) {
735 return 'google_pay';
736 }
737
738 if (this.enabledMethods.includes('apple_pay')) {
739 return 'apple_pay';
740 }
741
742 return 'card';
743 }
744
745 setActiveMethod(method) {
746 if (!method) {
747 return;
748 }
749
750 // Guard against selecting unsupported methods
751 if (!this.isMethodSelectable(method)) {
752 method = this.getDefaultMethod();
753 }
754
755 if (this.activeMethod === method) {
756 return;
757 }
758
759 this.activeMethod = method;
760
761 if (this.cardWrapper) {
762 this.cardWrapper.style.display = method === 'card' ? 'block' : 'none';
763 }
764
765 if (this.walletWrapper) {
766 const shouldShowWallet = method === 'google_pay' || method === 'apple_pay';
767 this.walletWrapper.style.display = shouldShowWallet ? 'block' : 'none';
768 }
769
770 this.syncMethodButtonsState();
771 }
772
773 syncMethodButtonsState() {
774 Object.entries(this.methodButtons).forEach(([method, button]) => {
775 if (!button) {
776 return;
777 }
778
779 const isActive = method === this.activeMethod;
780 button.classList.toggle('is-active', isActive);
781 button.classList.remove('is-disabled', 'is-pending');
782 button.removeAttribute('aria-disabled');
783
784 const tooltipEl = button.querySelector('.method-tooltip');
785 const baseHelp = this.methodHelpText[method] || '';
786
787 if (this.isWalletMethod(method)) {
788 if (!this.walletAvailabilityKnown) {
789 button.classList.add('is-pending');
790 button.setAttribute('aria-disabled', 'true');
791 this.applyMethodTooltipText(
792 tooltipEl,
793 __('Checking if Google Pay or Apple Pay is available on this device…', 'yatra')
794 );
795 } else {
796 const isSupported = !!this.walletCapabilities[method];
797 if (!isSupported) {
798 button.classList.add('is-disabled');
799 button.setAttribute('aria-disabled', 'true');
800 const why = this.walletMethodHints[method] || __('This wallet is not available on this device or browser. Use card payment instead.', 'yatra');
801 this.applyMethodTooltipText(tooltipEl, why);
802 if (isActive) {
803 this.setActiveMethod('card');
804 }
805 } else {
806 this.applyMethodTooltipText(tooltipEl, baseHelp);
807 }
808 }
809 } else {
810 this.applyMethodTooltipText(tooltipEl, baseHelp);
811 }
812 });
813
814 // Ensure at least one method is visible if everything else disabled
815 const activeBtn = this.activeMethod ? this.methodButtons[this.activeMethod] : null;
816 const activeBlocked = activeBtn && activeBtn.getAttribute('aria-disabled') === 'true';
817 if (!this.activeMethod || activeBlocked) {
818 const fallback = this.supportsCard ? 'card'
819 : (this.walletCapabilities.google_pay ? 'google_pay'
820 : (this.walletCapabilities.apple_pay ? 'apple_pay' : null));
821 if (fallback) {
822 this.setActiveMethod(fallback);
823 }
824 }
825 }
826
827 /**
828 * @param {HTMLElement | null} el
829 * @param {string} text
830 */
831 applyMethodTooltipText(el, text) {
832 if (!el) {
833 return;
834 }
835 const t = String(text);
836 el.setAttribute('data-tooltip', t);
837 el.setAttribute('aria-label', t);
838 el.setAttribute('title', t);
839 }
840
841 isWalletMethod(method) {
842 return method === 'google_pay' || method === 'apple_pay';
843 }
844
845 isMethodSelectable(method) {
846 if (method === 'card') {
847 return !!this.supportsCard;
848 }
849
850 if (this.isWalletMethod(method)) {
851 if (!this.walletAvailabilityKnown) {
852 return false;
853 }
854 return !!this.walletCapabilities[method];
855 }
856
857 return false;
858 }
859
860 async handlePaymentRequestPayment(event) {
861 try {
862 const bookingData = this.collectBookingData();
863 if (!bookingData) {
864 throw new Error(__('Please complete the booking form before using Apple Pay or Google Pay.', 'yatra'));
865 }
866
867 this.setLoadingState(true);
868 const paymentContext = await this.prepareStripePayment(bookingData, this.paymentForm);
869 const { bookingInfo, clientSecret, effectiveAmount } = paymentContext;
870
871 const confirmation = await this.stripe.confirmCardPayment(clientSecret, {
872 payment_method: event.paymentMethod.id,
873 return_url: this.buildReturnUrl(bookingInfo)
874 });
875
876 if (confirmation.error) {
877 event.complete('fail');
878 throw confirmation.error;
879 }
880
881 event.complete('success');
882
883 if (confirmation.paymentIntent?.status === 'requires_action') {
884 return;
885 }
886
887 if (confirmation.paymentIntent?.status === 'succeeded') {
888 await this.notifyPaymentSuccess({
889 bookingId: bookingInfo.booking_id,
890 transactionId: confirmation.paymentIntent.id,
891 saveCard: !!bookingData.save_card,
892 amount: effectiveAmount,
893 currency: bookingInfo.currency || bookingData.currency || 'USD'
894 });
895 }
896
897 window.location.href = this.buildConfirmationUrlFromBookingInfo(bookingInfo);
898 } catch (error) {
899 console.error('Stripe payment request error:', error);
900 if (event && typeof event.complete === 'function') {
901 event.complete('fail');
902 }
903 this.displayError(error.message || __('Payment failed. Please try again or use another payment method.', 'yatra'));
904 this.setLoadingState(false);
905 }
906 }
907
908 async prepareStripePayment(bookingData, formElement) {
909 // /booking/create requires BOTH the standard WP REST nonce
910 // (X-WP-Nonce) AND a booking-scoped action nonce
911 // (X-Yatra-Booking-Nonce). The booking-scoped one is what
912 // gates the actual write; the endpoint's public
913 // permission_callback intentionally bypasses WP's default
914 // cookie/nonce check so guest checkouts work, and falls back
915 // to the booking-scoped nonce as the real CSRF defence.
916 // Missing it returns { code: 'invalid_nonce' } 403.
917 const bookingNonce =
918 (typeof window !== 'undefined' && window.yatraBookingData && window.yatraBookingData.bookingNonce)
919 || (typeof document !== 'undefined'
920 ? (document.querySelector('input[name="yatra_booking_nonce"]') || {}).value
921 : '')
922 || '';
923
924 // Always call the same endpoint - server decides based on session type
925 const bookingResponse = await fetch(`${this.apiUrl}/booking/create`, {
926 method: 'POST',
927 headers: {
928 'Content-Type': 'application/json',
929 'X-WP-Nonce': this.nonce,
930 'X-Yatra-Booking-Nonce': bookingNonce
931 },
932 body: JSON.stringify(bookingData)
933 });
934
935 const bookingResult = await bookingResponse.json();
936
937 if (!bookingResult.success) {
938 throw new Error(bookingResult.message || __('Failed to process request. Please try again.', 'yatra'));
939 }
940
941 const bookingInfo = bookingResult.data;
942 const effectiveAmount = this.getEffectiveAmount(bookingData, bookingInfo);
943
944 if (bookingInfo.client_secret) {
945 const customerEmail = bookingInfo.customer_email || bookingData.contact_email || bookingData.email;
946 const customerName = bookingInfo.customer_name ||
947 [bookingData.contact_first_name, bookingData.contact_last_name]
948 .filter(Boolean)
949 .join(' ') || bookingData.full_name;
950
951 if (!customerEmail) {
952 throw new Error(__('Email address is required.', 'yatra'));
953 }
954
955 const billingDetails = this.getBillingDetails(bookingData, formElement);
956
957 return {
958 bookingInfo,
959 clientSecret: bookingInfo.client_secret,
960 billingDetails,
961 effectiveAmount,
962 };
963 }
964
965 const customerEmail = bookingInfo.customer_email || bookingData.contact_email || bookingData.email;
966 const customerName = bookingInfo.customer_name ||
967 [bookingData.contact_first_name, bookingData.contact_last_name]
968 .filter(Boolean)
969 .join(' ') || bookingData.full_name;
970
971 if (!customerEmail) {
972 throw new Error(__('Email address is required.', 'yatra'));
973 }
974
975 const paymentIntentResponse = await fetch(`${this.apiUrl}/payment/create-intent`, {
976 method: 'POST',
977 headers: {
978 'Content-Type': 'application/json',
979 'X-WP-Nonce': this.nonce
980 },
981 body: JSON.stringify({
982 gateway: 'stripe',
983 booking_id: bookingInfo.booking_id,
984 amount: effectiveAmount,
985 currency: bookingInfo.currency,
986 trip_id: bookingInfo.trip_id || bookingData.trip_id || null,
987 trip_date: bookingInfo.trip_date || bookingData.travel_date || '',
988 customer_email: customerEmail,
989 customer_name: customerName,
990 return_url: this.buildReturnUrl(bookingInfo)
991 })
992 });
993
994 const intentPayload = await paymentIntentResponse.json();
995 if (!paymentIntentResponse.ok) {
996 throw new Error(intentPayload?.message || __('Unable to process payment. Please try again.', 'yatra'));
997 }
998 const clientSecret = intentPayload.client_secret;
999
1000 if (!clientSecret) {
1001 throw new Error(intentPayload?.error?.message || __('Unable to process payment. Please try again.', 'yatra'));
1002 }
1003
1004 const billingDetails = this.getBillingDetails(bookingData, formElement);
1005
1006 return {
1007 bookingInfo,
1008 clientSecret,
1009 billingDetails,
1010 effectiveAmount,
1011 };
1012 }
1013
1014 getBillingDetails(bookingData, formElement) {
1015 const getField = (keys) => {
1016 const candidates = Array.isArray(keys) ? keys : [keys];
1017 for (const key of candidates) {
1018 if (bookingData && bookingData[key]) {
1019 return bookingData[key];
1020 }
1021 if (formElement) {
1022 const input = formElement.querySelector(`[name="${key}"]`);
1023 if (input && input.value) {
1024 return input.value;
1025 }
1026 }
1027 }
1028 return '';
1029 };
1030
1031 const firstName = getField(['billing_first_name', 'contact_first_name', 'first_name']);
1032 const lastName = getField(['billing_last_name', 'contact_last_name', 'last_name']);
1033 const email = getField(['billing_email', 'contact_email', 'email']);
1034 const phone = getField(['billing_phone', 'contact_phone', 'phone']);
1035 const line1 = getField(['billing_address_1', 'contact_address']);
1036 const city = getField(['billing_city', 'contact_city']);
1037 const country = getField(['billing_country', 'contact_country']);
1038 const postalCode = getField(['billing_postcode', 'contact_postcode', 'contact_zip']);
1039
1040 const billingDetails = {};
1041 const name = `${firstName} ${lastName}`.trim();
1042 if (name) {
1043 billingDetails.name = name;
1044 }
1045 if (email) {
1046 billingDetails.email = email;
1047 }
1048 if (phone) {
1049 billingDetails.phone = phone;
1050 }
1051
1052 const address = {};
1053 if (line1) {
1054 address.line1 = line1;
1055 }
1056 if (city) {
1057 address.city = city;
1058 }
1059 if (country) {
1060 const normalizedCountry = country.trim().toUpperCase();
1061 if (normalizedCountry.length === 2) {
1062 address.country = normalizedCountry;
1063 }
1064 }
1065 if (postalCode) {
1066 address.postal_code = postalCode;
1067 }
1068
1069 if (Object.keys(address).length > 0) {
1070 billingDetails.address = address;
1071 }
1072
1073 return billingDetails;
1074 }
1075
1076 collectBookingData() {
1077 if (!this.paymentForm) {
1078 return null;
1079 }
1080
1081 const formData = new FormData(this.paymentForm);
1082 const bookingData = {};
1083
1084 for (const [key, value] of formData.entries()) {
1085 const matches = key.match(/^([^\[]+)(?:\[([^\]]*)\])?(?:\[([^\]]*)\])?$/);
1086
1087 if (matches) {
1088 const [, base, index, field] = matches;
1089
1090 if (index !== undefined && field !== undefined) {
1091 bookingData[base] = bookingData[base] || {};
1092 bookingData[base][index] = bookingData[base][index] || {};
1093 bookingData[base][index][field] = value;
1094 } else if (index !== undefined) {
1095 bookingData[base] = bookingData[base] || [];
1096 bookingData[base][index] = value;
1097 } else {
1098 bookingData[key] = value;
1099 }
1100 } else {
1101 bookingData[key] = value;
1102 }
1103 }
1104
1105 bookingData.accept_terms = this.paymentForm.querySelector('input[name="accept_terms"]')?.checked || false;
1106 bookingData.accept_privacy = this.paymentForm.querySelector('input[name="accept_privacy"]')?.checked || false;
1107 bookingData.subscribe_newsletter = this.paymentForm.querySelector('input[name="subscribe_newsletter"]')?.checked || false;
1108 bookingData.payment_due = parseFloat(this.paymentForm.getAttribute('data-payment-due'))
1109 || window.yatraBookingData?.paymentDue
1110 || null;
1111 bookingData.currency = window.yatraBookingData?.currency
1112 || bookingData.currency
1113 || this.paymentForm.querySelector('input[name="currency"]')?.value
1114 || 'USD';
1115 bookingData.payment_gateway = 'stripe';
1116
1117 return bookingData;
1118 }
1119
1120 displayError(message) {
1121 const errorElement = document.getElementById('yatra-stripe-card-errors');
1122 if (errorElement) {
1123 errorElement.textContent = message || '';
1124 errorElement.style.display = message ? 'block' : 'none';
1125 if (message) {
1126 errorElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
1127 }
1128 } else if (message) {
1129 console.error(message);
1130 }
1131 }
1132
1133 setLoadingState(isLoading) {
1134 if (!this.submitButton) {
1135 return;
1136 }
1137
1138 if (isLoading) {
1139 this.submitButton.disabled = true;
1140 // Spinner markup is fixed; only the user-visible text is
1141 // wrapped so it ends up in the .pot with a clean msgid.
1142 this.submitButton.innerHTML =
1143 '<span class="yatra-spinner"></span> ' + __('Processing Payment...', 'yatra');
1144 } else {
1145 this.submitButton.disabled = false;
1146 this.submitButton.innerHTML = this.originalButtonHtml;
1147 }
1148 }
1149
1150 getEffectiveAmount(bookingData, bookingInfo) {
1151 // After /booking/create, server-calculated charge (deposit / partial / full) is authoritative.
1152 // `amount` = charge for this checkout (matches gateway). Never fall back to `total_amount` (full order).
1153 const serverFirst = [
1154 parseFloat(bookingInfo?.amount),
1155 parseFloat(bookingInfo?.amount_due)
1156 ];
1157 for (let i = 0; i < serverFirst.length; i++) {
1158 const v = serverFirst[i];
1159 if (!isNaN(v) && v > 0) {
1160 return v;
1161 }
1162 }
1163
1164 const summaryDue = parseFloat(window.yatraBookingSummary?.amountDue);
1165 if (!isNaN(summaryDue) && summaryDue > 0) {
1166 return summaryDue;
1167 }
1168
1169 const candidates = [
1170 parseFloat(bookingData?.amount_due),
1171 parseFloat(bookingData?.payment_due),
1172 parseFloat(this.paymentForm?.dataset.paymentDue),
1173 parseFloat(window.yatraBookingData?.paymentDue)
1174 ].map((val) => (isNaN(val) ? 0 : val));
1175
1176 const picked = candidates.find((val) => val > 0) ?? 0;
1177 return picked > 0 ? picked : 0;
1178 }
1179
1180 getCurrentPaymentDue() {
1181 const summaryDue = parseFloat(window.yatraBookingSummary?.amountDue);
1182 if (!isNaN(summaryDue) && summaryDue > 0) {
1183 return summaryDue;
1184 }
1185
1186 const candidates = [
1187 parseFloat(this.paymentForm?.dataset.paymentDue),
1188 parseFloat(window.yatraBookingData?.paymentDue)
1189 ].map((val) => (isNaN(val) ? 0 : val));
1190
1191 return candidates.find((val) => val > 0) ?? 0;
1192 }
1193
1194 getPaymentRequestTotal() {
1195 return {
1196 label: this.tripTitle,
1197 amount: this.getAmountInCents(this.getCurrentPaymentDue())
1198 };
1199 }
1200
1201 getAmountInCents(amount) {
1202 const value = isNaN(amount) ? 0 : amount;
1203 return Math.max(50, Math.round(value * 100));
1204 }
1205
1206 /**
1207 * Pageless confirmation URLs use the same base as Settings → Default Booking URL Base
1208 * (e.g. book → /book/confirmation/{reference}/). Never hard-code /booking-confirmation/.
1209 */
1210 buildConfirmationUrlFromBookingInfo(bookingInfo) {
1211 if (bookingInfo.redirect_url) {
1212 return bookingInfo.redirect_url;
1213 }
1214 const d = window.yatraBookingData || {};
1215 const site = (d.siteUrl || window.location.origin || '').replace(/\/$/, '');
1216 const ref = String(bookingInfo.reference || '').trim();
1217 const id = bookingInfo.booking_id;
1218 const segment = ref || (id != null && id !== '' ? String(id) : '');
1219
1220 if (!segment) {
1221 return `${site}/`;
1222 }
1223
1224 if (d.permalinkStructure === 'plain') {
1225 return `${site}/?yatra_booking_confirmation=${encodeURIComponent(segment)}`;
1226 }
1227
1228 const bb = String(d.bookingBase || 'book').replace(/^\/|\/$/g, '');
1229 return `${site}/${bb}/confirmation/${encodeURIComponent(segment)}/`;
1230 }
1231
1232 buildReturnUrl(bookingInfo) {
1233 const base = this.buildConfirmationUrlFromBookingInfo(bookingInfo);
1234 try {
1235 const url = new URL(base, window.location.origin);
1236 url.searchParams.set('stripe_return', '1');
1237 return url.toString();
1238 } catch (e) {
1239 const join = base.includes('?') ? '&' : '?';
1240 return `${base}${join}stripe_return=1`;
1241 }
1242 }
1243
1244 async notifyPaymentSuccess({ bookingId, transactionId, saveCard, amount, currency }) {
1245 try {
1246 const response = await fetch(`${this.apiUrl}/payment/confirm`, {
1247 method: 'POST',
1248 headers: {
1249 'Content-Type': 'application/json',
1250 'X-WP-Nonce': this.nonce,
1251 },
1252 body: JSON.stringify({
1253 gateway: 'stripe',
1254 booking_id: bookingId,
1255 transaction_id: transactionId,
1256 save_card: !!saveCard,
1257 amount,
1258 currency,
1259 }),
1260 });
1261
1262 const payload = await response.json();
1263 if (!response.ok || payload?.success === false) {
1264 throw new Error(payload?.message || __('Failed to record payment.', 'yatra'));
1265 }
1266
1267 return payload;
1268 } catch (error) {
1269 console.error('Failed to notify backend about payment success:', error);
1270 throw error;
1271 }
1272 }
1273 }
1274
1275 document.addEventListener('DOMContentLoaded', () => {
1276 if (document.getElementById('yatra-booking-form')) {
1277 const apiUrl = window.yatraBookingData?.apiUrl || '/wp-json/yatra/v1';
1278 const nonce = window.yatraBookingData?.nonce || '';
1279 new YatraStripe(apiUrl, nonce);
1280 }
1281 });
1282