PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.14.2
Yatra – Travel Booking & Tour Operator Software v3.0.14.2
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 2.0.10 All 83 releases
yatra / app / Providers / FrontendAssetsProvider.php

FrontendAssetsProvider.php in Yatra – Travel Booking & Tour Operator Software 3.0.14.2, at app/Providers/FrontendAssetsProvider.php

1,328 lines 53.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Providers;
6
7 use Yatra\Core\Modules\ModuleManager;
8 use Yatra\Utils\Logger;
9
10 /**
11 * Frontend Assets Provider
12 *
13 * Handles enqueuing of all frontend-related CSS and JavaScript assets
14 * Centralizes frontend asset management for better organization and maintainability
15 *
16 * @package Yatra\Providers
17 * @since 3.0.0
18 */
19 class FrontendAssetsProvider
20 {
21 /**
22 * Register the provider
23 *
24 * @return void
25 */
26 public function register(): void
27 {
28 add_action('init', [self::class, 'registerCoreFrontendStylesheets'], 5);
29 // Hook into WordPress to enqueue assets
30 add_action('wp_enqueue_scripts', [$this, 'enqueueAssets']);
31 }
32
33 /**
34 * Register Font Awesome (optional) and common.css so block editor + shortcode styles can
35 * depend on `yatra-common` (shared @keyframes: yatra-spin, yatra-shimmer, etc.).
36 */
37 /**
38 * @return list<string>
39 */
40 public static function shortcodeStyleDependencies(): array
41 {
42 self::registerCoreFrontendStylesheets();
43
44 return wp_style_is('yatra-common', 'registered') ? ['yatra-common'] : [];
45 }
46
47 public static function registerCoreFrontendStylesheets(): void
48 {
49 $faPath = YATRA_PLUGIN_PATH . 'assets/vendor/fontawesome/css/all.min.css';
50 if (file_exists($faPath) && !wp_style_is('yatra-fontawesome-6', 'registered')) {
51 wp_register_style(
52 'yatra-fontawesome-6',
53 YATRA_PLUGIN_URL . 'assets/vendor/fontawesome/css/all.min.css',
54 [],
55 '6.7.2.' . filemtime($faPath)
56 );
57 }
58
59 if (wp_style_is('yatra-common', 'registered')) {
60 return;
61 }
62 $path = YATRA_PLUGIN_PATH . 'assets/css/common.css';
63 if (!is_readable($path)) {
64 return;
65 }
66 $commonDeps = wp_style_is('yatra-fontawesome-6', 'registered') ? ['yatra-fontawesome-6'] : [];
67 wp_register_style(
68 'yatra-common',
69 YATRA_PLUGIN_URL . 'assets/css/common.css',
70 $commonDeps,
71 YATRA_VERSION . '.' . filemtime($path)
72 );
73 }
74
75 /**
76 * Enqueue frontend assets based on context
77 *
78 * @return void
79 */
80 public function enqueueAssets(): void
81 {
82 // Only run on frontend
83 if (is_admin()) {
84 return;
85 }
86
87 // Always enqueue common frontend assets
88 $this->enqueueCommonAssets();
89
90 // Enqueue page-specific assets
91 $this->enqueuePageSpecificAssets();
92 }
93
94 /**
95 * Enqueue common frontend assets
96 *
97 * @return void
98 */
99 private function enqueueCommonAssets(): void
100 {
101 // Enqueue common CSS
102 $this->enqueueCommonCss();
103
104 // Enqueue common JavaScript
105 $this->enqueueCommonJs();
106 }
107
108 /**
109 * Enqueue common CSS files
110 *
111 * @return void
112 */
113 private function enqueueCommonCss(): void
114 {
115 $cssFiles = [
116 'common' => 'common.css',
117 'listing' => 'listing.css',
118 'stripe' => 'stripe.css',
119 'trip' => 'trip.css',
120 'activity' => 'activity.css',
121 'destination' => 'destination.css',
122 'yatra-capacity' => 'yatra-capacity.css',
123 'video-player' => 'video-player.css',
124 'tour-viewer' => 'tour-viewer.css',
125 ];
126
127 self::registerCoreFrontendStylesheets();
128
129 if (wp_style_is('yatra-fontawesome-6', 'registered')) {
130 wp_enqueue_style('yatra-fontawesome-6');
131 }
132
133 foreach ($cssFiles as $handle => $filename) {
134 $filePath = YATRA_PLUGIN_PATH . "assets/css/{$filename}";
135 if (!file_exists($filePath)) {
136 continue;
137 }
138 $styleHandle = 'yatra-' . $handle;
139 $ver = YATRA_VERSION . '.' . filemtime($filePath);
140
141 if ($handle === 'common') {
142 if (wp_style_is('yatra-common', 'registered')) {
143 wp_enqueue_style('yatra-common');
144 } else {
145 $deps = wp_style_is('yatra-fontawesome-6', 'registered') ? ['yatra-fontawesome-6'] : [];
146 wp_enqueue_style($styleHandle, YATRA_PLUGIN_URL . "assets/css/{$filename}", $deps, $ver);
147 }
148 continue;
149 }
150
151 $deps = [];
152 if ($handle !== 'common' && wp_style_is('yatra-common', 'registered')) {
153 $deps[] = 'yatra-common';
154 }
155
156 wp_enqueue_style(
157 $styleHandle,
158 YATRA_PLUGIN_URL . "assets/css/{$filename}",
159 $deps,
160 $ver
161 );
162 }
163
164 $this->enqueueFrontendThemeVariables();
165 $this->enqueueFrontendLayoutVariables();
166 }
167
168 /**
169 * Override design tokens from Settings (single primary color → related shades).
170 */
171 private function enqueueFrontendThemeVariables(): void
172 {
173 if (!wp_style_is('yatra-common', 'enqueued')) {
174 return;
175 }
176 $primary = \Yatra\Services\SettingsService::getString(
177 'frontend_primary_color',
178 \Yatra\Utils\FrontendThemeCss::DEFAULT_PRIMARY
179 );
180 $primary = \Yatra\Utils\FrontendThemeCss::sanitizePrimaryColor($primary);
181 if (strtolower($primary) === strtolower(\Yatra\Utils\FrontendThemeCss::DEFAULT_PRIMARY)) {
182 return;
183 }
184 $css = \Yatra\Utils\FrontendThemeCss::buildInlineRootCss($primary);
185 if ($css !== '') {
186 wp_add_inline_style('yatra-common', $css);
187 }
188 }
189
190 /**
191 * Align --yatra-container-max-width with the active theme (theme.json wide/content size or $content_width).
192 */
193 private function enqueueFrontendLayoutVariables(): void
194 {
195 if (!wp_style_is('yatra-common', 'enqueued')) {
196 return;
197 }
198 $fromSetting = \Yatra\Utils\FrontendThemeCss::sanitizeContainerMaxWidthSetting(
199 \Yatra\Services\SettingsService::getString('frontend_container_max_width', '')
200 );
201 $max = $fromSetting !== ''
202 ? $fromSetting
203 : \Yatra\Utils\FrontendThemeCss::resolveThemeContainerMaxWidth();
204 if ($max === null || $max === '') {
205 return;
206 }
207 $maxEsc = esc_attr($max);
208 wp_add_inline_style('yatra-common', ':root{--yatra-container-max-width:' . $maxEsc . ';}');
209 }
210
211 /**
212 * Enqueue common JavaScript files
213 *
214 * @return void
215 */
216 private function enqueueCommonJs(): void
217 {
218 $jsFiles = [
219 'api-helper' => 'api-helper.js',
220 'video-player' => 'video-player.js',
221 'tour-viewer' => 'tour-viewer.js',
222 'listing' => 'listing.js',
223 'listing-filters' => 'listing-filters.js',
224 'stripe' => 'stripe.js',
225 'trip' => 'trip.js',
226 ];
227
228 foreach ($jsFiles as $handle => $filename) {
229 $filePath = YATRA_PLUGIN_PATH . "assets/js/{$filename}";
230 if (file_exists($filePath)) {
231 // Set dependencies
232 $dependencies = ['jquery'];
233 if ($handle === 'trip') {
234 $dependencies[] = 'yatra-api-helper';
235 }
236 // Scripts that call `wp.i18n.__()` for user-facing
237 // strings need wp-i18n as a dependency so the global
238 // exists before they run AND a `wp_set_script_translations`
239 // call below so WordPress loads each one's Jed JSON
240 // catalog (each handle has its own md5-named JSON
241 // because the .po references their respective source
242 // file paths). Add a handle here whenever you wrap a
243 // new string in __() inside its file.
244 $i18nHandles = ['trip', 'listing', 'stripe', 'tour-viewer', 'video-player'];
245 if (in_array($handle, $i18nHandles, true)) {
246 $dependencies[] = 'wp-i18n';
247 }
248
249 wp_enqueue_script(
250 "yatra-{$handle}",
251 YATRA_PLUGIN_URL . "assets/js/{$filename}",
252 $dependencies,
253 YATRA_VERSION . '.' . filemtime($filePath),
254 true
255 );
256
257 // Mirror the wp-i18n dep list above. wp_set_script_translations
258 // tells WordPress where to look for this script's Jed JSON
259 // catalog (path = plugin's i18n/languages/) and the loader
260 // hashes md5(handle src) to find the right file.
261 if (in_array($handle, $i18nHandles, true)
262 && function_exists('wp_set_script_translations')
263 ) {
264 wp_set_script_translations(
265 "yatra-{$handle}",
266 'yatra',
267 YATRA_PLUGIN_PATH . 'i18n/languages'
268 );
269 }
270 }
271 }
272
273 if (\Yatra\Services\SettingsService::wishlistEnabled()) {
274 $wishPath = YATRA_PLUGIN_PATH . 'assets/js/listing-wishlist.js';
275 if (file_exists($wishPath)) {
276 wp_enqueue_script(
277 'yatra-listing-wishlist',
278 YATRA_PLUGIN_URL . 'assets/js/listing-wishlist.js',
279 ['jquery', 'wp-i18n'],
280 YATRA_VERSION . '.' . filemtime($wishPath),
281 true
282 );
283 if (function_exists('wp_set_script_translations')) {
284 wp_set_script_translations(
285 'yatra-listing-wishlist',
286 'yatra',
287 YATRA_PLUGIN_PATH . 'i18n/languages'
288 );
289 }
290 // Wishlist "Login" should send guests to the configured
291 // My Account page (Settings → Permalink slug), not the raw
292 // wp-login.php screen. Fall back to wp_login_url() only when
293 // no account base is configured so the button never dead-ends.
294 $yatraAccountBase = \Yatra\Services\SettingsService::getAccountBase();
295 $yatraAccountLoginUrl = $yatraAccountBase !== ''
296 ? home_url('/' . trim($yatraAccountBase, '/') . '/')
297 : wp_login_url();
298 wp_localize_script('yatra-listing-wishlist', 'yatraWishlistConfig', [
299 'enabled' => true,
300 'restUrl' => rest_url('yatra/v1'),
301 'nonce' => wp_create_nonce('wp_rest'),
302 'isLoggedIn' => is_user_logged_in(),
303 'loginUrl' => $yatraAccountLoginUrl,
304 'i18n' => [
305 'loginRequired' => __('Login Required', 'yatra'),
306 'loginPrompt' => __('Please login to save trips to your wishlist.', 'yatra'),
307 'login' => __('Login', 'yatra'),
308 'cancel' => __('Cancel', 'yatra'),
309 'genericError' => __('An error occurred. Please try again.', 'yatra'),
310 'saved' => __('Trip saved to wishlist', 'yatra'),
311 'removed' => __('Trip removed from wishlist', 'yatra'),
312 'saveFailed' => __('Failed to save trip', 'yatra'),
313 'removeFailed' => __('Failed to remove trip', 'yatra'),
314 'addAria' => __('Add to favorites', 'yatra'),
315 'removeAria' => __('Remove from favorites', 'yatra'),
316 ],
317 ]);
318 }
319 }
320 }
321
322 /**
323 * Enqueue page-specific assets
324 *
325 * @return void
326 */
327 private function enqueuePageSpecificAssets(): void
328 {
329 if (yatra_is_account_page()) {
330 $this->enqueueAccountAssets();
331
332 return;
333 }
334
335 // Check current page context and enqueue specific assets using helper functions
336 if (yatra_is_trip_listing()) {
337 $this->enqueueTripListingAssets();
338 }
339
340 if (yatra_is_single_trip()) {
341 $this->enqueueTripDetailAssets();
342 }
343
344 if (yatra_is_activity_listing()) {
345 $this->enqueueActivityListingAssets();
346 }
347
348 if (yatra_is_destination_listing()) {
349 $this->enqueueDestinationListingAssets();
350 }
351
352 if (yatra_is_booking_page()) {
353 $this->enqueueBookingAssets();
354 }
355
356 if (yatra_is_taxonomy_page()) {
357 // Taxonomy pages use the same assets as trip listing
358 $this->enqueueTripListingAssets();
359 }
360 }
361
362 /**
363 * Enqueue trip listing specific assets
364 *
365 * @return void
366 */
367 private function enqueueTripListingAssets(): void
368 {
369 $this->enqueueListingFiltersJs();
370 }
371
372 /**
373 * Enqueue trip detail specific assets
374 *
375 * @return void
376 */
377 private function enqueueTripDetailAssets(): void
378 {
379 // Enqueue booking assets for trip detail pages (booking forms)
380 $bookingJs = YATRA_PLUGIN_PATH . 'assets/js/booking.js';
381 if (file_exists($bookingJs)) {
382 wp_enqueue_script(
383 'yatra-booking',
384 YATRA_PLUGIN_URL . 'assets/js/booking.js',
385 ['jquery', 'wp-i18n'],
386 YATRA_VERSION . '.' . filemtime($bookingJs),
387 true
388 );
389 if (function_exists('wp_set_script_translations')) {
390 wp_set_script_translations(
391 'yatra-booking',
392 'yatra',
393 YATRA_PLUGIN_PATH . 'i18n/languages'
394 );
395 }
396 }
397
398 // International phone-number widget (country flag + dial code) used by
399 // the booking form's tel fields.
400 $this->enqueuePhoneInputAssets();
401 $this->enqueueCountrySelectAssets();
402
403 // Mobile sticky-sidebar + flatpickr init for the single-trip page. Lives in a
404 // dedicated file rather than as inline <script> in the partial because
405 // WordPress core's `convert_chars` filter (hooked to the_content) rewrites the
406 // `&&` operators inside inline scripts as `&#038;&#038;` — JS parsers don't
407 // decode HTML entities inside <script>, producing a SyntaxError. As a properly
408 // enqueued external file, the source is delivered verbatim.
409 $sidebarJs = YATRA_PLUGIN_PATH . 'assets/js/single-trip-sidebar.js';
410 if (file_exists($sidebarJs)) {
411 wp_enqueue_script(
412 'yatra-single-trip-sidebar',
413 YATRA_PLUGIN_URL . 'assets/js/single-trip-sidebar.js',
414 ['yatra-trip', 'wp-i18n'], // depends on window.yatraTripData from yatra-trip
415 YATRA_VERSION . '.' . filemtime($sidebarJs),
416 true
417 );
418 if (function_exists('wp_set_script_translations')) {
419 wp_set_script_translations(
420 'yatra-single-trip-sidebar',
421 'yatra',
422 YATRA_PLUGIN_PATH . 'i18n/languages'
423 );
424 }
425 }
426
427 // Localize trip page data for JS (trip.js, booking.js)
428 global $trip;
429
430 $permalink_structure = get_option('permalink_structure') ?: '';
431 $is_plain = empty($permalink_structure);
432
433 $trip_id = null;
434 $trip_slug = null;
435 $has_trip = isset($trip) && is_object($trip) && isset($trip->id);
436 if ($has_trip) {
437 $trip_id = (int) $trip->id;
438 }
439 if ($has_trip && isset($trip->slug)) {
440 $trip_slug = $trip->slug;
441 }
442
443 $tripData = [
444 'apiUrl' => rest_url('yatra/v1'),
445 'restUrl' => rest_url(),
446 'siteUrl' => site_url(),
447 'bookingBase' => \Yatra\Services\SettingsService::getBookingBase(),
448 'permalinkStructure' => $is_plain ? 'plain' : $permalink_structure,
449 'nonce' => wp_create_nonce('wp_rest'),
450 'tripId' => $trip_id,
451 'tripSlug' => $trip_slug,
452 'wishlistEnabled' => \Yatra\Services\SettingsService::wishlistEnabled(),
453 'isLoggedIn' => is_user_logged_in(),
454 // Wishlist "Login" (guest) → the configured My Account page
455 // (Settings → Permalink slug), NOT wp-login.php. Without this key
456 // trip.js falls back to a hardcoded '/wp-login.php'. Falls back to
457 // wp_login_url() only when no account base slug is configured.
458 'loginUrl' => (function () {
459 $base = \Yatra\Services\SettingsService::getAccountBase();
460 return $base !== '' ? home_url('/' . trim($base, '/') . '/') : wp_login_url();
461 })(),
462 // Regional settings
463 'timezone' => \Yatra\Services\SettingsService::getString('timezone', 'UTC'),
464 'dateFormat' => \Yatra\Services\SettingsService::getString('date_format', 'Y-m-d'),
465 'timeFormat' => \Yatra\Services\SettingsService::getString('time_format', 'H:i'),
466 // Currency/settings
467 'currency' => \Yatra\Services\SettingsService::getCurrency(),
468 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
469 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
470 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(),
471 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
472 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
473 'basePrice' => 0.0,
474 'currencySymbol' => function_exists('yatra_get_currency_symbol')
475 ? yatra_get_currency_symbol(\Yatra\Services\SettingsService::getCurrency())
476 : '$',
477 'availabilityDates' => [],
478 'groupDiscountsUrl' => rest_url('yatra/v1/discounts/group-discounts'),
479 'dynamicPricingDisplay' => apply_filters('yatra_get_dynamic_pricing_display_settings', [
480 'show_original_price' => true,
481 'show_savings_badge' => true,
482 'show_urgency_messages' => false,
483 ]),
484 'pricingType' => 'regular',
485 'sidebarAvailability' => [],
486 'sidebarGroupDiscounts' => [],
487 'flatpickrLocale' => $this->buildFlatpickrLocalePayload(),
488 ];
489
490 if ($has_trip) {
491 if (function_exists('yatra_single_trip_calculate_base_price')) {
492 $pricing_data = yatra_single_trip_calculate_base_price($trip);
493 $tripData['basePrice'] = (float) ($pricing_data['base_price'] ?? 0);
494 }
495 if (method_exists($trip, 'getAvailabilityDates')) {
496 $tripData['availabilityDates'] = array_values(array_filter(array_map(static function ($avail) {
497 if (is_object($avail)) {
498 return $avail->departure_date ?? $avail->date ?? null;
499 }
500 if (is_array($avail)) {
501 return $avail['departure_date'] ?? $avail['date'] ?? null;
502 }
503
504 return null;
505 }, $trip->getAvailabilityDates())));
506 }
507 if (function_exists('yatra_single_trip_get_client_booking_payload')) {
508 $bookingPayload = yatra_single_trip_get_client_booking_payload($trip);
509 $tripData['pricingType'] = $bookingPayload['pricingType'];
510 $tripData['sidebarAvailability'] = $bookingPayload['sidebarAvailability'];
511 $tripData['sidebarGroupDiscounts'] = $bookingPayload['sidebarGroupDiscounts'];
512 }
513 }
514
515 wp_localize_script('yatra-trip', 'yatraTripData', $tripData);
516 $tripTitle = '';
517 if ($has_trip && isset($trip->title)) {
518 $tripTitle = is_string($trip->title) ? $trip->title : '';
519 }
520 wp_localize_script('yatra-booking', 'yatraBookingData', array_merge(
521 $tripData,
522 $this->getStripeFrontendBookingPayload(),
523 [
524 'tripTitle' => $tripTitle !== '' ? $tripTitle : ($tripData['tripTitle'] ?? 'Trip Booking'),
525 // booking page expects these keys; leave placeholders if not set on trip view
526 'isRemainingPayment' => false,
527 'remainingAmount' => 0,
528 'totalAmount' => 0,
529 'amountPaid' => 0,
530 // Booking-scoped CSRF nonce — covers BOTH logged-in and
531 // guest checkouts. The REST endpoint's public
532 // permission_callback intentionally bypasses the WP REST
533 // cookie/nonce check (so guests can hit it at all);
534 // this token is what gates the actual write. The JS
535 // forwards it in the `X-Yatra-Booking-Nonce` header on
536 // every booking-create / booking-update POST.
537 'bookingNonce' => wp_create_nonce('yatra_booking_action'),
538 ]
539 ));
540 }
541
542 /**
543 * Enqueue the country selector widget (assets/js/country-select.js +
544 * assets/css/country-select.css).
545 *
546 * Upgrades every `type => country` field (Country, Nationality, on both the
547 * contact and traveler sections) into a searchable dropdown showing the
548 * national flag, matching the phone country-code control. Purely additive:
549 * the underlying <select> still renders and submits, so a site that never
550 * loads this script behaves exactly as before.
551 *
552 * Idempotent, so it is safe to call from every path that renders the form.
553 *
554 * @return void
555 */
556 private function enqueueCountrySelectAssets(): void
557 {
558 if (wp_script_is('yatra-country-select', 'enqueued')) {
559 return;
560 }
561
562 $css = YATRA_PLUGIN_PATH . 'assets/css/country-select.css';
563 if (file_exists($css)) {
564 wp_enqueue_style(
565 'yatra-country-select',
566 YATRA_PLUGIN_URL . 'assets/css/country-select.css',
567 [],
568 YATRA_VERSION . '.' . filemtime($css)
569 );
570 }
571
572 $js = YATRA_PLUGIN_PATH . 'assets/js/country-select.js';
573 if (!file_exists($js)) {
574 return;
575 }
576
577 wp_enqueue_script(
578 'yatra-country-select',
579 YATRA_PLUGIN_URL . 'assets/js/country-select.js',
580 [],
581 YATRA_VERSION . '.' . filemtime($js),
582 true
583 );
584
585 wp_localize_script('yatra-country-select', 'yatraCountrySelectData', [
586 'i18n' => [
587 'search' => __('Search country', 'yatra'),
588 'noResults' => __('No matches', 'yatra'),
589 ],
590 ]);
591 }
592
593 /**
594 * Enqueue the international phone-number widget (assets/js/phone-input.js +
595 * assets/css/phone-input.css) and localize its country + dial-code dataset.
596 *
597 * Self-contained (reads its own `yatraPhoneData` global) and idempotent, so
598 * it can be called from every path that renders the booking form. Country
599 * data is the single source of truth in {@see FormatHelper}.
600 *
601 * @return void
602 */
603 private function enqueuePhoneInputAssets(): void
604 {
605 if (wp_script_is('yatra-phone-input', 'enqueued')) {
606 return;
607 }
608
609 $css = YATRA_PLUGIN_PATH . 'assets/css/phone-input.css';
610 if (file_exists($css)) {
611 wp_enqueue_style(
612 'yatra-phone-input',
613 YATRA_PLUGIN_URL . 'assets/css/phone-input.css',
614 [],
615 YATRA_VERSION . '.' . filemtime($css)
616 );
617 }
618
619 $js = YATRA_PLUGIN_PATH . 'assets/js/phone-input.js';
620 if (!file_exists($js)) {
621 return;
622 }
623 wp_enqueue_script(
624 'yatra-phone-input',
625 YATRA_PLUGIN_URL . 'assets/js/phone-input.js',
626 [],
627 YATRA_VERSION . '.' . filemtime($js),
628 true
629 );
630 wp_localize_script('yatra-phone-input', 'yatraPhoneData', [
631 'countries' => \Yatra\Helpers\FormatHelper::getPhoneCountries(),
632 'priority' => \Yatra\Helpers\FormatHelper::getPhonePriority(),
633 'i18n' => [
634 'search' => __('Search country', 'yatra'),
635 'noResults' => __('No matches', 'yatra'),
636 ],
637 ]);
638 }
639
640 /**
641 * Enqueue activity listing specific assets
642 *
643 * @return void
644 */
645 private function enqueueActivityListingAssets(): void
646 {
647 // Activity listing specific assets
648 $filePath = YATRA_PLUGIN_PATH . 'assets/css/activity.css';
649 if (file_exists($filePath)) {
650 wp_enqueue_style(
651 'yatra-activity-listing',
652 YATRA_PLUGIN_URL . 'assets/css/activity.css',
653 [],
654 YATRA_VERSION . '.' . filemtime($filePath)
655 );
656 }
657 }
658
659 /**
660 * Enqueue destination listing specific assets
661 *
662 * @return void
663 */
664 private function enqueueDestinationListingAssets(): void
665 {
666 // Destination listing specific assets
667 $filePath = YATRA_PLUGIN_PATH . 'assets/css/destination.css';
668 if (file_exists($filePath)) {
669 wp_enqueue_style(
670 'yatra-destination-listing',
671 YATRA_PLUGIN_URL . 'assets/css/destination.css',
672 [],
673 YATRA_VERSION . '.' . filemtime($filePath)
674 );
675 }
676 }
677
678 /**
679 * Enqueue booking specific assets
680 *
681 * @return void
682 */
683 private function enqueueBookingAssets(): void
684 {
685 // Enqueue booking-specific CSS
686 $bookingCss = YATRA_PLUGIN_PATH . 'assets/css/booking.css';
687 if (file_exists($bookingCss)) {
688 wp_enqueue_style(
689 'yatra-booking',
690 YATRA_PLUGIN_URL . 'assets/css/booking.css',
691 ['yatra-common'],
692 YATRA_VERSION . '.' . filemtime($bookingCss)
693 );
694 }
695
696 // Flatpickr — used by booking.js to upgrade Date-of-Birth (and other
697 // date) inputs to a picker with fast, typeable year navigation. The
698 // single-trip page already ships flatpickr (see single-trip.php); the
699 // dedicated booking page did not, so enqueue it here. booking.js
700 // self-guards on `typeof flatpickr`, so this is safe either way.
701 wp_enqueue_style(
702 'yatra-flatpickr',
703 'https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css',
704 [],
705 YATRA_VERSION
706 );
707 wp_enqueue_script(
708 'yatra-flatpickr',
709 'https://cdn.jsdelivr.net/npm/flatpickr',
710 [],
711 YATRA_VERSION,
712 true
713 );
714
715 // Enqueue booking-specific JavaScript
716 $bookingJs = YATRA_PLUGIN_PATH . 'assets/js/booking.js';
717 if (file_exists($bookingJs)) {
718 // booking.js renders user-facing strings via wp.i18n.__() (the
719 // "Processing..." button label and the per-gateway info messages
720 // shown when a payment method is selected). It therefore needs
721 // wp-i18n as a dependency AND wp_set_script_translations so its
722 // Jed catalog loads — mirroring the trip-detail enqueue above.
723 // Without these, those strings stay English on the standalone
724 // booking page regardless of site locale.
725 wp_enqueue_script(
726 'yatra-booking',
727 YATRA_PLUGIN_URL . 'assets/js/booking.js',
728 ['jquery', 'yatra-flatpickr', 'wp-i18n'],
729 YATRA_VERSION . '.' . filemtime($bookingJs),
730 true
731 );
732 if (function_exists('wp_set_script_translations')) {
733 wp_set_script_translations(
734 'yatra-booking',
735 'yatra',
736 YATRA_PLUGIN_PATH . 'i18n/languages'
737 );
738 }
739 }
740
741 // International phone-number widget (country flag + dial code).
742 $this->enqueuePhoneInputAssets();
743 $this->enqueueCountrySelectAssets();
744
745 // Load each available gateway's own client scripts on the checkout page
746 // (e.g. Square Web Payments SDK + square.js, Authorize.Net Accept.js +
747 // its handler, Razorpay SDK + its handler). Every gateway's
748 // enqueueScripts() self-guards on isAvailable(), so only enabled +
749 // configured gateways load anything. This call was previously missing,
750 // so Pro gateways that render an inline card form shipped no JS to
751 // checkout and clicking "Pay" just span the button forever. It is
752 // additive and safe for the others: Stripe's enqueueScripts() is a
753 // no-op (Stripe is loaded via enqueueCommonJs), and PayPal/Pay Later
754 // have no client scripts.
755 if (class_exists(\Yatra\PaymentGateways\PaymentGatewayRegistry::class)) {
756 \Yatra\PaymentGateways\PaymentGatewayRegistry::getInstance()->enqueueScripts();
757 }
758
759 // Localize booking data for booking.js
760 $permalink_structure = get_option('permalink_structure') ?: '';
761 $is_plain = empty($permalink_structure);
762
763 $deposit_pct_store = (int) \Yatra\Services\SettingsService::get('deposit_percentage', 20);
764 $partial_pct_store = (int) \Yatra\Services\SettingsService::get('partial_payment_percentage', 30);
765 $deposit_pct_resolved = (int) apply_filters('yatra_deposit_percentage', $deposit_pct_store);
766 $partial_pct_resolved = (int) apply_filters('yatra_partial_payment_percentage', $partial_pct_store);
767 $flexible_payments_enabled = (bool) apply_filters('yatra_flexible_payments_enabled', false);
768 $flexible_module_on = class_exists(ModuleManager::class)
769 ? ModuleManager::isModuleEnabled('flexible_payments')
770 : false;
771
772 Logger::debug('Yatra booking localize: flexible payment snapshot', [
773 'context' => 'booking_localize',
774 'flexible_payments_enabled' => $flexible_payments_enabled,
775 'flexible_payments_module' => $flexible_module_on,
776 'partial_payment_setting' => (bool) \Yatra\Services\SettingsService::get('partial_payment', false),
777 'deposit_required_setting' => (bool) \Yatra\Services\SettingsService::get('deposit_required', false),
778 'deposit_percentage_store' => $deposit_pct_store,
779 'partial_percentage_store' => $partial_pct_store,
780 'deposit_percentage_resolved' => $deposit_pct_resolved,
781 'partial_percentage_resolved' => $partial_pct_resolved,
782 ]);
783
784 $bookingData = [
785 'apiUrl' => rest_url('yatra/v1'),
786 'restUrl' => rest_url(),
787 'siteUrl' => site_url(),
788 'bookingBase' => \Yatra\Services\SettingsService::getBookingBase(),
789 'permalinkStructure' => $is_plain ? 'plain' : $permalink_structure,
790 'nonce' => wp_create_nonce('wp_rest'),
791 // Booking-scoped CSRF nonce. See enqueueTripDetailAssets()
792 // for the rationale: the booking REST endpoint bypasses
793 // the WP REST cookie/nonce check (so guests can use it),
794 // and this token is what gates the actual booking write.
795 'bookingNonce' => wp_create_nonce('yatra_booking_action'),
796 'currency' => \Yatra\Services\SettingsService::getCurrency(),
797 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
798 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
799 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(),
800 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
801 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
802 // Payment gateways data
803 'paymentGateways' => $this->sanitizeGatewayConfigsForFrontend(apply_filters('yatra_payment_gateways', \Yatra\Services\SettingsService::get('payment_gateways', []))),
804 'paymentMethods' => \Yatra\Services\SettingsService::get('payment_methods', []),
805 'paymentTestMode' => \Yatra\Services\SettingsService::get('payment_test_mode', false),
806 'partialPayment' => \Yatra\Services\SettingsService::get('partial_payment', false),
807 'partialPaymentPercentage' => \Yatra\Services\SettingsService::get('partial_payment_percentage', 0),
808 // Flexible payments (Pro) — keep these keys stable for frontend booking.js.
809 // Values are resolved via generic filters so Pro can override without hard-coding premium logic here.
810 'depositRequired' => (bool) \Yatra\Services\SettingsService::get('deposit_required', false),
811 'depositPercentage' => $deposit_pct_resolved,
812 'partialPercentage' => $partial_pct_resolved,
813 'gatewayOrder' => \Yatra\Services\SettingsService::get('gateway_order', []),
814 'autoConfirmPayLater' => \Yatra\Services\SettingsService::get('auto_confirm_pay_later', true),
815 'allowWaitlist' => \Yatra\Services\SettingsService::isEnabled('allow_waitlist'),
816 'waitlistAutoConfirm' => \Yatra\Services\SettingsService::isEnabled('waitlist_auto_confirm'),
817 'gateways' => $this->getGatewayFrontendConfigs(),
818 'enabledGateways' => $this->sanitizeGatewayConfigsForFrontend(\Yatra\Services\SettingsService::get('payment_gateways', [])),
819 // Server-side translated UI strings for booking.js. PHP __() resolves via .mo
820 // (reliable), so these stay translatable even when the JS-translation JSON
821 // chain (wp_set_script_translations) doesn't load on a given setup.
822 'i18n' => [
823 'complete_booking' => __('Complete Booking', 'yatra'),
824 'pay_now' => __('Pay Now', 'yatra'),
825 ],
826 ];
827
828 $bookingData = array_merge($bookingData, $this->getStripeFrontendBookingPayload());
829
830 wp_localize_script('yatra-booking', 'yatraBookingData', $bookingData);
831 }
832
833 /**
834 * Enqueue account page specific assets
835 *
836 * @return void
837 */
838 private function enqueueAccountAssets(): void
839 {
840 // Account bundle shares admin Vite chunks; CSS is extracted to admin/dist/css (ES modules do not auto-load it).
841 $reactVendorCss = YATRA_PLUGIN_PATH . 'assets/admin/dist/css/react-vendor.css';
842 if (file_exists($reactVendorCss)) {
843 wp_enqueue_style(
844 'yatra-account-react-vendor',
845 YATRA_PLUGIN_URL . 'assets/admin/dist/css/react-vendor.css',
846 [],
847 YATRA_VERSION . '.' . filemtime($reactVendorCss)
848 );
849 }
850
851 $accountUiCss = YATRA_PLUGIN_PATH . 'assets/admin/dist/css/index.css';
852 $accountUiDeps = file_exists($reactVendorCss) ? ['yatra-account-react-vendor'] : [];
853 if (file_exists($accountUiCss)) {
854 wp_enqueue_style(
855 'yatra-account-ui',
856 YATRA_PLUGIN_URL . 'assets/admin/dist/css/index.css',
857 $accountUiDeps,
858 YATRA_VERSION . '.' . filemtime($accountUiCss)
859 );
860 }
861
862 // Vite build outputs to assets/dist/js/account-page.js (ES module + shared chunks).
863 $accountJs = YATRA_PLUGIN_PATH . 'assets/dist/js/account-page.js';
864 if (!file_exists($accountJs)) {
865 return;
866 }
867
868 wp_enqueue_script(
869 'yatra-account-page',
870 YATRA_PLUGIN_URL . 'assets/dist/js/account-page.js',
871 [],
872 YATRA_VERSION . '.' . filemtime($accountJs),
873 true
874 );
875
876 wp_script_add_data('yatra-account-page', 'type', 'module');
877
878 wp_localize_script('yatra-account-page', 'yatraAccountPage', [
879 'apiUrl' => rest_url('yatra/v1'),
880 'nonce' => wp_create_nonce('wp_rest'),
881 'userId' => get_current_user_id(),
882 'siteUrl' => site_url(),
883 'logoutUrl' => wp_logout_url(home_url('/')),
884 'companyPhone' => \Yatra\Services\SettingsService::getString('company_phone', ''),
885 'companyName' => \Yatra\Services\SettingsService::getString('company_name', ''),
886 'companyEmail' => \Yatra\Services\SettingsService::getString('company_email', ''),
887 'currency' => \Yatra\Services\SettingsService::getCurrency(),
888 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
889 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
890 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(),
891 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
892 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
893 'locale' => get_locale(),
894 // Global date/time format so the customer account pages render dates
895 // in the operator's configured format (Settings → General), not a
896 // hardcoded browser style. Keys mirror what the admin app receives.
897 'date_format' => \Yatra\Services\SettingsService::getString('date_format', 'Y-m-d'),
898 'time_format' => \Yatra\Services\SettingsService::getString('time_format', 'H:i'),
899 'timezone' => \Yatra\Services\SettingsService::getString('timezone', 'UTC'),
900 // Full ISO country map (code => name) so the account profile can show
901 // full country names and render the country dropdown. Mirrors the
902 // admin (`yatraAdmin.countries`); honours the `yatra_countries_list` filter.
903 'countries' => class_exists('\\Yatra\\Helpers\\FormatHelper')
904 ? \Yatra\Helpers\FormatHelper::getCountries()
905 : [],
906 'translations' => $this->getFrontendTranslations(),
907 'wishlistEnabled' => \Yatra\Services\SettingsService::wishlistEnabled(),
908 ]);
909
910 // Match admin React (`yatra-admin`): register Jed translations for this handle so `wp.i18n` resolves
911 // strings from PHP/Loco JSON catalogs. Without this, only keys in `translations` above work; the rest
912 // stay English because the account bundle is not in `yatra-admin`'s Jed file (different script hash).
913 if (function_exists('wp_set_script_translations')) {
914 wp_set_script_translations('yatra-account-page', 'yatra', YATRA_PLUGIN_PATH . 'i18n/languages');
915 }
916 }
917
918 /**
919 * Enqueue listing filters JavaScript
920 *
921 * @return void
922 */
923 private function enqueueListingFiltersJs(): void
924 {
925 $filtersJs = YATRA_PLUGIN_PATH . 'assets/js/listing-filters.js';
926 if (file_exists($filtersJs)) {
927 wp_enqueue_script(
928 'yatra-listing-filters',
929 YATRA_PLUGIN_URL . 'assets/js/listing-filters.js',
930 ['jquery'],
931 YATRA_VERSION . '.' . filemtime($filtersJs),
932 true
933 );
934
935 // Add currency formatting function
936 wp_add_inline_script('yatra-listing-filters', "
937 window.yatra_format_price = function(amount) {
938 if (!amount || amount == 0) return '" . esc_js(__('Contact for pricing', 'yatra')) . "';
939 const currency = window.yatraSettings?.currency || 'USD';
940 const symbol = window.yatraSettings?.currencySymbol || '$';
941 return symbol + amount.toLocaleString();
942 };
943 ");
944 }
945 }
946
947 /**
948 * Month and weekday labels for Flatpickr from {@see \WP_Locale} (site language).
949 *
950 * @return array<string, mixed>
951 */
952 private function buildFlatpickrLocalePayload(): array
953 {
954 global $wp_locale;
955
956 $first_day = (int) get_option('start_of_week', 1);
957 $first_day = max(0, min(6, $first_day));
958
959 if (!($wp_locale instanceof \WP_Locale)) {
960 return [
961 'firstDayOfWeek' => $first_day,
962 ];
963 }
964
965 // IMPORTANT — `month_abbrev` and `weekday_abbrev` are keyed by the
966 // TRANSLATED LONG NAME, not by a numeric index:
967 //
968 // $wp_locale->month['01'] = 'January' (or 'जनवरी', 'enero'…)
969 // $wp_locale->month_abbrev['January'] = 'Jan' (or 'जन', 'ene'…)
970 //
971 // An earlier version of this code mistakenly indexed
972 // month_abbrev by '01'..'12' / weekday_abbrev by 0..6, which
973 // ALWAYS returned null → flatpickr's locale.months.shorthand
974 // shipped as an array of empty strings → the `M` token in any
975 // altFormat rendered as nothing. Net effect: a date set to
976 // "19 May 2026" displayed as "19 2026" (no month) under any
977 // non-en_US locale that exposed the bug.
978 //
979 // WP_Locale exposes get_month_abbrev() / get_weekday_abbrev()
980 // which take the long name and do the right lookup. We use
981 // those so the indexing rule lives inside core, not here.
982 $months_long = [];
983 $months_short = [];
984 for ($m = 1; $m <= 12; ++$m) {
985 $key = sprintf('%02d', $m);
986 $long = $wp_locale->month[$key] ?? '';
987 $short = $long !== '' ? (string) $wp_locale->get_month_abbrev($long) : '';
988 $months_long[] = $long;
989 // Final fallback to the long name if the locale has no
990 // abbreviated form — better than shipping an empty string
991 // that flatpickr would render as blank.
992 $months_short[] = $short !== '' ? $short : $long;
993 }
994
995 $weekdays_long = [];
996 $weekdays_short = [];
997 for ($d = 0; $d <= 6; ++$d) {
998 $long = $wp_locale->weekday[$d] ?? '';
999 $short = $long !== '' ? (string) $wp_locale->get_weekday_abbrev($long) : '';
1000 $weekdays_long[] = $long;
1001 $weekdays_short[] = $short !== '' ? $short : $long;
1002 }
1003
1004 $payload = [
1005 'weekdays' => [
1006 'shorthand' => $weekdays_short,
1007 'longhand' => $weekdays_long,
1008 ],
1009 'months' => [
1010 'shorthand' => $months_short,
1011 'longhand' => $months_long,
1012 ],
1013 'firstDayOfWeek' => $first_day,
1014 ];
1015
1016 return apply_filters('yatra_flatpickr_locale', $payload);
1017 }
1018
1019 /**
1020 * Get frontend translations
1021 *
1022 * @return array
1023 */
1024 private function getFrontendTranslations(): array
1025 {
1026 return [
1027 // Account page
1028 'My Account' => __('My Account', 'yatra'),
1029 'My Bookings' => __('My Bookings', 'yatra'),
1030 'Account Settings' => __('Account Settings', 'yatra'),
1031 'Logout' => __('Logout', 'yatra'),
1032 'Login' => __('Login', 'yatra'),
1033 'Register' => __('Register', 'yatra'),
1034
1035 // Booking related
1036 'Booking Details' => __('Booking Details', 'yatra'),
1037 'Booking Status' => __('Booking Status', 'yatra'),
1038 'Total Amount' => __('Total Amount', 'yatra'),
1039 'Payment Status' => __('Payment Status', 'yatra'),
1040 'View Details' => __('View Details', 'yatra'),
1041
1042 // Traveler / contact / emergency field labels on the account page.
1043 // Keep in sync with the `fieldLabel()` map in account/BookingDetails.tsx.
1044 'First Name' => __('First Name', 'yatra'),
1045 'Last Name' => __('Last Name', 'yatra'),
1046 'Full Name' => __('Full Name', 'yatra'),
1047 'Name' => __('Name', 'yatra'),
1048 'Email' => __('Email', 'yatra'),
1049 'Phone' => __('Phone', 'yatra'),
1050 'Mobile' => __('Mobile', 'yatra'),
1051 'Date of Birth' => __('Date of Birth', 'yatra'),
1052 'Gender' => __('Gender', 'yatra'),
1053 'Nationality' => __('Nationality', 'yatra'),
1054 'Country' => __('Country', 'yatra'),
1055 'Address' => __('Address', 'yatra'),
1056 'City' => __('City', 'yatra'),
1057 'State' => __('State', 'yatra'),
1058 'Postal Code' => __('Postal Code', 'yatra'),
1059 'Zip Code' => __('Zip Code', 'yatra'),
1060 'Passport' => __('Passport', 'yatra'),
1061 'Passport Number' => __('Passport Number', 'yatra'),
1062 'Passport Expiry' => __('Passport Expiry', 'yatra'),
1063 'Dietary Requirements' => __('Dietary Requirements', 'yatra'),
1064 'Special Requirements' => __('Special Requirements', 'yatra'),
1065 'Relationship' => __('Relationship', 'yatra'),
1066 'Company' => __('Company', 'yatra'),
1067
1068 // Common
1069 'Loading...' => __('Loading...', 'yatra'),
1070 'No data available' => __('No data available', 'yatra'),
1071 'Error loading data' => __('Error loading data', 'yatra'),
1072 'Please try again' => __('Please try again', 'yatra'),
1073
1074 // Currency and pricing
1075 'Contact for pricing' => __('Contact for pricing', 'yatra'),
1076 'Free' => __('Free', 'yatra'),
1077 'Price' => __('Price', 'yatra'),
1078
1079 // Trip related
1080 'Trip Details' => __('Trip Details', 'yatra'),
1081 'Duration' => __('Duration', 'yatra'),
1082 'Difficulty' => __('Difficulty', 'yatra'),
1083 'Group Size' => __('Group Size', 'yatra'),
1084
1085 // Navigation
1086 'Home' => __('Home', 'yatra'),
1087 'Trips' => __('Trips', 'yatra'),
1088 'Destinations' => __('Destinations', 'yatra'),
1089 'Activities' => __('Activities', 'yatra'),
1090 'About Us' => __('About Us', 'yatra'),
1091 'Contact' => __('Contact', 'yatra'),
1092 ];
1093 }
1094
1095 /**
1096 * Enqueue assets for specific shortcodes
1097 *
1098 * @param array $shortcodes Array of shortcodes that need assets
1099 * @return void
1100 */
1101 public function enqueueShortcodeAssets(array $shortcodes): void
1102 {
1103 global $post;
1104
1105 if (!$post || !has_shortcode($post->post_content, $shortcodes)) {
1106 return;
1107 }
1108
1109 // Enqueue common frontend assets for shortcodes
1110 $this->enqueueCommonAssets();
1111
1112 // Shortcode-specific assets can be added here based on $shortcodes array
1113 foreach ($shortcodes as $shortcode) {
1114 switch ($shortcode) {
1115 case 'yatra_trip_listing':
1116 $this->enqueueTripListingAssets();
1117 break;
1118 case 'yatra_cart':
1119 case 'yatra_checkout':
1120 $this->enqueueBookingAssets();
1121 break;
1122 case 'yatra_my_account':
1123 $this->enqueueAccountAssets();
1124 break;
1125 }
1126 }
1127 }
1128
1129 /**
1130 * Enqueue assets conditionally based on custom conditions
1131 *
1132 * @param callable $condition Function that returns true if assets should be enqueued
1133 * @return void
1134 */
1135 public function enqueueConditionalAssets(callable $condition): void
1136 {
1137 if ($condition()) {
1138 $this->enqueueAssets();
1139 }
1140 }
1141
1142 /**
1143 * Get asset URL with versioning
1144 *
1145 * @param string $path Relative path to asset
1146 * @param string $type 'css' or 'js'
1147 * @return string Asset URL or empty string if file doesn't exist
1148 */
1149 public function getAssetUrl(string $path, string $type = 'css'): string
1150 {
1151 $basePath = $type === 'css' ? 'assets/css/' : 'assets/js/';
1152 $fullPath = YATRA_PLUGIN_PATH . $basePath . $path;
1153
1154 if (!file_exists($fullPath)) {
1155 return '';
1156 }
1157
1158 $version = YATRA_VERSION . '.' . filemtime($fullPath);
1159 return YATRA_PLUGIN_URL . $basePath . $path . '?ver=' . $version;
1160 }
1161
1162 /**
1163 * Check if asset file exists
1164 *
1165 * @param string $path Relative path to asset
1166 * @param string $type 'css' or 'js'
1167 * @return bool
1168 */
1169 public function assetExists(string $path, string $type = 'css'): bool
1170 {
1171 $basePath = $type === 'css' ? 'assets/css/' : 'assets/js/';
1172 $fullPath = YATRA_PLUGIN_PATH . $basePath . $path;
1173 return file_exists($fullPath);
1174 }
1175
1176 /**
1177 * Strip secret credentials from per-gateway config before it is localized
1178 * into the page (yatraBookingData). The stored payment_gateways option
1179 * holds private keys / access tokens that must NEVER reach the browser; the
1180 * checkout scripts only ever read public values (publishable keys, Square
1181 * application/location IDs, Authorize.Net public client key, the enabled
1182 * flag, etc.). This removes the known secret keys while preserving the
1183 * structure and every public field, so existing gateways/consumers are
1184 * unaffected — only secrets are dropped.
1185 *
1186 * @param mixed $gateways
1187 * @return array<string, mixed>
1188 */
1189 /**
1190 * Per-gateway PUBLIC config for the booking page, keyed by gateway id
1191 * (window.yatraBookingData.gateways.<id>). Checkout scripts read their public
1192 * settings from here — e.g. square.js → gateways.square.application_id /
1193 * location_id, authorizenet.js → gateways.authorize_net.public_client_key /
1194 * api_login_id.
1195 *
1196 * Source of truth is each ENABLED gateway's own getFrontendData(), i.e. an
1197 * allowlist the gateway itself declares. This is deliberately NOT a denylist
1198 * over the raw stored config: a denylist would leak any secret whose key we
1199 * forgot (e.g. Stripe live_secret_key / test_secret_key, Bank Transfer
1200 * account_number / routing_code). Gateways without a getFrontendData()
1201 * (Bank Transfer, PayPal, Pay Later, …) contribute nothing, so their stored
1202 * details never reach the browser. Disabled gateways are excluded.
1203 *
1204 * @return array<string, array<string, mixed>>
1205 */
1206 private function getGatewayFrontendConfigs(): array
1207 {
1208 if (!class_exists(\Yatra\PaymentGateways\PaymentGatewayRegistry::class)) {
1209 return [];
1210 }
1211
1212 $out = [];
1213 try {
1214 $registry = \Yatra\PaymentGateways\PaymentGatewayRegistry::getInstance();
1215 foreach ($registry->getEnabledGateways() as $id => $gateway) {
1216 if (!is_object($gateway) || !method_exists($gateway, 'getFrontendData')) {
1217 continue;
1218 }
1219 $data = $gateway->getFrontendData();
1220 if (is_array($data) && $data !== []) {
1221 $data['enabled'] = true;
1222 $out[(string) $id] = $data;
1223 }
1224 }
1225 } catch (\Throwable $e) {
1226 return [];
1227 }
1228
1229 return $out;
1230 }
1231
1232 private function sanitizeGatewayConfigsForFrontend($gateways): array
1233 {
1234 if (!is_array($gateways)) {
1235 return [];
1236 }
1237
1238 // Credential fields that are private to the server.
1239 $secretKeys = [
1240 'access_token',
1241 'api_key',
1242 'api_secret',
1243 'secret_key',
1244 'key_secret',
1245 'client_secret',
1246 'transaction_key',
1247 'webhook_secret',
1248 'webhook_signing_secret',
1249 'signing_secret',
1250 'private_key',
1251 'password',
1252 'secret',
1253 ];
1254
1255 $clean = [];
1256 foreach ($gateways as $id => $config) {
1257 if (is_array($config)) {
1258 foreach ($secretKeys as $secret) {
1259 unset($config[$secret]);
1260 }
1261 }
1262 $clean[$id] = $config;
1263 }
1264
1265 return $clean;
1266 }
1267
1268 /**
1269 * Stripe Elements (assets/js/stripe.js) expects publishableKey under yatraBookingData.stripe.
1270 * Mirror YatraPro StripeGateway::loadConfig() live/test selection; never expose secret keys.
1271 *
1272 * @return array<string, mixed>
1273 */
1274 private function getStripeFrontendBookingPayload(): array
1275 {
1276 $allConfigs = get_option('yatra_gateway_configs', []);
1277 if (is_string($allConfigs)) {
1278 $maybe = maybe_unserialize($allConfigs);
1279 $allConfigs = is_array($maybe) ? $maybe : [];
1280 }
1281 if (!is_array($allConfigs)) {
1282 $allConfigs = [];
1283 }
1284
1285 $stripe = isset($allConfigs['stripe']) && is_array($allConfigs['stripe'])
1286 ? $allConfigs['stripe']
1287 : [];
1288
1289 $test = filter_var(\Yatra\Services\SettingsService::get('payment_test_mode', true), FILTER_VALIDATE_BOOLEAN);
1290
1291 $livePub = trim((string) ($stripe['live_publishable_key'] ?? ''));
1292 $testPub = trim((string) ($stripe['test_publishable_key'] ?? ''));
1293
1294 $publishableKey = trim((string) ($stripe['api_key'] ?? ''));
1295 if ($test) {
1296 if ($testPub !== '') {
1297 $publishableKey = $testPub;
1298 }
1299 } elseif ($livePub !== '') {
1300 $publishableKey = $livePub;
1301 }
1302
1303 // Match StripeGateway default + admin multi-select when unset (was dropped by old sanitizer).
1304 $defaultMethods = 'card,google_pay,apple_pay';
1305 $enabledMethods = $stripe['enabled_methods'] ?? $defaultMethods;
1306 if (is_array($enabledMethods)) {
1307 // stripe.js accepts an array of method ids
1308 } elseif (!is_string($enabledMethods) || trim($enabledMethods) === '') {
1309 $enabledMethods = $defaultMethods;
1310 }
1311
1312 $companyCountry = trim((string) \Yatra\Services\SettingsService::get('company_country', ''));
1313 if ($companyCountry === '') {
1314 $companyCountry = 'US';
1315 }
1316
1317 $payload = [
1318 'stripe' => [
1319 'publishableKey' => $publishableKey,
1320 'enabledMethods' => $enabledMethods,
1321 ],
1322 'companyCountry' => $companyCountry,
1323 ];
1324
1325 return apply_filters('yatra_booking_stripe_frontend_data', $payload, $stripe, $test);
1326 }
1327 }
1328