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 +92 -22 3.0.2.9 → 3.0.16 View file →
@@ -163,12 +163,28 @@
163 163 const btn = document.getElementById('stripe-submit-btn');
164 164 btn.disabled = true;
165 165 btn.textContent = __('Processing...', 'yatra');
166 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 +
167 183 const { error } = await this.stripe.confirmPayment({
168 184 elements,
169 185 confirmParams: {
170 - return_url: data.confirmation_url + '?stripe=success',
186 + return_url: confirmReturnUrl,
171 187 },
172 188 });
173 189
174 190 if (error) {
@@ -219,19 +235,27 @@
219 235 const stripePreselected = !!stripeOption.checked;
220 236 container.style.display = stripePreselected ? 'block' : 'none';
221 237
222 238 this.methodSwitcher = this.createMethodSwitcher();
223 - 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 + }
224 247
225 248 if (this.supportsPaymentRequest) {
226 249 const requestWrapper = document.createElement('div');
227 250 requestWrapper.className = 'yatra-stripe-payment-request-wrapper';
228 - 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');
229 253 requestWrapper.innerHTML = `
230 254 <div class="yatra-wallet-header">
231 255 <div>
232 256 <p class="yatra-wallet-eyebrow">${__('One-tap checkout', 'yatra')}</p>
233 - <h4 class="yatra-wallet-title">Apple Pay & Google Pay</h4>
257 + <h4 class="yatra-wallet-title">${walletTitle}</h4>
234 258 </div>
235 259 <div class="yatra-wallet-badges">
236 260 <span class="wallet-badge wallet-badge--apple"> Pay</span>
237 261 <span class="wallet-badge wallet-badge--google">G Pay</span>
@@ -305,9 +329,9 @@
305 329 initializeStripe() {
306 330 const publishableKey = window.yatraBookingData?.stripe?.publishableKey
307 331 || window.yatraBookingData?.stripe?.publishable_key;
308 332 if (!publishableKey) {
309 - this.displayError('Stripe publishable key is missing. Please contact support.');
333 + this.displayError(__('Stripe publishable key is missing. Please contact support.', 'yatra'));
310 334 return;
311 335 }
312 336
313 337 if (typeof Stripe === 'undefined') {
@@ -314,9 +338,9 @@
314 338 const script = document.createElement('script');
315 339 script.src = 'https://js.stripe.com/v3/';
316 340 script.onload = () => this.setupStripeElements(publishableKey);
317 341 script.onerror = () => {
318 - 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'));
319 343 };
320 344 document.head.appendChild(script);
321 345 } else {
322 346 this.setupStripeElements(publishableKey);
@@ -365,9 +389,9 @@
365 389 stripeContainer.style.display = 'block';
366 390 }
367 391 } catch (error) {
368 392 console.error('Error initializing Stripe:', error);
369 - 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'));
370 394 }
371 395 }
372 396
373 397 async handleGatewayIntercept(event) {
@@ -383,21 +407,21 @@
383 407 return false;
384 408 }
385 409
386 410 if (!this.supportsCard) {
387 - 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'));
388 412 return false;
389 413 }
390 414
391 415 const bookingData = event?.detail?.bookingData || this.collectBookingData();
392 416 if (!bookingData) {
393 - this.displayError('Please complete the booking form before continuing.');
417 + this.displayError(__('Please complete the booking form before continuing.', 'yatra'));
394 418 return false;
395 419 }
396 420
397 421 if (!this.stripe || !this.cardElement) {
398 422 this.initializeStripe();
399 - 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'));
400 424 return false;
401 425 }
402 426
403 427 if (!this.originalButtonHtml && event?.detail?.originalBtnHtml) {
@@ -434,13 +458,13 @@
434 458 currency: bookingInfo.currency || bookingData.currency || 'USD'
435 459 });
436 460 window.location.href = this.buildConfirmationUrlFromBookingInfo(bookingInfo);
437 461 } else {
438 - 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'));
439 463 }
440 464 } catch (error) {
441 465 console.error('Stripe payment error:', error);
442 - 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'));
443 467 this.setLoadingState(false);
444 468 }
445 469
446 470 return false;
@@ -620,8 +644,13 @@
620 644 icon: this.getMethodIconMarkup('apple_pay')
621 645 });
622 646 }
623 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 +
624 653 const buttons = document.createElement('div');
625 654 buttons.className = 'yatra-method-switcher__buttons';
626 655
627 656 availableButtons.forEach((method) => {
@@ -831,9 +860,9 @@
831 860 async handlePaymentRequestPayment(event) {
832 861 try {
833 862 const bookingData = this.collectBookingData();
834 863 if (!bookingData) {
835 - 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'));
836 865 }
837 866
838 867 this.setLoadingState(true);
839 868 const paymentContext = await this.prepareStripePayment(bookingData, this.paymentForm);
@@ -870,20 +899,58 @@
870 899 console.error('Stripe payment request error:', error);
871 900 if (event && typeof event.complete === 'function') {
872 901 event.complete('fail');
873 902 }
874 - 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'));
875 904 this.setLoadingState(false);
876 905 }
877 906 }
878 907
879 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 +
880 946 // Always call the same endpoint - server decides based on session type
881 947 const bookingResponse = await fetch(`${this.apiUrl}/booking/create`, {
882 948 method: 'POST',
883 949 headers: {
884 950 'Content-Type': 'application/json',
885 - 'X-WP-Nonce': this.nonce
951 + 'X-WP-Nonce': this.nonce,
952 + 'X-Yatra-Booking-Nonce': bookingNonce
886 953 },
887 954 body: JSON.stringify(bookingData)
888 955 });
889 956
@@ -889,9 +956,9 @@
889 956
890 957 const bookingResult = await bookingResponse.json();
891 958
892 959 if (!bookingResult.success) {
893 - 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'));
894 961 }
895 962
896 963 const bookingInfo = bookingResult.data;
897 964 const effectiveAmount = this.getEffectiveAmount(bookingData, bookingInfo);
@@ -903,9 +970,9 @@
903 970 .filter(Boolean)
904 971 .join(' ') || bookingData.full_name;
905 972
906 973 if (!customerEmail) {
907 - throw new Error('Email address is required.');
974 + throw new Error(__('Email address is required.', 'yatra'));
908 975 }
909 976
910 977 const billingDetails = this.getBillingDetails(bookingData, formElement);
911 978
@@ -923,9 +990,9 @@
923 990 .filter(Boolean)
924 991 .join(' ') || bookingData.full_name;
925 992
926 993 if (!customerEmail) {
927 - throw new Error('Email address is required.');
994 + throw new Error(__('Email address is required.', 'yatra'));
928 995 }
929 996
930 997 const paymentIntentResponse = await fetch(`${this.apiUrl}/payment/create-intent`, {
931 998 method: 'POST',
@@ -947,14 +1014,14 @@
947 1014 });
948 1015
949 1016 const intentPayload = await paymentIntentResponse.json();
950 1017 if (!paymentIntentResponse.ok) {
951 - 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'));
952 1019 }
953 1020 const clientSecret = intentPayload.client_secret;
954 1021
955 1022 if (!clientSecret) {
956 - 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'));
957 1024 }
958 1025
959 1026 const billingDetails = this.getBillingDetails(bookingData, formElement);
960 1027
@@ -1091,9 +1158,12 @@
1091 1158 }
1092 1159
1093 1160 if (isLoading) {
1094 1161 this.submitButton.disabled = true;
1095 - 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');
1096 1166 } else {
1097 1167 this.submitButton.disabled = false;
1098 1168 this.submitButton.innerHTML = this.originalButtonHtml;
1099 1169 }
@@ -1212,9 +1282,9 @@
1212 1282 });
1213 1283
1214 1284 const payload = await response.json();
1215 1285 if (!response.ok || payload?.success === false) {
1216 - throw new Error(payload?.message || 'Failed to record payment.');
1286 + throw new Error(payload?.message || __('Failed to record payment.', 'yatra'));
1217 1287 }
1218 1288
1219 1289 return payload;
1220 1290 } catch (error) {