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 | app/Providers/FrontendAssetsProvider.php +463 -16 3.0.4 → 3.0.16 View file →
@@ -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 `&#038;&#038;` — 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.