| @@ -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 | /** |
| @@ -232,9 +274,21 @@ | ||
| 232 | 274 | $dependencies = ['jquery']; |
| 233 | 275 | if ($handle === 'trip') { |
| 234 | 276 | $dependencies[] = 'yatra-api-helper'; |
| 235 | 277 | } |
| 236 | - | |
| 278 | + // Scripts that call `wp.i18n.__()` for user-facing | |
| 279 | + // strings need wp-i18n as a dependency so the global | |
| 280 | + // exists before they run AND a `wp_set_script_translations` | |
| 281 | + // call below so WordPress loads each one's Jed JSON | |
| 282 | + // catalog (each handle has its own md5-named JSON | |
| 283 | + // because the .po references their respective source | |
| 284 | + // file paths). Add a handle here whenever you wrap a | |
| 285 | + // new string in __() inside its file. | |
| 286 | + $i18nHandles = ['trip', 'listing', 'stripe', 'tour-viewer', 'video-player']; | |
| 287 | + if (in_array($handle, $i18nHandles, true)) { | |
| 288 | + $dependencies[] = 'wp-i18n'; | |
| 289 | + } | |
| 290 | + | |
| 237 | 291 | wp_enqueue_script( |
| 238 | 292 | "yatra-{$handle}", |
| 239 | 293 | YATRA_PLUGIN_URL . "assets/js/{$filename}", |
| 240 | 294 | $dependencies, |
| @@ -240,8 +294,22 @@ | ||
| 240 | 294 | $dependencies, |
| 241 | 295 | YATRA_VERSION . '.' . filemtime($filePath), |
| 242 | 296 | true |
| 243 | 297 | ); |
| 298 | + | |
| 299 | + // Mirror the wp-i18n dep list above. wp_set_script_translations | |
| 300 | + // tells WordPress where to look for this script's Jed JSON | |
| 301 | + // catalog (path = plugin's i18n/languages/) and the loader | |
| 302 | + // hashes md5(handle src) to find the right file. | |
| 303 | + if (in_array($handle, $i18nHandles, true) | |
| 304 | + && function_exists('wp_set_script_translations') | |
| 305 | + ) { | |
| 306 | + wp_set_script_translations( | |
| 307 | + "yatra-{$handle}", | |
| 308 | + 'yatra', | |
| 309 | + YATRA_PLUGIN_PATH . 'i18n/languages' | |
| 310 | + ); | |
| 311 | + } | |
| 244 | 312 | } |
| 245 | 313 | } |
| 246 | 314 | |
| 247 | 315 | if (\Yatra\Services\SettingsService::wishlistEnabled()) { |
| @@ -249,17 +317,46 @@ | ||
| 249 | 317 | if (file_exists($wishPath)) { |
| 250 | 318 | wp_enqueue_script( |
| 251 | 319 | 'yatra-listing-wishlist', |
| 252 | 320 | YATRA_PLUGIN_URL . 'assets/js/listing-wishlist.js', |
| 253 | - ['jquery'], | |
| 321 | + ['jquery', 'wp-i18n'], | |
| 254 | 322 | YATRA_VERSION . '.' . filemtime($wishPath), |
| 255 | 323 | true |
| 256 | 324 | ); |
| 325 | + if (function_exists('wp_set_script_translations')) { | |
| 326 | + wp_set_script_translations( | |
| 327 | + 'yatra-listing-wishlist', | |
| 328 | + 'yatra', | |
| 329 | + YATRA_PLUGIN_PATH . 'i18n/languages' | |
| 330 | + ); | |
| 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(); | |
| 257 | 340 | wp_localize_script('yatra-listing-wishlist', 'yatraWishlistConfig', [ |
| 258 | 341 | 'enabled' => true, |
| 259 | 342 | 'restUrl' => rest_url('yatra/v1'), |
| 260 | 343 | 'nonce' => wp_create_nonce('wp_rest'), |
| 261 | 344 | 'isLoggedIn' => is_user_logged_in(), |
| 345 | + 'loginUrl' => $yatraAccountLoginUrl, | |
| 346 | + 'i18n' => [ | |
| 347 | + 'loginRequired' => __('Login Required', 'yatra'), | |
| 348 | + 'loginPrompt' => __('Please login to save trips to your wishlist.', 'yatra'), | |
| 349 | + 'login' => __('Login', 'yatra'), | |
| 350 | + 'cancel' => __('Cancel', 'yatra'), | |
| 351 | + 'genericError' => __('An error occurred. Please try again.', 'yatra'), | |
| 352 | + 'saved' => __('Trip saved to wishlist', 'yatra'), | |
| 353 | + 'removed' => __('Trip removed from wishlist', 'yatra'), | |
| 354 | + 'saveFailed' => __('Failed to save trip', 'yatra'), | |
| 355 | + 'removeFailed' => __('Failed to remove trip', 'yatra'), | |
| 356 | + 'addAria' => __('Add to favorites', 'yatra'), | |
| 357 | + 'removeAria' => __('Remove from favorites', 'yatra'), | |
| 358 | + ], | |
| 262 | 359 | ]); |
| 263 | 360 | } |
| 264 | 361 | } |
| 265 | 362 | } |
| @@ -326,14 +423,26 @@ | ||
| 326 | 423 | if (file_exists($bookingJs)) { |
| 327 | 424 | wp_enqueue_script( |
| 328 | 425 | 'yatra-booking', |
| 329 | 426 | YATRA_PLUGIN_URL . 'assets/js/booking.js', |
| 330 | - ['jquery'], | |
| 427 | + ['jquery', 'wp-i18n'], | |
| 331 | 428 | YATRA_VERSION . '.' . filemtime($bookingJs), |
| 332 | 429 | true |
| 333 | 430 | ); |
| 431 | + if (function_exists('wp_set_script_translations')) { | |
| 432 | + wp_set_script_translations( | |
| 433 | + 'yatra-booking', | |
| 434 | + 'yatra', | |
| 435 | + YATRA_PLUGIN_PATH . 'i18n/languages' | |
| 436 | + ); | |
| 437 | + } | |
| 334 | 438 | } |
| 335 | 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 | + | |
| 336 | 445 | // Mobile sticky-sidebar + flatpickr init for the single-trip page. Lives in a |
| 337 | 446 | // dedicated file rather than as inline <script> in the partial because |
| 338 | 447 | // WordPress core's `convert_chars` filter (hooked to the_content) rewrites the |
| 339 | 448 | // `&&` operators inside inline scripts as `&&` — JS parsers don't |
| @@ -343,12 +452,19 @@ | ||
| 343 | 452 | if (file_exists($sidebarJs)) { |
| 344 | 453 | wp_enqueue_script( |
| 345 | 454 | 'yatra-single-trip-sidebar', |
| 346 | 455 | YATRA_PLUGIN_URL . 'assets/js/single-trip-sidebar.js', |
| 347 | - ['yatra-trip'], // depends on window.yatraTripData from yatra-trip | |
| 456 | + ['yatra-trip', 'wp-i18n'], // depends on window.yatraTripData from yatra-trip | |
| 348 | 457 | YATRA_VERSION . '.' . filemtime($sidebarJs), |
| 349 | 458 | true |
| 350 | 459 | ); |
| 460 | + if (function_exists('wp_set_script_translations')) { | |
| 461 | + wp_set_script_translations( | |
| 462 | + 'yatra-single-trip-sidebar', | |
| 463 | + 'yatra', | |
| 464 | + YATRA_PLUGIN_PATH . 'i18n/languages' | |
| 465 | + ); | |
| 466 | + } | |
| 351 | 467 | } |
| 352 | 468 | |
| 353 | 469 | // Localize trip page data for JS (trip.js, booking.js) |
| 354 | 470 | global $trip; |
| @@ -376,8 +492,16 @@ | ||
| 376 | 492 | 'tripId' => $trip_id, |
| 377 | 493 | 'tripSlug' => $trip_slug, |
| 378 | 494 | 'wishlistEnabled' => \Yatra\Services\SettingsService::wishlistEnabled(), |
| 379 | 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 | + })(), | |
| 380 | 504 | // Regional settings |
| 381 | 505 | 'timezone' => \Yatra\Services\SettingsService::getString('timezone', 'UTC'), |
| 382 | 506 | 'dateFormat' => \Yatra\Services\SettingsService::getString('date_format', 'Y-m-d'), |
| 383 | 507 | 'timeFormat' => \Yatra\Services\SettingsService::getString('time_format', 'H:i'), |
| @@ -384,9 +508,9 @@ | ||
| 384 | 508 | // Currency/settings |
| 385 | 509 | 'currency' => \Yatra\Services\SettingsService::getCurrency(), |
| 386 | 510 | 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'), |
| 387 | 511 | 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'), |
| 388 | - 'decimalPlaces' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'), | |
| 512 | + 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(), | |
| 389 | 513 | 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','), |
| 390 | 514 | 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'), |
| 391 | 515 | 'basePrice' => 0.0, |
| 392 | 516 | 'currencySymbol' => function_exists('yatra_get_currency_symbol') |
| @@ -444,13 +568,119 @@ | ||
| 444 | 568 | 'isRemainingPayment' => false, |
| 445 | 569 | 'remainingAmount' => 0, |
| 446 | 570 | 'totalAmount' => 0, |
| 447 | 571 | 'amountPaid' => 0, |
| 572 | + // Booking-scoped CSRF nonce — covers BOTH logged-in and | |
| 573 | + // guest checkouts. The REST endpoint's public | |
| 574 | + // permission_callback intentionally bypasses the WP REST | |
| 575 | + // cookie/nonce check (so guests can hit it at all); | |
| 576 | + // this token is what gates the actual write. The JS | |
| 577 | + // forwards it in the `X-Yatra-Booking-Nonce` header on | |
| 578 | + // every booking-create / booking-update POST. | |
| 579 | + 'bookingNonce' => wp_create_nonce('yatra_booking_action'), | |
| 448 | 580 | ] |
| 449 | 581 | )); |
| 450 | 582 | } |
| 451 | 583 | |
| 452 | 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 | + /** | |
| 453 | 683 | * Enqueue activity listing specific assets |
| 454 | 684 | * |
| 455 | 685 | * @return void |
| 456 | 686 | */ |
| @@ -504,20 +734,71 @@ | ||
| 504 | 734 | YATRA_VERSION . '.' . filemtime($bookingCss) |
| 505 | 735 | ); |
| 506 | 736 | } |
| 507 | 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 | + | |
| 508 | 757 | // Enqueue booking-specific JavaScript |
| 509 | 758 | $bookingJs = YATRA_PLUGIN_PATH . 'assets/js/booking.js'; |
| 510 | 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. | |
| 511 | 767 | wp_enqueue_script( |
| 512 | 768 | 'yatra-booking', |
| 513 | 769 | YATRA_PLUGIN_URL . 'assets/js/booking.js', |
| 514 | - ['jquery'], | |
| 770 | + ['jquery', 'yatra-flatpickr', 'wp-i18n'], | |
| 515 | 771 | YATRA_VERSION . '.' . filemtime($bookingJs), |
| 516 | 772 | true |
| 517 | 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 | + } | |
| 518 | 781 | } |
| 519 | - | |
| 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 | + | |
| 520 | 801 | // Localize booking data for booking.js |
| 521 | 802 | $permalink_structure = get_option('permalink_structure') ?: ''; |
| 522 | 803 | $is_plain = empty($permalink_structure); |
| 523 | 804 | |
| @@ -548,16 +829,21 @@ | ||
| 548 | 829 | 'siteUrl' => site_url(), |
| 549 | 830 | 'bookingBase' => \Yatra\Services\SettingsService::getBookingBase(), |
| 550 | 831 | 'permalinkStructure' => $is_plain ? 'plain' : $permalink_structure, |
| 551 | 832 | 'nonce' => wp_create_nonce('wp_rest'), |
| 833 | + // Booking-scoped CSRF nonce. See enqueueTripDetailAssets() | |
| 834 | + // for the rationale: the booking REST endpoint bypasses | |
| 835 | + // the WP REST cookie/nonce check (so guests can use it), | |
| 836 | + // and this token is what gates the actual booking write. | |
| 837 | + 'bookingNonce' => wp_create_nonce('yatra_booking_action'), | |
| 552 | 838 | 'currency' => \Yatra\Services\SettingsService::getCurrency(), |
| 553 | 839 | 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'), |
| 554 | 840 | 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'), |
| 555 | - 'decimalPlaces' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'), | |
| 841 | + 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(), | |
| 556 | 842 | 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','), |
| 557 | 843 | 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'), |
| 558 | 844 | // Payment gateways data |
| 559 | - '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', []))), | |
| 560 | 846 | 'paymentMethods' => \Yatra\Services\SettingsService::get('payment_methods', []), |
| 561 | 847 | 'paymentTestMode' => \Yatra\Services\SettingsService::get('payment_test_mode', false), |
| 562 | 848 | 'partialPayment' => \Yatra\Services\SettingsService::get('partial_payment', false), |
| 563 | 849 | 'partialPaymentPercentage' => \Yatra\Services\SettingsService::get('partial_payment_percentage', 0), |
| @@ -569,10 +855,17 @@ | ||
| 569 | 855 | 'gatewayOrder' => \Yatra\Services\SettingsService::get('gateway_order', []), |
| 570 | 856 | 'autoConfirmPayLater' => \Yatra\Services\SettingsService::get('auto_confirm_pay_later', true), |
| 571 | 857 | 'allowWaitlist' => \Yatra\Services\SettingsService::isEnabled('allow_waitlist'), |
| 572 | 858 | 'waitlistAutoConfirm' => \Yatra\Services\SettingsService::isEnabled('waitlist_auto_confirm'), |
| 573 | - 'gateways' => apply_filters('yatra_payment_gateways', \Yatra\Services\SettingsService::get('payment_gateways', [])), | |
| 574 | - '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 | + ], | |
| 575 | 868 | ]; |
| 576 | 869 | |
| 577 | 870 | $bookingData = array_merge($bookingData, $this->getStripeFrontendBookingPayload()); |
| 578 | 871 | |
| @@ -635,12 +928,24 @@ | ||
| 635 | 928 | 'companyEmail' => \Yatra\Services\SettingsService::getString('company_email', ''), |
| 636 | 929 | 'currency' => \Yatra\Services\SettingsService::getCurrency(), |
| 637 | 930 | 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'), |
| 638 | 931 | 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'), |
| 639 | - 'decimalPlaces' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'), | |
| 932 | + 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(), | |
| 640 | 933 | 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','), |
| 641 | 934 | 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'), |
| 642 | 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 | + : [], | |
| 643 | 948 | 'translations' => $this->getFrontendTranslations(), |
| 644 | 949 | 'wishlistEnabled' => \Yatra\Services\SettingsService::wishlistEnabled(), |
| 645 | 950 | ]); |
| 646 | 951 | |
| @@ -698,21 +1003,45 @@ | ||
| 698 | 1003 | 'firstDayOfWeek' => $first_day, |
| 699 | 1004 | ]; |
| 700 | 1005 | } |
| 701 | 1006 | |
| 1007 | + // IMPORTANT — `month_abbrev` and `weekday_abbrev` are keyed by the | |
| 1008 | + // TRANSLATED LONG NAME, not by a numeric index: | |
| 1009 | + // | |
| 1010 | + // $wp_locale->month['01'] = 'January' (or 'जनवरी', 'enero'…) | |
| 1011 | + // $wp_locale->month_abbrev['January'] = 'Jan' (or 'जन', 'ene'…) | |
| 1012 | + // | |
| 1013 | + // An earlier version of this code mistakenly indexed | |
| 1014 | + // month_abbrev by '01'..'12' / weekday_abbrev by 0..6, which | |
| 1015 | + // ALWAYS returned null → flatpickr's locale.months.shorthand | |
| 1016 | + // shipped as an array of empty strings → the `M` token in any | |
| 1017 | + // altFormat rendered as nothing. Net effect: a date set to | |
| 1018 | + // "19 May 2026" displayed as "19 2026" (no month) under any | |
| 1019 | + // non-en_US locale that exposed the bug. | |
| 1020 | + // | |
| 1021 | + // WP_Locale exposes get_month_abbrev() / get_weekday_abbrev() | |
| 1022 | + // which take the long name and do the right lookup. We use | |
| 1023 | + // those so the indexing rule lives inside core, not here. | |
| 702 | 1024 | $months_long = []; |
| 703 | 1025 | $months_short = []; |
| 704 | 1026 | for ($m = 1; $m <= 12; ++$m) { |
| 705 | 1027 | $key = sprintf('%02d', $m); |
| 706 | - $months_long[] = $wp_locale->month[$key] ?? ''; | |
| 707 | - $months_short[] = $wp_locale->month_abbrev[$key] ?? ''; | |
| 1028 | + $long = $wp_locale->month[$key] ?? ''; | |
| 1029 | + $short = $long !== '' ? (string) $wp_locale->get_month_abbrev($long) : ''; | |
| 1030 | + $months_long[] = $long; | |
| 1031 | + // Final fallback to the long name if the locale has no | |
| 1032 | + // abbreviated form — better than shipping an empty string | |
| 1033 | + // that flatpickr would render as blank. | |
| 1034 | + $months_short[] = $short !== '' ? $short : $long; | |
| 708 | 1035 | } |
| 709 | 1036 | |
| 710 | 1037 | $weekdays_long = []; |
| 711 | 1038 | $weekdays_short = []; |
| 712 | 1039 | for ($d = 0; $d <= 6; ++$d) { |
| 713 | - $weekdays_long[] = $wp_locale->weekday[$d] ?? ''; | |
| 714 | - $weekdays_short[] = $wp_locale->weekday_abbrev[$d] ?? ''; | |
| 1040 | + $long = $wp_locale->weekday[$d] ?? ''; | |
| 1041 | + $short = $long !== '' ? (string) $wp_locale->get_weekday_abbrev($long) : ''; | |
| 1042 | + $weekdays_long[] = $long; | |
| 1043 | + $weekdays_short[] = $short !== '' ? $short : $long; | |
| 715 | 1044 | } |
| 716 | 1045 | |
| 717 | 1046 | $payload = [ |
| 718 | 1047 | 'weekdays' => [ |
| @@ -751,8 +1080,34 @@ | ||
| 751 | 1080 | 'Total Amount' => __('Total Amount', 'yatra'), |
| 752 | 1081 | 'Payment Status' => __('Payment Status', 'yatra'), |
| 753 | 1082 | 'View Details' => __('View Details', 'yatra'), |
| 754 | 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 | + | |
| 755 | 1110 | // Common |
| 756 | 1111 | 'Loading...' => __('Loading...', 'yatra'), |
| 757 | 1112 | 'No data available' => __('No data available', 'yatra'), |
| 758 | 1113 | 'Error loading data' => __('Error loading data', 'yatra'), |
| @@ -857,8 +1212,100 @@ | ||
| 857 | 1212 | { |
| 858 | 1213 | $basePath = $type === 'css' ? 'assets/css/' : 'assets/js/'; |
| 859 | 1214 | $fullPath = YATRA_PLUGIN_PATH . $basePath . $path; |
| 860 | 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; | |
| 861 | 1308 | } |
| 862 | 1309 | |
| 863 | 1310 | /** |
| 864 | 1311 | * Stripe Elements (assets/js/stripe.js) expects publishableKey under yatraBookingData.stripe. |