| @@ -235,9 +235,16 @@ | ||
| 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'; |
| @@ -637,8 +644,13 @@ | ||
| 637 | 644 | icon: this.getMethodIconMarkup('apple_pay') |
| 638 | 645 | }); |
| 639 | 646 | } |
| 640 | 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 | + | |
| 641 | 653 | const buttons = document.createElement('div'); |
| 642 | 654 | buttons.className = 'yatra-method-switcher__buttons'; |
| 643 | 655 | |
| 644 | 656 | availableButtons.forEach((method) => { |
| @@ -907,8 +919,30 @@ | ||
| 907 | 919 | || (typeof document !== 'undefined' |
| 908 | 920 | ? (document.querySelector('input[name="yatra_booking_nonce"]') || {}).value |
| 909 | 921 | : '') |
| 910 | 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 | + } | |
| 911 | 945 | |
| 912 | 946 | // Always call the same endpoint - server decides based on session type |
| 913 | 947 | const bookingResponse = await fetch(`${this.apiUrl}/booking/create`, { |
| 914 | 948 | method: 'POST', |