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 +673 -22 3.0.2.9 → 3.0.16 View file →
@@ -24,13 +24,98 @@
24 24 * @return void
25 25 */
26 26 public function register(): void
27 27 {
28 + add_action('init', [self::class, 'registerCoreFrontendStylesheets'], 5);
28 29 // Hook into WordPress to enqueue assets
29 30 add_action('wp_enqueue_scripts', [$this, 'enqueueAssets']);
30 31 }
31 32
32 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 + /**
76 + * Register Font Awesome (optional) and common.css so block editor + shortcode styles can
77 + * depend on `yatra-common` (shared @keyframes: yatra-spin, yatra-shimmer, etc.).
78 + */
79 + /**
80 + * @return list<string>
81 + */
82 + public static function shortcodeStyleDependencies(): array
83 + {
84 + self::registerCoreFrontendStylesheets();
85 +
86 + return wp_style_is('yatra-common', 'registered') ? ['yatra-common'] : [];
87 + }
88 +
89 + public static function registerCoreFrontendStylesheets(): void
90 + {
91 + $faPath = YATRA_PLUGIN_PATH . 'assets/vendor/fontawesome/css/all.min.css';
92 + if (file_exists($faPath) && !wp_style_is('yatra-fontawesome-6', 'registered')) {
93 + wp_register_style(
94 + 'yatra-fontawesome-6',
95 + YATRA_PLUGIN_URL . 'assets/vendor/fontawesome/css/all.min.css',
96 + [],
97 + '6.7.2.' . filemtime($faPath)
98 + );
99 + }
100 +
101 + if (wp_style_is('yatra-common', 'registered')) {
102 + return;
103 + }
104 + $path = YATRA_PLUGIN_PATH . 'assets/css/common.css';
105 + if (!is_readable($path)) {
106 + return;
107 + }
108 + $commonDeps = wp_style_is('yatra-fontawesome-6', 'registered') ? ['yatra-fontawesome-6'] : [];
109 + wp_register_style(
110 + 'yatra-common',
111 + YATRA_PLUGIN_URL . 'assets/css/common.css',
112 + $commonDeps,
113 + YATRA_VERSION . '.' . filemtime($path)
114 + );
115 + }
116 +
117 + /**
33 118 * Enqueue frontend assets based on context
34 119 *
35 120 * @return void
36 121 */
@@ -80,22 +165,93 @@
80 165 'video-player' => 'video-player.css',
81 166 'tour-viewer' => 'tour-viewer.css',
82 167 ];
83 168
169 + self::registerCoreFrontendStylesheets();
170 +
171 + if (wp_style_is('yatra-fontawesome-6', 'registered')) {
172 + wp_enqueue_style('yatra-fontawesome-6');
173 + }
174 +
84 175 foreach ($cssFiles as $handle => $filename) {
85 176 $filePath = YATRA_PLUGIN_PATH . "assets/css/{$filename}";
86 - if (file_exists($filePath)) {
87 - wp_enqueue_style(
88 - "yatra-{$handle}",
89 - YATRA_PLUGIN_URL . "assets/css/{$filename}",
90 - [],
91 - YATRA_VERSION . '.' . filemtime($filePath)
92 - );
177 + if (!file_exists($filePath)) {
178 + continue;
93 179 }
180 + $styleHandle = 'yatra-' . $handle;
181 + $ver = YATRA_VERSION . '.' . filemtime($filePath);
182 +
183 + if ($handle === 'common') {
184 + if (wp_style_is('yatra-common', 'registered')) {
185 + wp_enqueue_style('yatra-common');
186 + } else {
187 + $deps = wp_style_is('yatra-fontawesome-6', 'registered') ? ['yatra-fontawesome-6'] : [];
188 + wp_enqueue_style($styleHandle, YATRA_PLUGIN_URL . "assets/css/{$filename}", $deps, $ver);
189 + }
190 + continue;
191 + }
192 +
193 + $deps = [];
194 + if ($handle !== 'common' && wp_style_is('yatra-common', 'registered')) {
195 + $deps[] = 'yatra-common';
196 + }
197 +
198 + wp_enqueue_style(
199 + $styleHandle,
200 + YATRA_PLUGIN_URL . "assets/css/{$filename}",
201 + $deps,
202 + $ver
203 + );
94 204 }
205 +
206 + $this->enqueueFrontendThemeVariables();
207 + $this->enqueueFrontendLayoutVariables();
95 208 }
96 209
97 210 /**
211 + * Override design tokens from Settings (single primary color → related shades).
212 + */
213 + private function enqueueFrontendThemeVariables(): void
214 + {
215 + if (!wp_style_is('yatra-common', 'enqueued')) {
216 + return;
217 + }
218 + $primary = \Yatra\Services\SettingsService::getString(
219 + 'frontend_primary_color',
220 + \Yatra\Utils\FrontendThemeCss::DEFAULT_PRIMARY
221 + );
222 + $primary = \Yatra\Utils\FrontendThemeCss::sanitizePrimaryColor($primary);
223 + if (strtolower($primary) === strtolower(\Yatra\Utils\FrontendThemeCss::DEFAULT_PRIMARY)) {
224 + return;
225 + }
226 + $css = \Yatra\Utils\FrontendThemeCss::buildInlineRootCss($primary);
227 + if ($css !== '') {
228 + wp_add_inline_style('yatra-common', $css);
229 + }
230 + }
231 +
232 + /**
233 + * Align --yatra-container-max-width with the active theme (theme.json wide/content size or $content_width).
234 + */
235 + private function enqueueFrontendLayoutVariables(): void
236 + {
237 + if (!wp_style_is('yatra-common', 'enqueued')) {
238 + return;
239 + }
240 + $fromSetting = \Yatra\Utils\FrontendThemeCss::sanitizeContainerMaxWidthSetting(
241 + \Yatra\Services\SettingsService::getString('frontend_container_max_width', '')
242 + );
243 + $max = $fromSetting !== ''
244 + ? $fromSetting
245 + : \Yatra\Utils\FrontendThemeCss::resolveThemeContainerMaxWidth();
246 + if ($max === null || $max === '') {
247 + return;
248 + }
249 + $maxEsc = esc_attr($max);
250 + wp_add_inline_style('yatra-common', ':root{--yatra-container-max-width:' . $maxEsc . ';}');
251 + }
252 +
253 + /**
98 254 * Enqueue common JavaScript files
99 255 *
100 256 * @return void
101 257 */
@@ -118,9 +274,21 @@
118 274 $dependencies = ['jquery'];
119 275 if ($handle === 'trip') {
120 276 $dependencies[] = 'yatra-api-helper';
121 277 }
122 -
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 +
123 291 wp_enqueue_script(
124 292 "yatra-{$handle}",
125 293 YATRA_PLUGIN_URL . "assets/js/{$filename}",
126 294 $dependencies,
@@ -126,8 +294,22 @@
126 294 $dependencies,
127 295 YATRA_VERSION . '.' . filemtime($filePath),
128 296 true
129 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 + }
130 312 }
131 313 }
132 314
133 315 if (\Yatra\Services\SettingsService::wishlistEnabled()) {
@@ -135,17 +317,46 @@
135 317 if (file_exists($wishPath)) {
136 318 wp_enqueue_script(
137 319 'yatra-listing-wishlist',
138 320 YATRA_PLUGIN_URL . 'assets/js/listing-wishlist.js',
139 - ['jquery'],
321 + ['jquery', 'wp-i18n'],
140 322 YATRA_VERSION . '.' . filemtime($wishPath),
141 323 true
142 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();
143 340 wp_localize_script('yatra-listing-wishlist', 'yatraWishlistConfig', [
144 341 'enabled' => true,
145 342 'restUrl' => rest_url('yatra/v1'),
146 343 'nonce' => wp_create_nonce('wp_rest'),
147 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 + ],
148 359 ]);
149 360 }
150 361 }
151 362 }
@@ -212,13 +423,49 @@
212 423 if (file_exists($bookingJs)) {
213 424 wp_enqueue_script(
214 425 'yatra-booking',
215 426 YATRA_PLUGIN_URL . 'assets/js/booking.js',
216 - ['jquery'],
427 + ['jquery', 'wp-i18n'],
217 428 YATRA_VERSION . '.' . filemtime($bookingJs),
218 429 true
219 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 + }
220 438 }
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 +
445 + // Mobile sticky-sidebar + flatpickr init for the single-trip page. Lives in a
446 + // dedicated file rather than as inline <script> in the partial because
447 + // WordPress core's `convert_chars` filter (hooked to the_content) rewrites the
448 + // `&&` operators inside inline scripts as `&#038;&#038;` — JS parsers don't
449 + // decode HTML entities inside <script>, producing a SyntaxError. As a properly
450 + // enqueued external file, the source is delivered verbatim.
451 + $sidebarJs = YATRA_PLUGIN_PATH . 'assets/js/single-trip-sidebar.js';
452 + if (file_exists($sidebarJs)) {
453 + wp_enqueue_script(
454 + 'yatra-single-trip-sidebar',
455 + YATRA_PLUGIN_URL . 'assets/js/single-trip-sidebar.js',
456 + ['yatra-trip', 'wp-i18n'], // depends on window.yatraTripData from yatra-trip
457 + YATRA_VERSION . '.' . filemtime($sidebarJs),
458 + true
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 + }
467 + }
221 468
222 469 // Localize trip page data for JS (trip.js, booking.js)
223 470 global $trip;
224 471
@@ -245,8 +492,16 @@
245 492 'tripId' => $trip_id,
246 493 'tripSlug' => $trip_slug,
247 494 'wishlistEnabled' => \Yatra\Services\SettingsService::wishlistEnabled(),
248 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 + })(),
249 504 // Regional settings
250 505 'timezone' => \Yatra\Services\SettingsService::getString('timezone', 'UTC'),
251 506 'dateFormat' => \Yatra\Services\SettingsService::getString('date_format', 'Y-m-d'),
252 507 'timeFormat' => \Yatra\Services\SettingsService::getString('time_format', 'H:i'),
@@ -251,10 +506,11 @@
251 506 'dateFormat' => \Yatra\Services\SettingsService::getString('date_format', 'Y-m-d'),
252 507 'timeFormat' => \Yatra\Services\SettingsService::getString('time_format', 'H:i'),
253 508 // Currency/settings
254 509 'currency' => \Yatra\Services\SettingsService::getCurrency(),
255 - 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'before'),
256 - 'decimalPlaces' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'),
510 + 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
511 + 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
512 + 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(),
257 513 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
258 514 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
259 515 'basePrice' => 0.0,
260 516 'currencySymbol' => function_exists('yatra_get_currency_symbol')
@@ -261,8 +517,17 @@
261 517 ? yatra_get_currency_symbol(\Yatra\Services\SettingsService::getCurrency())
262 518 : '$',
263 519 'availabilityDates' => [],
264 520 'groupDiscountsUrl' => rest_url('yatra/v1/discounts/group-discounts'),
521 + 'dynamicPricingDisplay' => apply_filters('yatra_get_dynamic_pricing_display_settings', [
522 + 'show_original_price' => true,
523 + 'show_savings_badge' => true,
524 + 'show_urgency_messages' => false,
525 + ]),
526 + 'pricingType' => 'regular',
527 + 'sidebarAvailability' => [],
528 + 'sidebarGroupDiscounts' => [],
529 + 'flatpickrLocale' => $this->buildFlatpickrLocalePayload(),
265 530 ];
266 531
267 532 if ($has_trip) {
268 533 if (function_exists('yatra_single_trip_calculate_base_price')) {
@@ -280,8 +545,14 @@
280 545
281 546 return null;
282 547 }, $trip->getAvailabilityDates())));
283 548 }
549 + if (function_exists('yatra_single_trip_get_client_booking_payload')) {
550 + $bookingPayload = yatra_single_trip_get_client_booking_payload($trip);
551 + $tripData['pricingType'] = $bookingPayload['pricingType'];
552 + $tripData['sidebarAvailability'] = $bookingPayload['sidebarAvailability'];
553 + $tripData['sidebarGroupDiscounts'] = $bookingPayload['sidebarGroupDiscounts'];
554 + }
284 555 }
285 556
286 557 wp_localize_script('yatra-trip', 'yatraTripData', $tripData);
287 558 $tripTitle = '';
@@ -297,13 +568,119 @@
297 568 'isRemainingPayment' => false,
298 569 'remainingAmount' => 0,
299 570 'totalAmount' => 0,
300 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'),
301 580 ]
302 581 ));
303 582 }
304 583
305 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 + /**
306 683 * Enqueue activity listing specific assets
307 684 *
308 685 * @return void
309 686 */
@@ -352,25 +729,76 @@
352 729 if (file_exists($bookingCss)) {
353 730 wp_enqueue_style(
354 731 'yatra-booking',
355 732 YATRA_PLUGIN_URL . 'assets/css/booking.css',
356 - [],
733 + ['yatra-common'],
357 734 YATRA_VERSION . '.' . filemtime($bookingCss)
358 735 );
359 736 }
360 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 +
361 757 // Enqueue booking-specific JavaScript
362 758 $bookingJs = YATRA_PLUGIN_PATH . 'assets/js/booking.js';
363 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.
364 767 wp_enqueue_script(
365 768 'yatra-booking',
366 769 YATRA_PLUGIN_URL . 'assets/js/booking.js',
367 - ['jquery'],
770 + ['jquery', 'yatra-flatpickr', 'wp-i18n'],
368 771 YATRA_VERSION . '.' . filemtime($bookingJs),
369 772 true
370 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 + }
371 781 }
372 -
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 +
373 801 // Localize booking data for booking.js
374 802 $permalink_structure = get_option('permalink_structure') ?: '';
375 803 $is_plain = empty($permalink_structure);
376 804
@@ -401,15 +829,21 @@
401 829 'siteUrl' => site_url(),
402 830 'bookingBase' => \Yatra\Services\SettingsService::getBookingBase(),
403 831 'permalinkStructure' => $is_plain ? 'plain' : $permalink_structure,
404 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'),
405 838 'currency' => \Yatra\Services\SettingsService::getCurrency(),
406 - 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'before'),
407 - 'decimalPlaces' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'),
839 + 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
840 + 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
841 + 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(),
408 842 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
409 843 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
410 844 // Payment gateways data
411 - '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', []))),
412 846 'paymentMethods' => \Yatra\Services\SettingsService::get('payment_methods', []),
413 847 'paymentTestMode' => \Yatra\Services\SettingsService::get('payment_test_mode', false),
414 848 'partialPayment' => \Yatra\Services\SettingsService::get('partial_payment', false),
415 849 'partialPaymentPercentage' => \Yatra\Services\SettingsService::get('partial_payment_percentage', 0),
@@ -421,10 +855,17 @@
421 855 'gatewayOrder' => \Yatra\Services\SettingsService::get('gateway_order', []),
422 856 'autoConfirmPayLater' => \Yatra\Services\SettingsService::get('auto_confirm_pay_later', true),
423 857 'allowWaitlist' => \Yatra\Services\SettingsService::isEnabled('allow_waitlist'),
424 858 'waitlistAutoConfirm' => \Yatra\Services\SettingsService::isEnabled('waitlist_auto_confirm'),
425 - 'gateways' => apply_filters('yatra_payment_gateways', \Yatra\Services\SettingsService::get('payment_gateways', [])),
426 - '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 + ],
427 868 ];
428 869
429 870 $bookingData = array_merge($bookingData, $this->getStripeFrontendBookingPayload());
430 871
@@ -485,16 +926,36 @@
485 926 'companyPhone' => \Yatra\Services\SettingsService::getString('company_phone', ''),
486 927 'companyName' => \Yatra\Services\SettingsService::getString('company_name', ''),
487 928 'companyEmail' => \Yatra\Services\SettingsService::getString('company_email', ''),
488 929 'currency' => \Yatra\Services\SettingsService::getCurrency(),
489 - 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'before'),
490 - 'decimalPlaces' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'),
930 + 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
931 + 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
932 + 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(),
491 933 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
492 934 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
493 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 + : [],
494 948 'translations' => $this->getFrontendTranslations(),
495 949 'wishlistEnabled' => \Yatra\Services\SettingsService::wishlistEnabled(),
496 950 ]);
951 +
952 + // Match admin React (`yatra-admin`): register Jed translations for this handle so `wp.i18n` resolves
953 + // strings from PHP/Loco JSON catalogs. Without this, only keys in `translations` above work; the rest
954 + // stay English because the account bundle is not in `yatra-admin`'s Jed file (different script hash).
955 + if (function_exists('wp_set_script_translations')) {
956 + wp_set_script_translations('yatra-account-page', 'yatra', YATRA_PLUGIN_PATH . 'i18n/languages');
957 + }
497 958 }
498 959
499 960 /**
500 961 * Enqueue listing filters JavaScript
@@ -525,8 +986,80 @@
525 986 }
526 987 }
527 988
528 989 /**
990 + * Month and weekday labels for Flatpickr from {@see \WP_Locale} (site language).
991 + *
992 + * @return array<string, mixed>
993 + */
994 + private function buildFlatpickrLocalePayload(): array
995 + {
996 + global $wp_locale;
997 +
998 + $first_day = (int) get_option('start_of_week', 1);
999 + $first_day = max(0, min(6, $first_day));
1000 +
1001 + if (!($wp_locale instanceof \WP_Locale)) {
1002 + return [
1003 + 'firstDayOfWeek' => $first_day,
1004 + ];
1005 + }
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.
1024 + $months_long = [];
1025 + $months_short = [];
1026 + for ($m = 1; $m <= 12; ++$m) {
1027 + $key = sprintf('%02d', $m);
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;
1035 + }
1036 +
1037 + $weekdays_long = [];
1038 + $weekdays_short = [];
1039 + for ($d = 0; $d <= 6; ++$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;
1044 + }
1045 +
1046 + $payload = [
1047 + 'weekdays' => [
1048 + 'shorthand' => $weekdays_short,
1049 + 'longhand' => $weekdays_long,
1050 + ],
1051 + 'months' => [
1052 + 'shorthand' => $months_short,
1053 + 'longhand' => $months_long,
1054 + ],
1055 + 'firstDayOfWeek' => $first_day,
1056 + ];
1057 +
1058 + return apply_filters('yatra_flatpickr_locale', $payload);
1059 + }
1060 +
1061 + /**
529 1062 * Get frontend translations
530 1063 *
531 1064 * @return array
532 1065 */
@@ -547,8 +1080,34 @@
547 1080 'Total Amount' => __('Total Amount', 'yatra'),
548 1081 'Payment Status' => __('Payment Status', 'yatra'),
549 1082 'View Details' => __('View Details', 'yatra'),
550 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 +
551 1110 // Common
552 1111 'Loading...' => __('Loading...', 'yatra'),
553 1112 'No data available' => __('No data available', 'yatra'),
554 1113 'Error loading data' => __('Error loading data', 'yatra'),
@@ -653,8 +1212,100 @@
653 1212 {
654 1213 $basePath = $type === 'css' ? 'assets/css/' : 'assets/js/';
655 1214 $fullPath = YATRA_PLUGIN_PATH . $basePath . $path;
656 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;
657 1308 }
658 1309
659 1310 /**
660 1311 * Stripe Elements (assets/js/stripe.js) expects publishableKey under yatraBookingData.stripe.