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
← All changes | assets/js/stripe.js +75 -21 3.0.4 → 3.0.16 View file →
@@ -235,19 +235,27 @@
235 235 const stripePreselected = !!stripeOption.checked;
236 236 container.style.display = stripePreselected ? 'block' : 'none';
237 237
238 238 this.methodSwitcher = this.createMethodSwitcher();
239 - container.appendChild(this.methodSwitcher);
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 + }
240 247
241 248 if (this.supportsPaymentRequest) {
242 249 const requestWrapper = document.createElement('div');
243 250 requestWrapper.className = 'yatra-stripe-payment-request-wrapper';
244 - const walletNote = window.yatraTranslations?.stripeWalletNote || 'Pay instantly with Apple Pay or Google Pay.';
251 + const walletNote = window.yatraTranslations?.stripeWalletNote || __('Pay instantly with Apple Pay or Google Pay.', 'yatra');
252 + const walletTitle = __('Apple Pay & Google Pay', 'yatra');
245 253 requestWrapper.innerHTML = `
246 254 <div class="yatra-wallet-header">
247 255 <div>
248 256 <p class="yatra-wallet-eyebrow">${__('One-tap checkout', 'yatra')}</p>
249 - <h4 class="yatra-wallet-title">Apple Pay & Google Pay</h4>
257 + <h4 class="yatra-wallet-title">${walletTitle}</h4>
250 258 </div>
251 259 <div class="yatra-wallet-badges">
252 260 <span class="wallet-badge wallet-badge--apple"> Pay</span>
253 261 <span class="wallet-badge wallet-badge--google">G Pay</span>
@@ -321,9 +329,9 @@
321 329 initializeStripe() {
322 330 const publishableKey = window.yatraBookingData?.stripe?.publishableKey
323 331 || window.yatraBookingData?.stripe?.publishable_key;
324 332 if (!publishableKey) {
325 - this.displayError('Stripe publishable key is missing. Please contact support.');
333 + this.displayError(__('Stripe publishable key is missing. Please contact support.', 'yatra'));
326 334 return;
327 335 }
328 336
329 337 if (typeof Stripe === 'undefined') {
@@ -330,9 +338,9 @@
330 338 const script = document.createElement('script');
331 339 script.src = 'https://js.stripe.com/v3/';
332 340 script.onload = () => this.setupStripeElements(publishableKey);
333 341 script.onerror = () => {
334 - this.displayError('Failed to load Stripe.js. Please try again or choose another payment method.');
342 + this.displayError(__('Failed to load Stripe.js. Please try again or choose another payment method.', 'yatra'));
335 343 };
336 344 document.head.appendChild(script);
337 345 } else {
338 346 this.setupStripeElements(publishableKey);
@@ -381,9 +389,9 @@
381 389 stripeContainer.style.display = 'block';
382 390 }
383 391 } catch (error) {
384 392 console.error('Error initializing Stripe:', error);
385 - this.displayError('Failed to initialize payment processor. Please try again or use another payment method.');
393 + this.displayError(__('Failed to initialize payment processor. Please try again or use another payment method.', 'yatra'));
386 394 }
387 395 }
388 396
389 397 async handleGatewayIntercept(event) {
@@ -399,21 +407,21 @@
399 407 return false;
400 408 }
401 409
402 410 if (!this.supportsCard) {
403 - this.displayError('Please use Apple Pay or Google Pay above to complete your payment.');
411 + this.displayError(__('Please use Apple Pay or Google Pay above to complete your payment.', 'yatra'));
404 412 return false;
405 413 }
406 414
407 415 const bookingData = event?.detail?.bookingData || this.collectBookingData();
408 416 if (!bookingData) {
409 - this.displayError('Please complete the booking form before continuing.');
417 + this.displayError(__('Please complete the booking form before continuing.', 'yatra'));
410 418 return false;
411 419 }
412 420
413 421 if (!this.stripe || !this.cardElement) {
414 422 this.initializeStripe();
415 - this.displayError('Stripe is still loading. Please wait a moment and try again.');
423 + this.displayError(__('Stripe is still loading. Please wait a moment and try again.', 'yatra'));
416 424 return false;
417 425 }
418 426
419 427 if (!this.originalButtonHtml && event?.detail?.originalBtnHtml) {
@@ -450,13 +458,13 @@
450 458 currency: bookingInfo.currency || bookingData.currency || 'USD'
451 459 });
452 460 window.location.href = this.buildConfirmationUrlFromBookingInfo(bookingInfo);
453 461 } else {
454 - throw new Error('Payment processing was not completed. Please check your payment details and try again.');
462 + throw new Error(__('Payment processing was not completed. Please check your payment details and try again.', 'yatra'));
455 463 }
456 464 } catch (error) {
457 465 console.error('Stripe payment error:', error);
458 - this.displayError(error.message || 'Payment failed. Please try again or use another payment method.');
466 + this.displayError(error.message || __('Payment failed. Please try again or use another payment method.', 'yatra'));
459 467 this.setLoadingState(false);
460 468 }
461 469
462 470 return false;
@@ -636,8 +644,13 @@
636 644 icon: this.getMethodIconMarkup('apple_pay')
637 645 });
638 646 }
639 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 +
640 653 const buttons = document.createElement('div');
641 654 buttons.className = 'yatra-method-switcher__buttons';
642 655
643 656 availableButtons.forEach((method) => {
@@ -847,9 +860,9 @@
847 860 async handlePaymentRequestPayment(event) {
848 861 try {
849 862 const bookingData = this.collectBookingData();
850 863 if (!bookingData) {
851 - throw new Error('Please complete the booking form before using Apple Pay or Google Pay.');
864 + throw new Error(__('Please complete the booking form before using Apple Pay or Google Pay.', 'yatra'));
852 865 }
853 866
854 867 this.setLoadingState(true);
855 868 const paymentContext = await this.prepareStripePayment(bookingData, this.paymentForm);
@@ -886,20 +899,58 @@
886 899 console.error('Stripe payment request error:', error);
887 900 if (event && typeof event.complete === 'function') {
888 901 event.complete('fail');
889 902 }
890 - this.displayError(error.message || 'Payment failed. Please try again or use another payment method.');
903 + this.displayError(error.message || __('Payment failed. Please try again or use another payment method.', 'yatra'));
891 904 this.setLoadingState(false);
892 905 }
893 906 }
894 907
895 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 +
896 946 // Always call the same endpoint - server decides based on session type
897 947 const bookingResponse = await fetch(`${this.apiUrl}/booking/create`, {
898 948 method: 'POST',
899 949 headers: {
900 950 'Content-Type': 'application/json',
901 - 'X-WP-Nonce': this.nonce
951 + 'X-WP-Nonce': this.nonce,
952 + 'X-Yatra-Booking-Nonce': bookingNonce
902 953 },
903 954 body: JSON.stringify(bookingData)
904 955 });
905 956
@@ -905,9 +956,9 @@
905 956
906 957 const bookingResult = await bookingResponse.json();
907 958
908 959 if (!bookingResult.success) {
909 - throw new Error(bookingResult.message || 'Failed to process request. Please try again.');
960 + throw new Error(bookingResult.message || __('Failed to process request. Please try again.', 'yatra'));
910 961 }
911 962
912 963 const bookingInfo = bookingResult.data;
913 964 const effectiveAmount = this.getEffectiveAmount(bookingData, bookingInfo);
@@ -919,9 +970,9 @@
919 970 .filter(Boolean)
920 971 .join(' ') || bookingData.full_name;
921 972
922 973 if (!customerEmail) {
923 - throw new Error('Email address is required.');
974 + throw new Error(__('Email address is required.', 'yatra'));
924 975 }
925 976
926 977 const billingDetails = this.getBillingDetails(bookingData, formElement);
927 978
@@ -939,9 +990,9 @@
939 990 .filter(Boolean)
940 991 .join(' ') || bookingData.full_name;
941 992
942 993 if (!customerEmail) {
943 - throw new Error('Email address is required.');
994 + throw new Error(__('Email address is required.', 'yatra'));
944 995 }
945 996
946 997 const paymentIntentResponse = await fetch(`${this.apiUrl}/payment/create-intent`, {
947 998 method: 'POST',
@@ -963,14 +1014,14 @@
963 1014 });
964 1015
965 1016 const intentPayload = await paymentIntentResponse.json();
966 1017 if (!paymentIntentResponse.ok) {
967 - throw new Error(intentPayload?.message || 'Unable to process payment. Please try again.');
1018 + throw new Error(intentPayload?.message || __('Unable to process payment. Please try again.', 'yatra'));
968 1019 }
969 1020 const clientSecret = intentPayload.client_secret;
970 1021
971 1022 if (!clientSecret) {
972 - throw new Error(intentPayload?.error?.message || 'Unable to process payment. Please try again.');
1023 + throw new Error(intentPayload?.error?.message || __('Unable to process payment. Please try again.', 'yatra'));
973 1024 }
974 1025
975 1026 const billingDetails = this.getBillingDetails(bookingData, formElement);
976 1027
@@ -1107,9 +1158,12 @@
1107 1158 }
1108 1159
1109 1160 if (isLoading) {
1110 1161 this.submitButton.disabled = true;
1111 - this.submitButton.innerHTML = '<span class="yatra-spinner"></span> Processing Payment...';
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');
1112 1166 } else {
1113 1167 this.submitButton.disabled = false;
1114 1168 this.submitButton.innerHTML = this.originalButtonHtml;
1115 1169 }
@@ -1228,9 +1282,9 @@
1228 1282 });
1229 1283
1230 1284 const payload = await response.json();
1231 1285 if (!response.ok || payload?.success === false) {
1232 - throw new Error(payload?.message || 'Failed to record payment.');
1286 + throw new Error(payload?.message || __('Failed to record payment.', 'yatra'));
1233 1287 }
1234 1288
1235 1289 return payload;
1236 1290 } catch (error) {