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

1,304 lines 52.9 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
925 // reCAPTCHA v3: this gateway posts to /booking/create itself, so it must
926 // attach its own token — the shared submit path in booking.js never runs
927 // for an intercepted gateway submit. Without this the server receives an
928 // empty token and rejects the booking with "reCAPTCHA verification
929 // failed", which no score threshold can get past.
930 //
931 // A fresh token every attempt: v3 tokens are single-use and expire after
932 // ~2 minutes, so a retry after a declined card must not reuse the old one.
933 try {
934 const yatraRc = (typeof window !== 'undefined') ? window.yatraRecaptcha : null;
935 if (yatraRc && typeof yatraRc.protects === 'function' && yatraRc.protects('booking')
936 && typeof yatraRc.execute === 'function') {
937 const recaptchaToken = await yatraRc.execute('booking');
938 if (recaptchaToken) {
939 bookingData.recaptcha_token = recaptchaToken;
940 }
941 }
942 } catch (e) {
943 // Never block checkout on the helper itself; the server still decides.
944 }
945
946 // Always call the same endpoint - server decides based on session type
947 const bookingResponse = await fetch(`${this.apiUrl}/booking/create`, {
948 method: 'POST',
949 headers: {
950 'Content-Type': 'application/json',
951 'X-WP-Nonce': this.nonce,
952 'X-Yatra-Booking-Nonce': bookingNonce
953 },
954 body: JSON.stringify(bookingData)
955 });
956
957 const bookingResult = await bookingResponse.json();
958
959 if (!bookingResult.success) {
960 throw new Error(bookingResult.message || __('Failed to process request. Please try again.', 'yatra'));
961 }
962
963 const bookingInfo = bookingResult.data;
964 const effectiveAmount = this.getEffectiveAmount(bookingData, bookingInfo);
965
966 if (bookingInfo.client_secret) {
967 const customerEmail = bookingInfo.customer_email || bookingData.contact_email || bookingData.email;
968 const customerName = bookingInfo.customer_name ||
969 [bookingData.contact_first_name, bookingData.contact_last_name]
970 .filter(Boolean)
971 .join(' ') || bookingData.full_name;
972
973 if (!customerEmail) {
974 throw new Error(__('Email address is required.', 'yatra'));
975 }
976
977 const billingDetails = this.getBillingDetails(bookingData, formElement);
978
979 return {
980 bookingInfo,
981 clientSecret: bookingInfo.client_secret,
982 billingDetails,
983 effectiveAmount,
984 };
985 }
986
987 const customerEmail = bookingInfo.customer_email || bookingData.contact_email || bookingData.email;
988 const customerName = bookingInfo.customer_name ||
989 [bookingData.contact_first_name, bookingData.contact_last_name]
990 .filter(Boolean)
991 .join(' ') || bookingData.full_name;
992
993 if (!customerEmail) {
994 throw new Error(__('Email address is required.', 'yatra'));
995 }
996
997 const paymentIntentResponse = await fetch(`${this.apiUrl}/payment/create-intent`, {
998 method: 'POST',
999 headers: {
1000 'Content-Type': 'application/json',
1001 'X-WP-Nonce': this.nonce
1002 },
1003 body: JSON.stringify({
1004 gateway: 'stripe',
1005 booking_id: bookingInfo.booking_id,
1006 amount: effectiveAmount,
1007 currency: bookingInfo.currency,
1008 trip_id: bookingInfo.trip_id || bookingData.trip_id || null,
1009 trip_date: bookingInfo.trip_date || bookingData.travel_date || '',
1010 customer_email: customerEmail,
1011 customer_name: customerName,
1012 return_url: this.buildReturnUrl(bookingInfo)
1013 })
1014 });
1015
1016 const intentPayload = await paymentIntentResponse.json();
1017 if (!paymentIntentResponse.ok) {
1018 throw new Error(intentPayload?.message || __('Unable to process payment. Please try again.', 'yatra'));
1019 }
1020 const clientSecret = intentPayload.client_secret;
1021
1022 if (!clientSecret) {
1023 throw new Error(intentPayload?.error?.message || __('Unable to process payment. Please try again.', 'yatra'));
1024 }
1025
1026 const billingDetails = this.getBillingDetails(bookingData, formElement);
1027
1028 return {
1029 bookingInfo,
1030 clientSecret,
1031 billingDetails,
1032 effectiveAmount,
1033 };
1034 }
1035
1036 getBillingDetails(bookingData, formElement) {
1037 const getField = (keys) => {
1038 const candidates = Array.isArray(keys) ? keys : [keys];
1039 for (const key of candidates) {
1040 if (bookingData && bookingData[key]) {
1041 return bookingData[key];
1042 }
1043 if (formElement) {
1044 const input = formElement.querySelector(`[name="${key}"]`);
1045 if (input && input.value) {
1046 return input.value;
1047 }
1048 }
1049 }
1050 return '';
1051 };
1052
1053 const firstName = getField(['billing_first_name', 'contact_first_name', 'first_name']);
1054 const lastName = getField(['billing_last_name', 'contact_last_name', 'last_name']);
1055 const email = getField(['billing_email', 'contact_email', 'email']);
1056 const phone = getField(['billing_phone', 'contact_phone', 'phone']);
1057 const line1 = getField(['billing_address_1', 'contact_address']);
1058 const city = getField(['billing_city', 'contact_city']);
1059 const country = getField(['billing_country', 'contact_country']);
1060 const postalCode = getField(['billing_postcode', 'contact_postcode', 'contact_zip']);
1061
1062 const billingDetails = {};
1063 const name = `${firstName} ${lastName}`.trim();
1064 if (name) {
1065 billingDetails.name = name;
1066 }
1067 if (email) {
1068 billingDetails.email = email;
1069 }
1070 if (phone) {
1071 billingDetails.phone = phone;
1072 }
1073
1074 const address = {};
1075 if (line1) {
1076 address.line1 = line1;
1077 }
1078 if (city) {
1079 address.city = city;
1080 }
1081 if (country) {
1082 const normalizedCountry = country.trim().toUpperCase();
1083 if (normalizedCountry.length === 2) {
1084 address.country = normalizedCountry;
1085 }
1086 }
1087 if (postalCode) {
1088 address.postal_code = postalCode;
1089 }
1090
1091 if (Object.keys(address).length > 0) {
1092 billingDetails.address = address;
1093 }
1094
1095 return billingDetails;
1096 }
1097
1098 collectBookingData() {
1099 if (!this.paymentForm) {
1100 return null;
1101 }
1102
1103 const formData = new FormData(this.paymentForm);
1104 const bookingData = {};
1105
1106 for (const [key, value] of formData.entries()) {
1107 const matches = key.match(/^([^\[]+)(?:\[([^\]]*)\])?(?:\[([^\]]*)\])?$/);
1108
1109 if (matches) {
1110 const [, base, index, field] = matches;
1111
1112 if (index !== undefined && field !== undefined) {
1113 bookingData[base] = bookingData[base] || {};
1114 bookingData[base][index] = bookingData[base][index] || {};
1115 bookingData[base][index][field] = value;
1116 } else if (index !== undefined) {
1117 bookingData[base] = bookingData[base] || [];
1118 bookingData[base][index] = value;
1119 } else {
1120 bookingData[key] = value;
1121 }
1122 } else {
1123 bookingData[key] = value;
1124 }
1125 }
1126
1127 bookingData.accept_terms = this.paymentForm.querySelector('input[name="accept_terms"]')?.checked || false;
1128 bookingData.accept_privacy = this.paymentForm.querySelector('input[name="accept_privacy"]')?.checked || false;
1129 bookingData.subscribe_newsletter = this.paymentForm.querySelector('input[name="subscribe_newsletter"]')?.checked || false;
1130 bookingData.payment_due = parseFloat(this.paymentForm.getAttribute('data-payment-due'))
1131 || window.yatraBookingData?.paymentDue
1132 || null;
1133 bookingData.currency = window.yatraBookingData?.currency
1134 || bookingData.currency
1135 || this.paymentForm.querySelector('input[name="currency"]')?.value
1136 || 'USD';
1137 bookingData.payment_gateway = 'stripe';
1138
1139 return bookingData;
1140 }
1141
1142 displayError(message) {
1143 const errorElement = document.getElementById('yatra-stripe-card-errors');
1144 if (errorElement) {
1145 errorElement.textContent = message || '';
1146 errorElement.style.display = message ? 'block' : 'none';
1147 if (message) {
1148 errorElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
1149 }
1150 } else if (message) {
1151 console.error(message);
1152 }
1153 }
1154
1155 setLoadingState(isLoading) {
1156 if (!this.submitButton) {
1157 return;
1158 }
1159
1160 if (isLoading) {
1161 this.submitButton.disabled = true;
1162 // Spinner markup is fixed; only the user-visible text is
1163 // wrapped so it ends up in the .pot with a clean msgid.
1164 this.submitButton.innerHTML =
1165 '<span class="yatra-spinner"></span> ' + __('Processing Payment...', 'yatra');
1166 } else {
1167 this.submitButton.disabled = false;
1168 this.submitButton.innerHTML = this.originalButtonHtml;
1169 }
1170 }
1171
1172 getEffectiveAmount(bookingData, bookingInfo) {
1173 // After /booking/create, server-calculated charge (deposit / partial / full) is authoritative.
1174 // `amount` = charge for this checkout (matches gateway). Never fall back to `total_amount` (full order).
1175 const serverFirst = [
1176 parseFloat(bookingInfo?.amount),
1177 parseFloat(bookingInfo?.amount_due)
1178 ];
1179 for (let i = 0; i < serverFirst.length; i++) {
1180 const v = serverFirst[i];
1181 if (!isNaN(v) && v > 0) {
1182 return v;
1183 }
1184 }
1185
1186 const summaryDue = parseFloat(window.yatraBookingSummary?.amountDue);
1187 if (!isNaN(summaryDue) && summaryDue > 0) {
1188 return summaryDue;
1189 }
1190
1191 const candidates = [
1192 parseFloat(bookingData?.amount_due),
1193 parseFloat(bookingData?.payment_due),
1194 parseFloat(this.paymentForm?.dataset.paymentDue),
1195 parseFloat(window.yatraBookingData?.paymentDue)
1196 ].map((val) => (isNaN(val) ? 0 : val));
1197
1198 const picked = candidates.find((val) => val > 0) ?? 0;
1199 return picked > 0 ? picked : 0;
1200 }
1201
1202 getCurrentPaymentDue() {
1203 const summaryDue = parseFloat(window.yatraBookingSummary?.amountDue);
1204 if (!isNaN(summaryDue) && summaryDue > 0) {
1205 return summaryDue;
1206 }
1207
1208 const candidates = [
1209 parseFloat(this.paymentForm?.dataset.paymentDue),
1210 parseFloat(window.yatraBookingData?.paymentDue)
1211 ].map((val) => (isNaN(val) ? 0 : val));
1212
1213 return candidates.find((val) => val > 0) ?? 0;
1214 }
1215
1216 getPaymentRequestTotal() {
1217 return {
1218 label: this.tripTitle,
1219 amount: this.getAmountInCents(this.getCurrentPaymentDue())
1220 };
1221 }
1222
1223 getAmountInCents(amount) {
1224 const value = isNaN(amount) ? 0 : amount;
1225 return Math.max(50, Math.round(value * 100));
1226 }
1227
1228 /**
1229 * Pageless confirmation URLs use the same base as Settings → Default Booking URL Base
1230 * (e.g. book → /book/confirmation/{reference}/). Never hard-code /booking-confirmation/.
1231 */
1232 buildConfirmationUrlFromBookingInfo(bookingInfo) {
1233 if (bookingInfo.redirect_url) {
1234 return bookingInfo.redirect_url;
1235 }
1236 const d = window.yatraBookingData || {};
1237 const site = (d.siteUrl || window.location.origin || '').replace(/\/$/, '');
1238 const ref = String(bookingInfo.reference || '').trim();
1239 const id = bookingInfo.booking_id;
1240 const segment = ref || (id != null && id !== '' ? String(id) : '');
1241
1242 if (!segment) {
1243 return `${site}/`;
1244 }
1245
1246 if (d.permalinkStructure === 'plain') {
1247 return `${site}/?yatra_booking_confirmation=${encodeURIComponent(segment)}`;
1248 }
1249
1250 const bb = String(d.bookingBase || 'book').replace(/^\/|\/$/g, '');
1251 return `${site}/${bb}/confirmation/${encodeURIComponent(segment)}/`;
1252 }
1253
1254 buildReturnUrl(bookingInfo) {
1255 const base = this.buildConfirmationUrlFromBookingInfo(bookingInfo);
1256 try {
1257 const url = new URL(base, window.location.origin);
1258 url.searchParams.set('stripe_return', '1');
1259 return url.toString();
1260 } catch (e) {
1261 const join = base.includes('?') ? '&' : '?';
1262 return `${base}${join}stripe_return=1`;
1263 }
1264 }
1265
1266 async notifyPaymentSuccess({ bookingId, transactionId, saveCard, amount, currency }) {
1267 try {
1268 const response = await fetch(`${this.apiUrl}/payment/confirm`, {
1269 method: 'POST',
1270 headers: {
1271 'Content-Type': 'application/json',
1272 'X-WP-Nonce': this.nonce,
1273 },
1274 body: JSON.stringify({
1275 gateway: 'stripe',
1276 booking_id: bookingId,
1277 transaction_id: transactionId,
1278 save_card: !!saveCard,
1279 amount,
1280 currency,
1281 }),
1282 });
1283
1284 const payload = await response.json();
1285 if (!response.ok || payload?.success === false) {
1286 throw new Error(payload?.message || __('Failed to record payment.', 'yatra'));
1287 }
1288
1289 return payload;
1290 } catch (error) {
1291 console.error('Failed to notify backend about payment success:', error);
1292 throw error;
1293 }
1294 }
1295 }
1296
1297 document.addEventListener('DOMContentLoaded', () => {
1298 if (document.getElementById('yatra-booking-form')) {
1299 const apiUrl = window.yatraBookingData?.apiUrl || '/wp-json/yatra/v1';
1300 const nonce = window.yatraBookingData?.nonce || '';
1301 new YatraStripe(apiUrl, nonce);
1302 }
1303 });
1304