PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
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 trunk, at app/Providers/FrontendAssetsProvider.php

1,370 lines 55.0 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 * 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 /**
118 * Enqueue frontend assets based on context
119 *
120 * @return void
121 */
122 public function enqueueAssets(): void
123 {
124 // Only run on frontend
125 if (is_admin()) {
126 return;
127 }
128
129 // Always enqueue common frontend assets
130 $this->enqueueCommonAssets();
131
132 // Enqueue page-specific assets
133 $this->enqueuePageSpecificAssets();
134 }
135
136 /**
137 * Enqueue common frontend assets
138 *
139 * @return void
140 */
141 private function enqueueCommonAssets(): void
142 {
143 // Enqueue common CSS
144 $this->enqueueCommonCss();
145
146 // Enqueue common JavaScript
147 $this->enqueueCommonJs();
148 }
149
150 /**
151 * Enqueue common CSS files
152 *
153 * @return void
154 */
155 private function enqueueCommonCss(): void
156 {
157 $cssFiles = [
158 'common' => 'common.css',
159 'listing' => 'listing.css',
160 'stripe' => 'stripe.css',
161 'trip' => 'trip.css',
162 'activity' => 'activity.css',
163 'destination' => 'destination.css',
164 'yatra-capacity' => 'yatra-capacity.css',
165 'video-player' => 'video-player.css',
166 'tour-viewer' => 'tour-viewer.css',
167 ];
168
169 self::registerCoreFrontendStylesheets();
170
171 if (wp_style_is('yatra-fontawesome-6', 'registered')) {
172 wp_enqueue_style('yatra-fontawesome-6');
173 }
174
175 foreach ($cssFiles as $handle => $filename) {
176 $filePath = YATRA_PLUGIN_PATH . "assets/css/{$filename}";
177 if (!file_exists($filePath)) {
178 continue;
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 );
204 }
205
206 $this->enqueueFrontendThemeVariables();
207 $this->enqueueFrontendLayoutVariables();
208 }
209
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 /**
254 * Enqueue common JavaScript files
255 *
256 * @return void
257 */
258 private function enqueueCommonJs(): void
259 {
260 $jsFiles = [
261 'api-helper' => 'api-helper.js',
262 'video-player' => 'video-player.js',
263 'tour-viewer' => 'tour-viewer.js',
264 'listing' => 'listing.js',
265 'listing-filters' => 'listing-filters.js',
266 'stripe' => 'stripe.js',
267 'trip' => 'trip.js',
268 ];
269
270 foreach ($jsFiles as $handle => $filename) {
271 $filePath = YATRA_PLUGIN_PATH . "assets/js/{$filename}";
272 if (file_exists($filePath)) {
273 // Set dependencies
274 $dependencies = ['jquery'];
275 if ($handle === 'trip') {
276 $dependencies[] = 'yatra-api-helper';
277 }
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
291 wp_enqueue_script(
292 "yatra-{$handle}",
293 YATRA_PLUGIN_URL . "assets/js/{$filename}",
294 $dependencies,
295 YATRA_VERSION . '.' . filemtime($filePath),
296 true
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 }
312 }
313 }
314
315 if (\Yatra\Services\SettingsService::wishlistEnabled()) {
316 $wishPath = YATRA_PLUGIN_PATH . 'assets/js/listing-wishlist.js';
317 if (file_exists($wishPath)) {
318 wp_enqueue_script(
319 'yatra-listing-wishlist',
320 YATRA_PLUGIN_URL . 'assets/js/listing-wishlist.js',
321 ['jquery', 'wp-i18n'],
322 YATRA_VERSION . '.' . filemtime($wishPath),
323 true
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();
340 wp_localize_script('yatra-listing-wishlist', 'yatraWishlistConfig', [
341 'enabled' => true,
342 'restUrl' => rest_url('yatra/v1'),
343 'nonce' => wp_create_nonce('wp_rest'),
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 ],
359 ]);
360 }
361 }
362 }
363
364 /**
365 * Enqueue page-specific assets
366 *
367 * @return void
368 */
369 private function enqueuePageSpecificAssets(): void
370 {
371 if (yatra_is_account_page()) {
372 $this->enqueueAccountAssets();
373
374 return;
375 }
376
377 // Check current page context and enqueue specific assets using helper functions
378 if (yatra_is_trip_listing()) {
379 $this->enqueueTripListingAssets();
380 }
381
382 if (yatra_is_single_trip()) {
383 $this->enqueueTripDetailAssets();
384 }
385
386 if (yatra_is_activity_listing()) {
387 $this->enqueueActivityListingAssets();
388 }
389
390 if (yatra_is_destination_listing()) {
391 $this->enqueueDestinationListingAssets();
392 }
393
394 if (yatra_is_booking_page()) {
395 $this->enqueueBookingAssets();
396 }
397
398 if (yatra_is_taxonomy_page()) {
399 // Taxonomy pages use the same assets as trip listing
400 $this->enqueueTripListingAssets();
401 }
402 }
403
404 /**
405 * Enqueue trip listing specific assets
406 *
407 * @return void
408 */
409 private function enqueueTripListingAssets(): void
410 {
411 $this->enqueueListingFiltersJs();
412 }
413
414 /**
415 * Enqueue trip detail specific assets
416 *
417 * @return void
418 */
419 private function enqueueTripDetailAssets(): void
420 {
421 // Enqueue booking assets for trip detail pages (booking forms)
422 $bookingJs = YATRA_PLUGIN_PATH . 'assets/js/booking.js';
423 if (file_exists($bookingJs)) {
424 wp_enqueue_script(
425 'yatra-booking',
426 YATRA_PLUGIN_URL . 'assets/js/booking.js',
427 ['jquery', 'wp-i18n'],
428 YATRA_VERSION . '.' . filemtime($bookingJs),
429 true
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 }
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 }
468
469 // Localize trip page data for JS (trip.js, booking.js)
470 global $trip;
471
472 $permalink_structure = get_option('permalink_structure') ?: '';
473 $is_plain = empty($permalink_structure);
474
475 $trip_id = null;
476 $trip_slug = null;
477 $has_trip = isset($trip) && is_object($trip) && isset($trip->id);
478 if ($has_trip) {
479 $trip_id = (int) $trip->id;
480 }
481 if ($has_trip && isset($trip->slug)) {
482 $trip_slug = $trip->slug;
483 }
484
485 $tripData = [
486 'apiUrl' => rest_url('yatra/v1'),
487 'restUrl' => rest_url(),
488 'siteUrl' => site_url(),
489 'bookingBase' => \Yatra\Services\SettingsService::getBookingBase(),
490 'permalinkStructure' => $is_plain ? 'plain' : $permalink_structure,
491 'nonce' => wp_create_nonce('wp_rest'),
492 'tripId' => $trip_id,
493 'tripSlug' => $trip_slug,
494 'wishlistEnabled' => \Yatra\Services\SettingsService::wishlistEnabled(),
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 })(),
504 // Regional settings
505 'timezone' => \Yatra\Services\SettingsService::getString('timezone', 'UTC'),
506 'dateFormat' => \Yatra\Services\SettingsService::getString('date_format', 'Y-m-d'),
507 'timeFormat' => \Yatra\Services\SettingsService::getString('time_format', 'H:i'),
508 // Currency/settings
509 'currency' => \Yatra\Services\SettingsService::getCurrency(),
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(),
513 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
514 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
515 'basePrice' => 0.0,
516 'currencySymbol' => function_exists('yatra_get_currency_symbol')
517 ? yatra_get_currency_symbol(\Yatra\Services\SettingsService::getCurrency())
518 : '$',
519 'availabilityDates' => [],
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(),
530 ];
531
532 if ($has_trip) {
533 if (function_exists('yatra_single_trip_calculate_base_price')) {
534 $pricing_data = yatra_single_trip_calculate_base_price($trip);
535 $tripData['basePrice'] = (float) ($pricing_data['base_price'] ?? 0);
536 }
537 if (method_exists($trip, 'getAvailabilityDates')) {
538 $tripData['availabilityDates'] = array_values(array_filter(array_map(static function ($avail) {
539 if (is_object($avail)) {
540 return $avail->departure_date ?? $avail->date ?? null;
541 }
542 if (is_array($avail)) {
543 return $avail['departure_date'] ?? $avail['date'] ?? null;
544 }
545
546 return null;
547 }, $trip->getAvailabilityDates())));
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 }
555 }
556
557 wp_localize_script('yatra-trip', 'yatraTripData', $tripData);
558 $tripTitle = '';
559 if ($has_trip && isset($trip->title)) {
560 $tripTitle = is_string($trip->title) ? $trip->title : '';
561 }
562 wp_localize_script('yatra-booking', 'yatraBookingData', array_merge(
563 $tripData,
564 $this->getStripeFrontendBookingPayload(),
565 [
566 'tripTitle' => $tripTitle !== '' ? $tripTitle : ($tripData['tripTitle'] ?? 'Trip Booking'),
567 // booking page expects these keys; leave placeholders if not set on trip view
568 'isRemainingPayment' => false,
569 'remainingAmount' => 0,
570 'totalAmount' => 0,
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'),
580 ]
581 ));
582 }
583
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 /**
683 * Enqueue activity listing specific assets
684 *
685 * @return void
686 */
687 private function enqueueActivityListingAssets(): void
688 {
689 // Activity listing specific assets
690 $filePath = YATRA_PLUGIN_PATH . 'assets/css/activity.css';
691 if (file_exists($filePath)) {
692 wp_enqueue_style(
693 'yatra-activity-listing',
694 YATRA_PLUGIN_URL . 'assets/css/activity.css',
695 [],
696 YATRA_VERSION . '.' . filemtime($filePath)
697 );
698 }
699 }
700
701 /**
702 * Enqueue destination listing specific assets
703 *
704 * @return void
705 */
706 private function enqueueDestinationListingAssets(): void
707 {
708 // Destination listing specific assets
709 $filePath = YATRA_PLUGIN_PATH . 'assets/css/destination.css';
710 if (file_exists($filePath)) {
711 wp_enqueue_style(
712 'yatra-destination-listing',
713 YATRA_PLUGIN_URL . 'assets/css/destination.css',
714 [],
715 YATRA_VERSION . '.' . filemtime($filePath)
716 );
717 }
718 }
719
720 /**
721 * Enqueue booking specific assets
722 *
723 * @return void
724 */
725 private function enqueueBookingAssets(): void
726 {
727 // Enqueue booking-specific CSS
728 $bookingCss = YATRA_PLUGIN_PATH . 'assets/css/booking.css';
729 if (file_exists($bookingCss)) {
730 wp_enqueue_style(
731 'yatra-booking',
732 YATRA_PLUGIN_URL . 'assets/css/booking.css',
733 ['yatra-common'],
734 YATRA_VERSION . '.' . filemtime($bookingCss)
735 );
736 }
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
757 // Enqueue booking-specific JavaScript
758 $bookingJs = YATRA_PLUGIN_PATH . 'assets/js/booking.js';
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.
767 wp_enqueue_script(
768 'yatra-booking',
769 YATRA_PLUGIN_URL . 'assets/js/booking.js',
770 ['jquery', 'yatra-flatpickr', 'wp-i18n'],
771 YATRA_VERSION . '.' . filemtime($bookingJs),
772 true
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 }
781 }
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
801 // Localize booking data for booking.js
802 $permalink_structure = get_option('permalink_structure') ?: '';
803 $is_plain = empty($permalink_structure);
804
805 $deposit_pct_store = (int) \Yatra\Services\SettingsService::get('deposit_percentage', 20);
806 $partial_pct_store = (int) \Yatra\Services\SettingsService::get('partial_payment_percentage', 30);
807 $deposit_pct_resolved = (int) apply_filters('yatra_deposit_percentage', $deposit_pct_store);
808 $partial_pct_resolved = (int) apply_filters('yatra_partial_payment_percentage', $partial_pct_store);
809 $flexible_payments_enabled = (bool) apply_filters('yatra_flexible_payments_enabled', false);
810 $flexible_module_on = class_exists(ModuleManager::class)
811 ? ModuleManager::isModuleEnabled('flexible_payments')
812 : false;
813
814 Logger::debug('Yatra booking localize: flexible payment snapshot', [
815 'context' => 'booking_localize',
816 'flexible_payments_enabled' => $flexible_payments_enabled,
817 'flexible_payments_module' => $flexible_module_on,
818 'partial_payment_setting' => (bool) \Yatra\Services\SettingsService::get('partial_payment', false),
819 'deposit_required_setting' => (bool) \Yatra\Services\SettingsService::get('deposit_required', false),
820 'deposit_percentage_store' => $deposit_pct_store,
821 'partial_percentage_store' => $partial_pct_store,
822 'deposit_percentage_resolved' => $deposit_pct_resolved,
823 'partial_percentage_resolved' => $partial_pct_resolved,
824 ]);
825
826 $bookingData = [
827 'apiUrl' => rest_url('yatra/v1'),
828 'restUrl' => rest_url(),
829 'siteUrl' => site_url(),
830 'bookingBase' => \Yatra\Services\SettingsService::getBookingBase(),
831 'permalinkStructure' => $is_plain ? 'plain' : $permalink_structure,
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'),
838 'currency' => \Yatra\Services\SettingsService::getCurrency(),
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(),
842 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
843 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
844 // Payment gateways data
845 'paymentGateways' => $this->sanitizeGatewayConfigsForFrontend(apply_filters('yatra_payment_gateways', \Yatra\Services\SettingsService::get('payment_gateways', []))),
846 'paymentMethods' => \Yatra\Services\SettingsService::get('payment_methods', []),
847 'paymentTestMode' => \Yatra\Services\SettingsService::get('payment_test_mode', false),
848 'partialPayment' => \Yatra\Services\SettingsService::get('partial_payment', false),
849 'partialPaymentPercentage' => \Yatra\Services\SettingsService::get('partial_payment_percentage', 0),
850 // Flexible payments (Pro) — keep these keys stable for frontend booking.js.
851 // Values are resolved via generic filters so Pro can override without hard-coding premium logic here.
852 'depositRequired' => (bool) \Yatra\Services\SettingsService::get('deposit_required', false),
853 'depositPercentage' => $deposit_pct_resolved,
854 'partialPercentage' => $partial_pct_resolved,
855 'gatewayOrder' => \Yatra\Services\SettingsService::get('gateway_order', []),
856 'autoConfirmPayLater' => \Yatra\Services\SettingsService::get('auto_confirm_pay_later', true),
857 'allowWaitlist' => \Yatra\Services\SettingsService::isEnabled('allow_waitlist'),
858 'waitlistAutoConfirm' => \Yatra\Services\SettingsService::isEnabled('waitlist_auto_confirm'),
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 ],
868 ];
869
870 $bookingData = array_merge($bookingData, $this->getStripeFrontendBookingPayload());
871
872 wp_localize_script('yatra-booking', 'yatraBookingData', $bookingData);
873 }
874
875 /**
876 * Enqueue account page specific assets
877 *
878 * @return void
879 */
880 private function enqueueAccountAssets(): void
881 {
882 // Account bundle shares admin Vite chunks; CSS is extracted to admin/dist/css (ES modules do not auto-load it).
883 $reactVendorCss = YATRA_PLUGIN_PATH . 'assets/admin/dist/css/react-vendor.css';
884 if (file_exists($reactVendorCss)) {
885 wp_enqueue_style(
886 'yatra-account-react-vendor',
887 YATRA_PLUGIN_URL . 'assets/admin/dist/css/react-vendor.css',
888 [],
889 YATRA_VERSION . '.' . filemtime($reactVendorCss)
890 );
891 }
892
893 $accountUiCss = YATRA_PLUGIN_PATH . 'assets/admin/dist/css/index.css';
894 $accountUiDeps = file_exists($reactVendorCss) ? ['yatra-account-react-vendor'] : [];
895 if (file_exists($accountUiCss)) {
896 wp_enqueue_style(
897 'yatra-account-ui',
898 YATRA_PLUGIN_URL . 'assets/admin/dist/css/index.css',
899 $accountUiDeps,
900 YATRA_VERSION . '.' . filemtime($accountUiCss)
901 );
902 }
903
904 // Vite build outputs to assets/dist/js/account-page.js (ES module + shared chunks).
905 $accountJs = YATRA_PLUGIN_PATH . 'assets/dist/js/account-page.js';
906 if (!file_exists($accountJs)) {
907 return;
908 }
909
910 wp_enqueue_script(
911 'yatra-account-page',
912 YATRA_PLUGIN_URL . 'assets/dist/js/account-page.js',
913 [],
914 YATRA_VERSION . '.' . filemtime($accountJs),
915 true
916 );
917
918 wp_script_add_data('yatra-account-page', 'type', 'module');
919
920 wp_localize_script('yatra-account-page', 'yatraAccountPage', [
921 'apiUrl' => rest_url('yatra/v1'),
922 'nonce' => wp_create_nonce('wp_rest'),
923 'userId' => get_current_user_id(),
924 'siteUrl' => site_url(),
925 'logoutUrl' => wp_logout_url(home_url('/')),
926 'companyPhone' => \Yatra\Services\SettingsService::getString('company_phone', ''),
927 'companyName' => \Yatra\Services\SettingsService::getString('company_name', ''),
928 'companyEmail' => \Yatra\Services\SettingsService::getString('company_email', ''),
929 'currency' => \Yatra\Services\SettingsService::getCurrency(),
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(),
933 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
934 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
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 : [],
948 'translations' => $this->getFrontendTranslations(),
949 'wishlistEnabled' => \Yatra\Services\SettingsService::wishlistEnabled(),
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 }
958 }
959
960 /**
961 * Enqueue listing filters JavaScript
962 *
963 * @return void
964 */
965 private function enqueueListingFiltersJs(): void
966 {
967 $filtersJs = YATRA_PLUGIN_PATH . 'assets/js/listing-filters.js';
968 if (file_exists($filtersJs)) {
969 wp_enqueue_script(
970 'yatra-listing-filters',
971 YATRA_PLUGIN_URL . 'assets/js/listing-filters.js',
972 ['jquery'],
973 YATRA_VERSION . '.' . filemtime($filtersJs),
974 true
975 );
976
977 // Add currency formatting function
978 wp_add_inline_script('yatra-listing-filters', "
979 window.yatra_format_price = function(amount) {
980 if (!amount || amount == 0) return '" . esc_js(__('Contact for pricing', 'yatra')) . "';
981 const currency = window.yatraSettings?.currency || 'USD';
982 const symbol = window.yatraSettings?.currencySymbol || '$';
983 return symbol + amount.toLocaleString();
984 };
985 ");
986 }
987 }
988
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 /**
1062 * Get frontend translations
1063 *
1064 * @return array
1065 */
1066 private function getFrontendTranslations(): array
1067 {
1068 return [
1069 // Account page
1070 'My Account' => __('My Account', 'yatra'),
1071 'My Bookings' => __('My Bookings', 'yatra'),
1072 'Account Settings' => __('Account Settings', 'yatra'),
1073 'Logout' => __('Logout', 'yatra'),
1074 'Login' => __('Login', 'yatra'),
1075 'Register' => __('Register', 'yatra'),
1076
1077 // Booking related
1078 'Booking Details' => __('Booking Details', 'yatra'),
1079 'Booking Status' => __('Booking Status', 'yatra'),
1080 'Total Amount' => __('Total Amount', 'yatra'),
1081 'Payment Status' => __('Payment Status', 'yatra'),
1082 'View Details' => __('View Details', 'yatra'),
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
1110 // Common
1111 'Loading...' => __('Loading...', 'yatra'),
1112 'No data available' => __('No data available', 'yatra'),
1113 'Error loading data' => __('Error loading data', 'yatra'),
1114 'Please try again' => __('Please try again', 'yatra'),
1115
1116 // Currency and pricing
1117 'Contact for pricing' => __('Contact for pricing', 'yatra'),
1118 'Free' => __('Free', 'yatra'),
1119 'Price' => __('Price', 'yatra'),
1120
1121 // Trip related
1122 'Trip Details' => __('Trip Details', 'yatra'),
1123 'Duration' => __('Duration', 'yatra'),
1124 'Difficulty' => __('Difficulty', 'yatra'),
1125 'Group Size' => __('Group Size', 'yatra'),
1126
1127 // Navigation
1128 'Home' => __('Home', 'yatra'),
1129 'Trips' => __('Trips', 'yatra'),
1130 'Destinations' => __('Destinations', 'yatra'),
1131 'Activities' => __('Activities', 'yatra'),
1132 'About Us' => __('About Us', 'yatra'),
1133 'Contact' => __('Contact', 'yatra'),
1134 ];
1135 }
1136
1137 /**
1138 * Enqueue assets for specific shortcodes
1139 *
1140 * @param array $shortcodes Array of shortcodes that need assets
1141 * @return void
1142 */
1143 public function enqueueShortcodeAssets(array $shortcodes): void
1144 {
1145 global $post;
1146
1147 if (!$post || !has_shortcode($post->post_content, $shortcodes)) {
1148 return;
1149 }
1150
1151 // Enqueue common frontend assets for shortcodes
1152 $this->enqueueCommonAssets();
1153
1154 // Shortcode-specific assets can be added here based on $shortcodes array
1155 foreach ($shortcodes as $shortcode) {
1156 switch ($shortcode) {
1157 case 'yatra_trip_listing':
1158 $this->enqueueTripListingAssets();
1159 break;
1160 case 'yatra_cart':
1161 case 'yatra_checkout':
1162 $this->enqueueBookingAssets();
1163 break;
1164 case 'yatra_my_account':
1165 $this->enqueueAccountAssets();
1166 break;
1167 }
1168 }
1169 }
1170
1171 /**
1172 * Enqueue assets conditionally based on custom conditions
1173 *
1174 * @param callable $condition Function that returns true if assets should be enqueued
1175 * @return void
1176 */
1177 public function enqueueConditionalAssets(callable $condition): void
1178 {
1179 if ($condition()) {
1180 $this->enqueueAssets();
1181 }
1182 }
1183
1184 /**
1185 * Get asset URL with versioning
1186 *
1187 * @param string $path Relative path to asset
1188 * @param string $type 'css' or 'js'
1189 * @return string Asset URL or empty string if file doesn't exist
1190 */
1191 public function getAssetUrl(string $path, string $type = 'css'): string
1192 {
1193 $basePath = $type === 'css' ? 'assets/css/' : 'assets/js/';
1194 $fullPath = YATRA_PLUGIN_PATH . $basePath . $path;
1195
1196 if (!file_exists($fullPath)) {
1197 return '';
1198 }
1199
1200 $version = YATRA_VERSION . '.' . filemtime($fullPath);
1201 return YATRA_PLUGIN_URL . $basePath . $path . '?ver=' . $version;
1202 }
1203
1204 /**
1205 * Check if asset file exists
1206 *
1207 * @param string $path Relative path to asset
1208 * @param string $type 'css' or 'js'
1209 * @return bool
1210 */
1211 public function assetExists(string $path, string $type = 'css'): bool
1212 {
1213 $basePath = $type === 'css' ? 'assets/css/' : 'assets/js/';
1214 $fullPath = YATRA_PLUGIN_PATH . $basePath . $path;
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;
1308 }
1309
1310 /**
1311 * Stripe Elements (assets/js/stripe.js) expects publishableKey under yatraBookingData.stripe.
1312 * Mirror YatraPro StripeGateway::loadConfig() live/test selection; never expose secret keys.
1313 *
1314 * @return array<string, mixed>
1315 */
1316 private function getStripeFrontendBookingPayload(): array
1317 {
1318 $allConfigs = get_option('yatra_gateway_configs', []);
1319 if (is_string($allConfigs)) {
1320 $maybe = maybe_unserialize($allConfigs);
1321 $allConfigs = is_array($maybe) ? $maybe : [];
1322 }
1323 if (!is_array($allConfigs)) {
1324 $allConfigs = [];
1325 }
1326
1327 $stripe = isset($allConfigs['stripe']) && is_array($allConfigs['stripe'])
1328 ? $allConfigs['stripe']
1329 : [];
1330
1331 $test = filter_var(\Yatra\Services\SettingsService::get('payment_test_mode', true), FILTER_VALIDATE_BOOLEAN);
1332
1333 $livePub = trim((string) ($stripe['live_publishable_key'] ?? ''));
1334 $testPub = trim((string) ($stripe['test_publishable_key'] ?? ''));
1335
1336 $publishableKey = trim((string) ($stripe['api_key'] ?? ''));
1337 if ($test) {
1338 if ($testPub !== '') {
1339 $publishableKey = $testPub;
1340 }
1341 } elseif ($livePub !== '') {
1342 $publishableKey = $livePub;
1343 }
1344
1345 // Match StripeGateway default + admin multi-select when unset (was dropped by old sanitizer).
1346 $defaultMethods = 'card,google_pay,apple_pay';
1347 $enabledMethods = $stripe['enabled_methods'] ?? $defaultMethods;
1348 if (is_array($enabledMethods)) {
1349 // stripe.js accepts an array of method ids
1350 } elseif (!is_string($enabledMethods) || trim($enabledMethods) === '') {
1351 $enabledMethods = $defaultMethods;
1352 }
1353
1354 $companyCountry = trim((string) \Yatra\Services\SettingsService::get('company_country', ''));
1355 if ($companyCountry === '') {
1356 $companyCountry = 'US';
1357 }
1358
1359 $payload = [
1360 'stripe' => [
1361 'publishableKey' => $publishableKey,
1362 'enabledMethods' => $enabledMethods,
1363 ],
1364 'companyCountry' => $companyCountry,
1365 ];
1366
1367 return apply_filters('yatra_booking_stripe_frontend_data', $payload, $stripe, $test);
1368 }
1369 }
1370