| @@ -30,8 +30,50 @@ | ||
| 30 | 30 | add_action('wp_enqueue_scripts', [$this, 'enqueueAssets']); |
| 31 | 31 | } |
| 32 | 32 | |
| 33 | 33 | /** |
| 34 | + * Resolve the effective trip-listing card layout for one listing. | |
| 35 | + * | |
| 36 | + * The site-wide Design setting (`frontend_listing_card_layout`) is the | |
| 37 | + * default; a shortcode/block may pass a per-instance override. Anything that | |
| 38 | + * is not a concrete layout (empty, "inherit", or an unknown value) falls | |
| 39 | + * back to the global setting, so existing shortcodes/blocks are unchanged. | |
| 40 | + * | |
| 41 | + * @param string $override Per-instance override (card_layout / cardLayout). | |
| 42 | + * @return string One of: standard | compact_mobile | compact_all. | |
| 43 | + */ | |
| 44 | + public static function resolveListingLayout(string $override = ''): string | |
| 45 | + { | |
| 46 | + $override = strtolower(trim($override)); | |
| 47 | + if (in_array($override, ['standard', 'compact_mobile', 'compact_all'], true)) { | |
| 48 | + return $override; | |
| 49 | + } | |
| 50 | + | |
| 51 | + $global = \Yatra\Services\SettingsService::getString('frontend_listing_card_layout', 'standard'); | |
| 52 | + return in_array($global, ['standard', 'compact_mobile', 'compact_all'], true) ? $global : 'standard'; | |
| 53 | + } | |
| 54 | + | |
| 55 | + /** | |
| 56 | + * Map a resolved layout to the container CSS class(es) the listing CSS keys | |
| 57 | + * off. Placed on the listing container (archive .yatra-listing-page, or the | |
| 58 | + * shortcode/block wrapper) so each listing can carry its own layout and an | |
| 59 | + * override never leaks into other listings on the same page. | |
| 60 | + * | |
| 61 | + * @param string $layout standard | compact_mobile | compact_all | |
| 62 | + * @return string Space-separated class list ('' for standard). | |
| 63 | + */ | |
| 64 | + public static function listingLayoutClasses(string $layout): string | |
| 65 | + { | |
| 66 | + if ($layout === 'compact_mobile') { | |
| 67 | + return 'yatra-listing-compact'; | |
| 68 | + } | |
| 69 | + if ($layout === 'compact_all') { | |
| 70 | + return 'yatra-listing-compact yatra-listing-compact--all'; | |
| 71 | + } | |
| 72 | + return ''; | |
| 73 | + } | |
| 74 | + | |
| 75 | + /** | |
| 34 | 76 | * Register Font Awesome (optional) and common.css so block editor + shortcode styles can |
| 35 | 77 | * depend on `yatra-common` (shared @keyframes: yatra-spin, yatra-shimmer, etc.). |
| 36 | 78 | */ |
| 37 | 79 | /** |
| @@ -286,14 +328,22 @@ | ||
| 286 | 328 | 'yatra', |
| 287 | 329 | YATRA_PLUGIN_PATH . 'i18n/languages' |
| 288 | 330 | ); |
| 289 | 331 | } |
| 332 | + // Wishlist "Login" should send guests to the configured | |
| 333 | + // My Account page (Settings → Permalink slug), not the raw | |
| 334 | + // wp-login.php screen. Fall back to wp_login_url() only when | |
| 335 | + // no account base is configured so the button never dead-ends. | |
| 336 | + $yatraAccountBase = \Yatra\Services\SettingsService::getAccountBase(); | |
| 337 | + $yatraAccountLoginUrl = $yatraAccountBase !== '' | |
| 338 | + ? home_url('/' . trim($yatraAccountBase, '/') . '/') | |
| 339 | + : wp_login_url(); | |
| 290 | 340 | wp_localize_script('yatra-listing-wishlist', 'yatraWishlistConfig', [ |
| 291 | 341 | 'enabled' => true, |
| 292 | 342 | 'restUrl' => rest_url('yatra/v1'), |
| 293 | 343 | 'nonce' => wp_create_nonce('wp_rest'), |
| 294 | 344 | 'isLoggedIn' => is_user_logged_in(), |
| 295 | - 'loginUrl' => wp_login_url(), | |
| 345 | + 'loginUrl' => $yatraAccountLoginUrl, | |
| 296 | 346 | 'i18n' => [ |
| 297 | 347 | 'loginRequired' => __('Login Required', 'yatra'), |
| 298 | 348 | 'loginPrompt' => __('Please login to save trips to your wishlist.', 'yatra'), |
| 299 | 349 | 'login' => __('Login', 'yatra'), |
| @@ -386,8 +436,13 @@ | ||
| 386 | 436 | ); |
| 387 | 437 | } |
| 388 | 438 | } |
| 389 | 439 | |
| 440 | + // International phone-number widget (country flag + dial code) used by | |
| 441 | + // the booking form's tel fields. | |
| 442 | + $this->enqueuePhoneInputAssets(); | |
| 443 | + $this->enqueueCountrySelectAssets(); | |
| 444 | + | |
| 390 | 445 | // Mobile sticky-sidebar + flatpickr init for the single-trip page. Lives in a |
| 391 | 446 | // dedicated file rather than as inline <script> in the partial because |
| 392 | 447 | // WordPress core's `convert_chars` filter (hooked to the_content) rewrites the |
| 393 | 448 | // `&&` operators inside inline scripts as `&&` — JS parsers don't |
| @@ -437,8 +492,16 @@ | ||
| 437 | 492 | 'tripId' => $trip_id, |
| 438 | 493 | 'tripSlug' => $trip_slug, |
| 439 | 494 | 'wishlistEnabled' => \Yatra\Services\SettingsService::wishlistEnabled(), |
| 440 | 495 | 'isLoggedIn' => is_user_logged_in(), |
| 496 | + // Wishlist "Login" (guest) → the configured My Account page | |
| 497 | + // (Settings → Permalink slug), NOT wp-login.php. Without this key | |
| 498 | + // trip.js falls back to a hardcoded '/wp-login.php'. Falls back to | |
| 499 | + // wp_login_url() only when no account base slug is configured. | |
| 500 | + 'loginUrl' => (function () { | |
| 501 | + $base = \Yatra\Services\SettingsService::getAccountBase(); | |
| 502 | + return $base !== '' ? home_url('/' . trim($base, '/') . '/') : wp_login_url(); | |
| 503 | + })(), | |
| 441 | 504 | // Regional settings |
| 442 | 505 | 'timezone' => \Yatra\Services\SettingsService::getString('timezone', 'UTC'), |
| 443 | 506 | 'dateFormat' => \Yatra\Services\SettingsService::getString('date_format', 'Y-m-d'), |
| 444 | 507 | 'timeFormat' => \Yatra\Services\SettingsService::getString('time_format', 'H:i'), |
| @@ -445,9 +508,9 @@ | ||
| 445 | 508 | // Currency/settings |
| 446 | 509 | 'currency' => \Yatra\Services\SettingsService::getCurrency(), |
| 447 | 510 | 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'), |
| 448 | 511 | 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'), |
| 449 | - 'decimalPlaces' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'), | |
| 512 | + 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(), | |
| 450 | 513 | 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','), |
| 451 | 514 | 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'), |
| 452 | 515 | 'basePrice' => 0.0, |
| 453 | 516 | 'currencySymbol' => function_exists('yatra_get_currency_symbol') |
| @@ -518,8 +581,106 @@ | ||
| 518 | 581 | )); |
| 519 | 582 | } |
| 520 | 583 | |
| 521 | 584 | /** |
| 585 | + * Enqueue the country selector widget (assets/js/country-select.js + | |
| 586 | + * assets/css/country-select.css). | |
| 587 | + * | |
| 588 | + * Upgrades every `type => country` field (Country, Nationality, on both the | |
| 589 | + * contact and traveler sections) into a searchable dropdown showing the | |
| 590 | + * national flag, matching the phone country-code control. Purely additive: | |
| 591 | + * the underlying <select> still renders and submits, so a site that never | |
| 592 | + * loads this script behaves exactly as before. | |
| 593 | + * | |
| 594 | + * Idempotent, so it is safe to call from every path that renders the form. | |
| 595 | + * | |
| 596 | + * @return void | |
| 597 | + */ | |
| 598 | + private function enqueueCountrySelectAssets(): void | |
| 599 | + { | |
| 600 | + if (wp_script_is('yatra-country-select', 'enqueued')) { | |
| 601 | + return; | |
| 602 | + } | |
| 603 | + | |
| 604 | + $css = YATRA_PLUGIN_PATH . 'assets/css/country-select.css'; | |
| 605 | + if (file_exists($css)) { | |
| 606 | + wp_enqueue_style( | |
| 607 | + 'yatra-country-select', | |
| 608 | + YATRA_PLUGIN_URL . 'assets/css/country-select.css', | |
| 609 | + [], | |
| 610 | + YATRA_VERSION . '.' . filemtime($css) | |
| 611 | + ); | |
| 612 | + } | |
| 613 | + | |
| 614 | + $js = YATRA_PLUGIN_PATH . 'assets/js/country-select.js'; | |
| 615 | + if (!file_exists($js)) { | |
| 616 | + return; | |
| 617 | + } | |
| 618 | + | |
| 619 | + wp_enqueue_script( | |
| 620 | + 'yatra-country-select', | |
| 621 | + YATRA_PLUGIN_URL . 'assets/js/country-select.js', | |
| 622 | + [], | |
| 623 | + YATRA_VERSION . '.' . filemtime($js), | |
| 624 | + true | |
| 625 | + ); | |
| 626 | + | |
| 627 | + wp_localize_script('yatra-country-select', 'yatraCountrySelectData', [ | |
| 628 | + 'i18n' => [ | |
| 629 | + 'search' => __('Search country', 'yatra'), | |
| 630 | + 'noResults' => __('No matches', 'yatra'), | |
| 631 | + ], | |
| 632 | + ]); | |
| 633 | + } | |
| 634 | + | |
| 635 | + /** | |
| 636 | + * Enqueue the international phone-number widget (assets/js/phone-input.js + | |
| 637 | + * assets/css/phone-input.css) and localize its country + dial-code dataset. | |
| 638 | + * | |
| 639 | + * Self-contained (reads its own `yatraPhoneData` global) and idempotent, so | |
| 640 | + * it can be called from every path that renders the booking form. Country | |
| 641 | + * data is the single source of truth in {@see FormatHelper}. | |
| 642 | + * | |
| 643 | + * @return void | |
| 644 | + */ | |
| 645 | + private function enqueuePhoneInputAssets(): void | |
| 646 | + { | |
| 647 | + if (wp_script_is('yatra-phone-input', 'enqueued')) { | |
| 648 | + return; | |
| 649 | + } | |
| 650 | + | |
| 651 | + $css = YATRA_PLUGIN_PATH . 'assets/css/phone-input.css'; | |
| 652 | + if (file_exists($css)) { | |
| 653 | + wp_enqueue_style( | |
| 654 | + 'yatra-phone-input', | |
| 655 | + YATRA_PLUGIN_URL . 'assets/css/phone-input.css', | |
| 656 | + [], | |
| 657 | + YATRA_VERSION . '.' . filemtime($css) | |
| 658 | + ); | |
| 659 | + } | |
| 660 | + | |
| 661 | + $js = YATRA_PLUGIN_PATH . 'assets/js/phone-input.js'; | |
| 662 | + if (!file_exists($js)) { | |
| 663 | + return; | |
| 664 | + } | |
| 665 | + wp_enqueue_script( | |
| 666 | + 'yatra-phone-input', | |
| 667 | + YATRA_PLUGIN_URL . 'assets/js/phone-input.js', | |
| 668 | + [], | |
| 669 | + YATRA_VERSION . '.' . filemtime($js), | |
| 670 | + true | |
| 671 | + ); | |
| 672 | + wp_localize_script('yatra-phone-input', 'yatraPhoneData', [ | |
| 673 | + 'countries' => \Yatra\Helpers\FormatHelper::getPhoneCountries(), | |
| 674 | + 'priority' => \Yatra\Helpers\FormatHelper::getPhonePriority(), | |
| 675 | + 'i18n' => [ | |
| 676 | + 'search' => __('Search country', 'yatra'), | |
| 677 | + 'noResults' => __('No matches', 'yatra'), | |
| 678 | + ], | |
| 679 | + ]); | |
| 680 | + } | |
| 681 | + | |
| 682 | + /** | |
| 522 | 683 | * Enqueue activity listing specific assets |
| 523 | 684 | * |
| 524 | 685 | * @return void |
| 525 | 686 | */ |
| @@ -573,20 +734,71 @@ | ||
| 573 | 734 | YATRA_VERSION . '.' . filemtime($bookingCss) |
| 574 | 735 | ); |
| 575 | 736 | } |
| 576 | 737 | |
| 738 | + // Flatpickr — used by booking.js to upgrade Date-of-Birth (and other | |
| 739 | + // date) inputs to a picker with fast, typeable year navigation. The | |
| 740 | + // single-trip page already ships flatpickr (see single-trip.php); the | |
| 741 | + // dedicated booking page did not, so enqueue it here. booking.js | |
| 742 | + // self-guards on `typeof flatpickr`, so this is safe either way. | |
| 743 | + wp_enqueue_style( | |
| 744 | + 'yatra-flatpickr', | |
| 745 | + 'https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css', | |
| 746 | + [], | |
| 747 | + YATRA_VERSION | |
| 748 | + ); | |
| 749 | + wp_enqueue_script( | |
| 750 | + 'yatra-flatpickr', | |
| 751 | + 'https://cdn.jsdelivr.net/npm/flatpickr', | |
| 752 | + [], | |
| 753 | + YATRA_VERSION, | |
| 754 | + true | |
| 755 | + ); | |
| 756 | + | |
| 577 | 757 | // Enqueue booking-specific JavaScript |
| 578 | 758 | $bookingJs = YATRA_PLUGIN_PATH . 'assets/js/booking.js'; |
| 579 | 759 | if (file_exists($bookingJs)) { |
| 760 | + // booking.js renders user-facing strings via wp.i18n.__() (the | |
| 761 | + // "Processing..." button label and the per-gateway info messages | |
| 762 | + // shown when a payment method is selected). It therefore needs | |
| 763 | + // wp-i18n as a dependency AND wp_set_script_translations so its | |
| 764 | + // Jed catalog loads — mirroring the trip-detail enqueue above. | |
| 765 | + // Without these, those strings stay English on the standalone | |
| 766 | + // booking page regardless of site locale. | |
| 580 | 767 | wp_enqueue_script( |
| 581 | 768 | 'yatra-booking', |
| 582 | 769 | YATRA_PLUGIN_URL . 'assets/js/booking.js', |
| 583 | - ['jquery'], | |
| 770 | + ['jquery', 'yatra-flatpickr', 'wp-i18n'], | |
| 584 | 771 | YATRA_VERSION . '.' . filemtime($bookingJs), |
| 585 | 772 | true |
| 586 | 773 | ); |
| 774 | + if (function_exists('wp_set_script_translations')) { | |
| 775 | + wp_set_script_translations( | |
| 776 | + 'yatra-booking', | |
| 777 | + 'yatra', | |
| 778 | + YATRA_PLUGIN_PATH . 'i18n/languages' | |
| 779 | + ); | |
| 780 | + } | |
| 587 | 781 | } |
| 588 | - | |
| 782 | + | |
| 783 | + // International phone-number widget (country flag + dial code). | |
| 784 | + $this->enqueuePhoneInputAssets(); | |
| 785 | + $this->enqueueCountrySelectAssets(); | |
| 786 | + | |
| 787 | + // Load each available gateway's own client scripts on the checkout page | |
| 788 | + // (e.g. Square Web Payments SDK + square.js, Authorize.Net Accept.js + | |
| 789 | + // its handler, Razorpay SDK + its handler). Every gateway's | |
| 790 | + // enqueueScripts() self-guards on isAvailable(), so only enabled + | |
| 791 | + // configured gateways load anything. This call was previously missing, | |
| 792 | + // so Pro gateways that render an inline card form shipped no JS to | |
| 793 | + // checkout and clicking "Pay" just span the button forever. It is | |
| 794 | + // additive and safe for the others: Stripe's enqueueScripts() is a | |
| 795 | + // no-op (Stripe is loaded via enqueueCommonJs), and PayPal/Pay Later | |
| 796 | + // have no client scripts. | |
| 797 | + if (class_exists(\Yatra\PaymentGateways\PaymentGatewayRegistry::class)) { | |
| 798 | + \Yatra\PaymentGateways\PaymentGatewayRegistry::getInstance()->enqueueScripts(); | |
| 799 | + } | |
| 800 | + | |
| 589 | 801 | // Localize booking data for booking.js |
| 590 | 802 | $permalink_structure = get_option('permalink_structure') ?: ''; |
| 591 | 803 | $is_plain = empty($permalink_structure); |
| 592 | 804 | |
| @@ -625,13 +837,13 @@ | ||
| 625 | 837 | 'bookingNonce' => wp_create_nonce('yatra_booking_action'), |
| 626 | 838 | 'currency' => \Yatra\Services\SettingsService::getCurrency(), |
| 627 | 839 | 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'), |
| 628 | 840 | 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'), |
| 629 | - 'decimalPlaces' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'), | |
| 841 | + 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(), | |
| 630 | 842 | 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','), |
| 631 | 843 | 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'), |
| 632 | 844 | // Payment gateways data |
| 633 | - 'paymentGateways' => apply_filters('yatra_payment_gateways', \Yatra\Services\SettingsService::get('payment_gateways', [])), | |
| 845 | + 'paymentGateways' => $this->sanitizeGatewayConfigsForFrontend(apply_filters('yatra_payment_gateways', \Yatra\Services\SettingsService::get('payment_gateways', []))), | |
| 634 | 846 | 'paymentMethods' => \Yatra\Services\SettingsService::get('payment_methods', []), |
| 635 | 847 | 'paymentTestMode' => \Yatra\Services\SettingsService::get('payment_test_mode', false), |
| 636 | 848 | 'partialPayment' => \Yatra\Services\SettingsService::get('partial_payment', false), |
| 637 | 849 | 'partialPaymentPercentage' => \Yatra\Services\SettingsService::get('partial_payment_percentage', 0), |
| @@ -643,10 +855,17 @@ | ||
| 643 | 855 | 'gatewayOrder' => \Yatra\Services\SettingsService::get('gateway_order', []), |
| 644 | 856 | 'autoConfirmPayLater' => \Yatra\Services\SettingsService::get('auto_confirm_pay_later', true), |
| 645 | 857 | 'allowWaitlist' => \Yatra\Services\SettingsService::isEnabled('allow_waitlist'), |
| 646 | 858 | 'waitlistAutoConfirm' => \Yatra\Services\SettingsService::isEnabled('waitlist_auto_confirm'), |
| 647 | - 'gateways' => apply_filters('yatra_payment_gateways', \Yatra\Services\SettingsService::get('payment_gateways', [])), | |
| 648 | - 'enabledGateways' => \Yatra\Services\SettingsService::get('payment_gateways', []), | |
| 859 | + 'gateways' => $this->getGatewayFrontendConfigs(), | |
| 860 | + 'enabledGateways' => $this->sanitizeGatewayConfigsForFrontend(\Yatra\Services\SettingsService::get('payment_gateways', [])), | |
| 861 | + // Server-side translated UI strings for booking.js. PHP __() resolves via .mo | |
| 862 | + // (reliable), so these stay translatable even when the JS-translation JSON | |
| 863 | + // chain (wp_set_script_translations) doesn't load on a given setup. | |
| 864 | + 'i18n' => [ | |
| 865 | + 'complete_booking' => __('Complete Booking', 'yatra'), | |
| 866 | + 'pay_now' => __('Pay Now', 'yatra'), | |
| 867 | + ], | |
| 649 | 868 | ]; |
| 650 | 869 | |
| 651 | 870 | $bookingData = array_merge($bookingData, $this->getStripeFrontendBookingPayload()); |
| 652 | 871 | |
| @@ -709,12 +928,24 @@ | ||
| 709 | 928 | 'companyEmail' => \Yatra\Services\SettingsService::getString('company_email', ''), |
| 710 | 929 | 'currency' => \Yatra\Services\SettingsService::getCurrency(), |
| 711 | 930 | 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'), |
| 712 | 931 | 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'), |
| 713 | - 'decimalPlaces' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'), | |
| 932 | + 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(), | |
| 714 | 933 | 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','), |
| 715 | 934 | 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'), |
| 716 | 935 | 'locale' => get_locale(), |
| 936 | + // Global date/time format so the customer account pages render dates | |
| 937 | + // in the operator's configured format (Settings → General), not a | |
| 938 | + // hardcoded browser style. Keys mirror what the admin app receives. | |
| 939 | + 'date_format' => \Yatra\Services\SettingsService::getString('date_format', 'Y-m-d'), | |
| 940 | + 'time_format' => \Yatra\Services\SettingsService::getString('time_format', 'H:i'), | |
| 941 | + 'timezone' => \Yatra\Services\SettingsService::getString('timezone', 'UTC'), | |
| 942 | + // Full ISO country map (code => name) so the account profile can show | |
| 943 | + // full country names and render the country dropdown. Mirrors the | |
| 944 | + // admin (`yatraAdmin.countries`); honours the `yatra_countries_list` filter. | |
| 945 | + 'countries' => class_exists('\\Yatra\\Helpers\\FormatHelper') | |
| 946 | + ? \Yatra\Helpers\FormatHelper::getCountries() | |
| 947 | + : [], | |
| 717 | 948 | 'translations' => $this->getFrontendTranslations(), |
| 718 | 949 | 'wishlistEnabled' => \Yatra\Services\SettingsService::wishlistEnabled(), |
| 719 | 950 | ]); |
| 720 | 951 | |
| @@ -849,8 +1080,34 @@ | ||
| 849 | 1080 | 'Total Amount' => __('Total Amount', 'yatra'), |
| 850 | 1081 | 'Payment Status' => __('Payment Status', 'yatra'), |
| 851 | 1082 | 'View Details' => __('View Details', 'yatra'), |
| 852 | 1083 | |
| 1084 | + // Traveler / contact / emergency field labels on the account page. | |
| 1085 | + // Keep in sync with the `fieldLabel()` map in account/BookingDetails.tsx. | |
| 1086 | + 'First Name' => __('First Name', 'yatra'), | |
| 1087 | + 'Last Name' => __('Last Name', 'yatra'), | |
| 1088 | + 'Full Name' => __('Full Name', 'yatra'), | |
| 1089 | + 'Name' => __('Name', 'yatra'), | |
| 1090 | + 'Email' => __('Email', 'yatra'), | |
| 1091 | + 'Phone' => __('Phone', 'yatra'), | |
| 1092 | + 'Mobile' => __('Mobile', 'yatra'), | |
| 1093 | + 'Date of Birth' => __('Date of Birth', 'yatra'), | |
| 1094 | + 'Gender' => __('Gender', 'yatra'), | |
| 1095 | + 'Nationality' => __('Nationality', 'yatra'), | |
| 1096 | + 'Country' => __('Country', 'yatra'), | |
| 1097 | + 'Address' => __('Address', 'yatra'), | |
| 1098 | + 'City' => __('City', 'yatra'), | |
| 1099 | + 'State' => __('State', 'yatra'), | |
| 1100 | + 'Postal Code' => __('Postal Code', 'yatra'), | |
| 1101 | + 'Zip Code' => __('Zip Code', 'yatra'), | |
| 1102 | + 'Passport' => __('Passport', 'yatra'), | |
| 1103 | + 'Passport Number' => __('Passport Number', 'yatra'), | |
| 1104 | + 'Passport Expiry' => __('Passport Expiry', 'yatra'), | |
| 1105 | + 'Dietary Requirements' => __('Dietary Requirements', 'yatra'), | |
| 1106 | + 'Special Requirements' => __('Special Requirements', 'yatra'), | |
| 1107 | + 'Relationship' => __('Relationship', 'yatra'), | |
| 1108 | + 'Company' => __('Company', 'yatra'), | |
| 1109 | + | |
| 853 | 1110 | // Common |
| 854 | 1111 | 'Loading...' => __('Loading...', 'yatra'), |
| 855 | 1112 | 'No data available' => __('No data available', 'yatra'), |
| 856 | 1113 | 'Error loading data' => __('Error loading data', 'yatra'), |
| @@ -955,8 +1212,100 @@ | ||
| 955 | 1212 | { |
| 956 | 1213 | $basePath = $type === 'css' ? 'assets/css/' : 'assets/js/'; |
| 957 | 1214 | $fullPath = YATRA_PLUGIN_PATH . $basePath . $path; |
| 958 | 1215 | return file_exists($fullPath); |
| 1216 | + } | |
| 1217 | + | |
| 1218 | + /** | |
| 1219 | + * Strip secret credentials from per-gateway config before it is localized | |
| 1220 | + * into the page (yatraBookingData). The stored payment_gateways option | |
| 1221 | + * holds private keys / access tokens that must NEVER reach the browser; the | |
| 1222 | + * checkout scripts only ever read public values (publishable keys, Square | |
| 1223 | + * application/location IDs, Authorize.Net public client key, the enabled | |
| 1224 | + * flag, etc.). This removes the known secret keys while preserving the | |
| 1225 | + * structure and every public field, so existing gateways/consumers are | |
| 1226 | + * unaffected — only secrets are dropped. | |
| 1227 | + * | |
| 1228 | + * @param mixed $gateways | |
| 1229 | + * @return array<string, mixed> | |
| 1230 | + */ | |
| 1231 | + /** | |
| 1232 | + * Per-gateway PUBLIC config for the booking page, keyed by gateway id | |
| 1233 | + * (window.yatraBookingData.gateways.<id>). Checkout scripts read their public | |
| 1234 | + * settings from here — e.g. square.js → gateways.square.application_id / | |
| 1235 | + * location_id, authorizenet.js → gateways.authorize_net.public_client_key / | |
| 1236 | + * api_login_id. | |
| 1237 | + * | |
| 1238 | + * Source of truth is each ENABLED gateway's own getFrontendData(), i.e. an | |
| 1239 | + * allowlist the gateway itself declares. This is deliberately NOT a denylist | |
| 1240 | + * over the raw stored config: a denylist would leak any secret whose key we | |
| 1241 | + * forgot (e.g. Stripe live_secret_key / test_secret_key, Bank Transfer | |
| 1242 | + * account_number / routing_code). Gateways without a getFrontendData() | |
| 1243 | + * (Bank Transfer, PayPal, Pay Later, …) contribute nothing, so their stored | |
| 1244 | + * details never reach the browser. Disabled gateways are excluded. | |
| 1245 | + * | |
| 1246 | + * @return array<string, array<string, mixed>> | |
| 1247 | + */ | |
| 1248 | + private function getGatewayFrontendConfigs(): array | |
| 1249 | + { | |
| 1250 | + if (!class_exists(\Yatra\PaymentGateways\PaymentGatewayRegistry::class)) { | |
| 1251 | + return []; | |
| 1252 | + } | |
| 1253 | + | |
| 1254 | + $out = []; | |
| 1255 | + try { | |
| 1256 | + $registry = \Yatra\PaymentGateways\PaymentGatewayRegistry::getInstance(); | |
| 1257 | + foreach ($registry->getEnabledGateways() as $id => $gateway) { | |
| 1258 | + if (!is_object($gateway) || !method_exists($gateway, 'getFrontendData')) { | |
| 1259 | + continue; | |
| 1260 | + } | |
| 1261 | + $data = $gateway->getFrontendData(); | |
| 1262 | + if (is_array($data) && $data !== []) { | |
| 1263 | + $data['enabled'] = true; | |
| 1264 | + $out[(string) $id] = $data; | |
| 1265 | + } | |
| 1266 | + } | |
| 1267 | + } catch (\Throwable $e) { | |
| 1268 | + return []; | |
| 1269 | + } | |
| 1270 | + | |
| 1271 | + return $out; | |
| 1272 | + } | |
| 1273 | + | |
| 1274 | + private function sanitizeGatewayConfigsForFrontend($gateways): array | |
| 1275 | + { | |
| 1276 | + if (!is_array($gateways)) { | |
| 1277 | + return []; | |
| 1278 | + } | |
| 1279 | + | |
| 1280 | + // Credential fields that are private to the server. | |
| 1281 | + $secretKeys = [ | |
| 1282 | + 'access_token', | |
| 1283 | + 'api_key', | |
| 1284 | + 'api_secret', | |
| 1285 | + 'secret_key', | |
| 1286 | + 'key_secret', | |
| 1287 | + 'client_secret', | |
| 1288 | + 'transaction_key', | |
| 1289 | + 'webhook_secret', | |
| 1290 | + 'webhook_signing_secret', | |
| 1291 | + 'signing_secret', | |
| 1292 | + 'private_key', | |
| 1293 | + 'password', | |
| 1294 | + 'secret', | |
| 1295 | + ]; | |
| 1296 | + | |
| 1297 | + $clean = []; | |
| 1298 | + foreach ($gateways as $id => $config) { | |
| 1299 | + if (is_array($config)) { | |
| 1300 | + foreach ($secretKeys as $secret) { | |
| 1301 | + unset($config[$secret]); | |
| 1302 | + } | |
| 1303 | + } | |
| 1304 | + $clean[$id] = $config; | |
| 1305 | + } | |
| 1306 | + | |
| 1307 | + return $clean; | |
| 959 | 1308 | } |
| 960 | 1309 | |
| 961 | 1310 | /** |
| 962 | 1311 | * Stripe Elements (assets/js/stripe.js) expects publishableKey under yatraBookingData.stripe. |