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

1,159 lines 45.8 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 wp_localize_script('yatra-listing-wishlist', 'yatraWishlistConfig', [
291 'enabled' => true,
292 'restUrl' => rest_url('yatra/v1'),
293 'nonce' => wp_create_nonce('wp_rest'),
294 'isLoggedIn' => is_user_logged_in(),
295 'loginUrl' => wp_login_url(),
296 'i18n' => [
297 'loginRequired' => __('Login Required', 'yatra'),
298 'loginPrompt' => __('Please login to save trips to your wishlist.', 'yatra'),
299 'login' => __('Login', 'yatra'),
300 'cancel' => __('Cancel', 'yatra'),
301 'genericError' => __('An error occurred. Please try again.', 'yatra'),
302 'saved' => __('Trip saved to wishlist', 'yatra'),
303 'removed' => __('Trip removed from wishlist', 'yatra'),
304 'saveFailed' => __('Failed to save trip', 'yatra'),
305 'removeFailed' => __('Failed to remove trip', 'yatra'),
306 'addAria' => __('Add to favorites', 'yatra'),
307 'removeAria' => __('Remove from favorites', 'yatra'),
308 ],
309 ]);
310 }
311 }
312 }
313
314 /**
315 * Enqueue page-specific assets
316 *
317 * @return void
318 */
319 private function enqueuePageSpecificAssets(): void
320 {
321 if (yatra_is_account_page()) {
322 $this->enqueueAccountAssets();
323
324 return;
325 }
326
327 // Check current page context and enqueue specific assets using helper functions
328 if (yatra_is_trip_listing()) {
329 $this->enqueueTripListingAssets();
330 }
331
332 if (yatra_is_single_trip()) {
333 $this->enqueueTripDetailAssets();
334 }
335
336 if (yatra_is_activity_listing()) {
337 $this->enqueueActivityListingAssets();
338 }
339
340 if (yatra_is_destination_listing()) {
341 $this->enqueueDestinationListingAssets();
342 }
343
344 if (yatra_is_booking_page()) {
345 $this->enqueueBookingAssets();
346 }
347
348 if (yatra_is_taxonomy_page()) {
349 // Taxonomy pages use the same assets as trip listing
350 $this->enqueueTripListingAssets();
351 }
352 }
353
354 /**
355 * Enqueue trip listing specific assets
356 *
357 * @return void
358 */
359 private function enqueueTripListingAssets(): void
360 {
361 $this->enqueueListingFiltersJs();
362 }
363
364 /**
365 * Enqueue trip detail specific assets
366 *
367 * @return void
368 */
369 private function enqueueTripDetailAssets(): void
370 {
371 // Enqueue booking assets for trip detail pages (booking forms)
372 $bookingJs = YATRA_PLUGIN_PATH . 'assets/js/booking.js';
373 if (file_exists($bookingJs)) {
374 wp_enqueue_script(
375 'yatra-booking',
376 YATRA_PLUGIN_URL . 'assets/js/booking.js',
377 ['jquery', 'wp-i18n'],
378 YATRA_VERSION . '.' . filemtime($bookingJs),
379 true
380 );
381 if (function_exists('wp_set_script_translations')) {
382 wp_set_script_translations(
383 'yatra-booking',
384 'yatra',
385 YATRA_PLUGIN_PATH . 'i18n/languages'
386 );
387 }
388 }
389
390 // Mobile sticky-sidebar + flatpickr init for the single-trip page. Lives in a
391 // dedicated file rather than as inline <script> in the partial because
392 // WordPress core's `convert_chars` filter (hooked to the_content) rewrites the
393 // `&&` operators inside inline scripts as `&#038;&#038;` — JS parsers don't
394 // decode HTML entities inside <script>, producing a SyntaxError. As a properly
395 // enqueued external file, the source is delivered verbatim.
396 $sidebarJs = YATRA_PLUGIN_PATH . 'assets/js/single-trip-sidebar.js';
397 if (file_exists($sidebarJs)) {
398 wp_enqueue_script(
399 'yatra-single-trip-sidebar',
400 YATRA_PLUGIN_URL . 'assets/js/single-trip-sidebar.js',
401 ['yatra-trip', 'wp-i18n'], // depends on window.yatraTripData from yatra-trip
402 YATRA_VERSION . '.' . filemtime($sidebarJs),
403 true
404 );
405 if (function_exists('wp_set_script_translations')) {
406 wp_set_script_translations(
407 'yatra-single-trip-sidebar',
408 'yatra',
409 YATRA_PLUGIN_PATH . 'i18n/languages'
410 );
411 }
412 }
413
414 // Localize trip page data for JS (trip.js, booking.js)
415 global $trip;
416
417 $permalink_structure = get_option('permalink_structure') ?: '';
418 $is_plain = empty($permalink_structure);
419
420 $trip_id = null;
421 $trip_slug = null;
422 $has_trip = isset($trip) && is_object($trip) && isset($trip->id);
423 if ($has_trip) {
424 $trip_id = (int) $trip->id;
425 }
426 if ($has_trip && isset($trip->slug)) {
427 $trip_slug = $trip->slug;
428 }
429
430 $tripData = [
431 'apiUrl' => rest_url('yatra/v1'),
432 'restUrl' => rest_url(),
433 'siteUrl' => site_url(),
434 'bookingBase' => \Yatra\Services\SettingsService::getBookingBase(),
435 'permalinkStructure' => $is_plain ? 'plain' : $permalink_structure,
436 'nonce' => wp_create_nonce('wp_rest'),
437 'tripId' => $trip_id,
438 'tripSlug' => $trip_slug,
439 'wishlistEnabled' => \Yatra\Services\SettingsService::wishlistEnabled(),
440 'isLoggedIn' => is_user_logged_in(),
441 // Regional settings
442 'timezone' => \Yatra\Services\SettingsService::getString('timezone', 'UTC'),
443 'dateFormat' => \Yatra\Services\SettingsService::getString('date_format', 'Y-m-d'),
444 'timeFormat' => \Yatra\Services\SettingsService::getString('time_format', 'H:i'),
445 // Currency/settings
446 'currency' => \Yatra\Services\SettingsService::getCurrency(),
447 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
448 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
449 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(),
450 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
451 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
452 'basePrice' => 0.0,
453 'currencySymbol' => function_exists('yatra_get_currency_symbol')
454 ? yatra_get_currency_symbol(\Yatra\Services\SettingsService::getCurrency())
455 : '$',
456 'availabilityDates' => [],
457 'groupDiscountsUrl' => rest_url('yatra/v1/discounts/group-discounts'),
458 'dynamicPricingDisplay' => apply_filters('yatra_get_dynamic_pricing_display_settings', [
459 'show_original_price' => true,
460 'show_savings_badge' => true,
461 'show_urgency_messages' => false,
462 ]),
463 'pricingType' => 'regular',
464 'sidebarAvailability' => [],
465 'sidebarGroupDiscounts' => [],
466 'flatpickrLocale' => $this->buildFlatpickrLocalePayload(),
467 ];
468
469 if ($has_trip) {
470 if (function_exists('yatra_single_trip_calculate_base_price')) {
471 $pricing_data = yatra_single_trip_calculate_base_price($trip);
472 $tripData['basePrice'] = (float) ($pricing_data['base_price'] ?? 0);
473 }
474 if (method_exists($trip, 'getAvailabilityDates')) {
475 $tripData['availabilityDates'] = array_values(array_filter(array_map(static function ($avail) {
476 if (is_object($avail)) {
477 return $avail->departure_date ?? $avail->date ?? null;
478 }
479 if (is_array($avail)) {
480 return $avail['departure_date'] ?? $avail['date'] ?? null;
481 }
482
483 return null;
484 }, $trip->getAvailabilityDates())));
485 }
486 if (function_exists('yatra_single_trip_get_client_booking_payload')) {
487 $bookingPayload = yatra_single_trip_get_client_booking_payload($trip);
488 $tripData['pricingType'] = $bookingPayload['pricingType'];
489 $tripData['sidebarAvailability'] = $bookingPayload['sidebarAvailability'];
490 $tripData['sidebarGroupDiscounts'] = $bookingPayload['sidebarGroupDiscounts'];
491 }
492 }
493
494 wp_localize_script('yatra-trip', 'yatraTripData', $tripData);
495 $tripTitle = '';
496 if ($has_trip && isset($trip->title)) {
497 $tripTitle = is_string($trip->title) ? $trip->title : '';
498 }
499 wp_localize_script('yatra-booking', 'yatraBookingData', array_merge(
500 $tripData,
501 $this->getStripeFrontendBookingPayload(),
502 [
503 'tripTitle' => $tripTitle !== '' ? $tripTitle : ($tripData['tripTitle'] ?? 'Trip Booking'),
504 // booking page expects these keys; leave placeholders if not set on trip view
505 'isRemainingPayment' => false,
506 'remainingAmount' => 0,
507 'totalAmount' => 0,
508 'amountPaid' => 0,
509 // Booking-scoped CSRF nonce — covers BOTH logged-in and
510 // guest checkouts. The REST endpoint's public
511 // permission_callback intentionally bypasses the WP REST
512 // cookie/nonce check (so guests can hit it at all);
513 // this token is what gates the actual write. The JS
514 // forwards it in the `X-Yatra-Booking-Nonce` header on
515 // every booking-create / booking-update POST.
516 'bookingNonce' => wp_create_nonce('yatra_booking_action'),
517 ]
518 ));
519 }
520
521 /**
522 * Enqueue activity listing specific assets
523 *
524 * @return void
525 */
526 private function enqueueActivityListingAssets(): void
527 {
528 // Activity listing specific assets
529 $filePath = YATRA_PLUGIN_PATH . 'assets/css/activity.css';
530 if (file_exists($filePath)) {
531 wp_enqueue_style(
532 'yatra-activity-listing',
533 YATRA_PLUGIN_URL . 'assets/css/activity.css',
534 [],
535 YATRA_VERSION . '.' . filemtime($filePath)
536 );
537 }
538 }
539
540 /**
541 * Enqueue destination listing specific assets
542 *
543 * @return void
544 */
545 private function enqueueDestinationListingAssets(): void
546 {
547 // Destination listing specific assets
548 $filePath = YATRA_PLUGIN_PATH . 'assets/css/destination.css';
549 if (file_exists($filePath)) {
550 wp_enqueue_style(
551 'yatra-destination-listing',
552 YATRA_PLUGIN_URL . 'assets/css/destination.css',
553 [],
554 YATRA_VERSION . '.' . filemtime($filePath)
555 );
556 }
557 }
558
559 /**
560 * Enqueue booking specific assets
561 *
562 * @return void
563 */
564 private function enqueueBookingAssets(): void
565 {
566 // Enqueue booking-specific CSS
567 $bookingCss = YATRA_PLUGIN_PATH . 'assets/css/booking.css';
568 if (file_exists($bookingCss)) {
569 wp_enqueue_style(
570 'yatra-booking',
571 YATRA_PLUGIN_URL . 'assets/css/booking.css',
572 ['yatra-common'],
573 YATRA_VERSION . '.' . filemtime($bookingCss)
574 );
575 }
576
577 // Flatpickr — used by booking.js to upgrade Date-of-Birth (and other
578 // date) inputs to a picker with fast, typeable year navigation. The
579 // single-trip page already ships flatpickr (see single-trip.php); the
580 // dedicated booking page did not, so enqueue it here. booking.js
581 // self-guards on `typeof flatpickr`, so this is safe either way.
582 wp_enqueue_style(
583 'yatra-flatpickr',
584 'https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css',
585 [],
586 YATRA_VERSION
587 );
588 wp_enqueue_script(
589 'yatra-flatpickr',
590 'https://cdn.jsdelivr.net/npm/flatpickr',
591 [],
592 YATRA_VERSION,
593 true
594 );
595
596 // Enqueue booking-specific JavaScript
597 $bookingJs = YATRA_PLUGIN_PATH . 'assets/js/booking.js';
598 if (file_exists($bookingJs)) {
599 wp_enqueue_script(
600 'yatra-booking',
601 YATRA_PLUGIN_URL . 'assets/js/booking.js',
602 ['jquery', 'yatra-flatpickr'],
603 YATRA_VERSION . '.' . filemtime($bookingJs),
604 true
605 );
606 }
607
608 // Load each available gateway's own client scripts on the checkout page
609 // (e.g. Square Web Payments SDK + square.js, Authorize.Net Accept.js +
610 // its handler, Razorpay SDK + its handler). Every gateway's
611 // enqueueScripts() self-guards on isAvailable(), so only enabled +
612 // configured gateways load anything. This call was previously missing,
613 // so Pro gateways that render an inline card form shipped no JS to
614 // checkout and clicking "Pay" just span the button forever. It is
615 // additive and safe for the others: Stripe's enqueueScripts() is a
616 // no-op (Stripe is loaded via enqueueCommonJs), and PayPal/Pay Later
617 // have no client scripts.
618 if (class_exists(\Yatra\PaymentGateways\PaymentGatewayRegistry::class)) {
619 \Yatra\PaymentGateways\PaymentGatewayRegistry::getInstance()->enqueueScripts();
620 }
621
622 // Localize booking data for booking.js
623 $permalink_structure = get_option('permalink_structure') ?: '';
624 $is_plain = empty($permalink_structure);
625
626 $deposit_pct_store = (int) \Yatra\Services\SettingsService::get('deposit_percentage', 20);
627 $partial_pct_store = (int) \Yatra\Services\SettingsService::get('partial_payment_percentage', 30);
628 $deposit_pct_resolved = (int) apply_filters('yatra_deposit_percentage', $deposit_pct_store);
629 $partial_pct_resolved = (int) apply_filters('yatra_partial_payment_percentage', $partial_pct_store);
630 $flexible_payments_enabled = (bool) apply_filters('yatra_flexible_payments_enabled', false);
631 $flexible_module_on = class_exists(ModuleManager::class)
632 ? ModuleManager::isModuleEnabled('flexible_payments')
633 : false;
634
635 Logger::debug('Yatra booking localize: flexible payment snapshot', [
636 'context' => 'booking_localize',
637 'flexible_payments_enabled' => $flexible_payments_enabled,
638 'flexible_payments_module' => $flexible_module_on,
639 'partial_payment_setting' => (bool) \Yatra\Services\SettingsService::get('partial_payment', false),
640 'deposit_required_setting' => (bool) \Yatra\Services\SettingsService::get('deposit_required', false),
641 'deposit_percentage_store' => $deposit_pct_store,
642 'partial_percentage_store' => $partial_pct_store,
643 'deposit_percentage_resolved' => $deposit_pct_resolved,
644 'partial_percentage_resolved' => $partial_pct_resolved,
645 ]);
646
647 $bookingData = [
648 'apiUrl' => rest_url('yatra/v1'),
649 'restUrl' => rest_url(),
650 'siteUrl' => site_url(),
651 'bookingBase' => \Yatra\Services\SettingsService::getBookingBase(),
652 'permalinkStructure' => $is_plain ? 'plain' : $permalink_structure,
653 'nonce' => wp_create_nonce('wp_rest'),
654 // Booking-scoped CSRF nonce. See enqueueTripDetailAssets()
655 // for the rationale: the booking REST endpoint bypasses
656 // the WP REST cookie/nonce check (so guests can use it),
657 // and this token is what gates the actual booking write.
658 'bookingNonce' => wp_create_nonce('yatra_booking_action'),
659 'currency' => \Yatra\Services\SettingsService::getCurrency(),
660 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
661 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
662 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(),
663 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
664 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
665 // Payment gateways data
666 'paymentGateways' => $this->sanitizeGatewayConfigsForFrontend(apply_filters('yatra_payment_gateways', \Yatra\Services\SettingsService::get('payment_gateways', []))),
667 'paymentMethods' => \Yatra\Services\SettingsService::get('payment_methods', []),
668 'paymentTestMode' => \Yatra\Services\SettingsService::get('payment_test_mode', false),
669 'partialPayment' => \Yatra\Services\SettingsService::get('partial_payment', false),
670 'partialPaymentPercentage' => \Yatra\Services\SettingsService::get('partial_payment_percentage', 0),
671 // Flexible payments (Pro) — keep these keys stable for frontend booking.js.
672 // Values are resolved via generic filters so Pro can override without hard-coding premium logic here.
673 'depositRequired' => (bool) \Yatra\Services\SettingsService::get('deposit_required', false),
674 'depositPercentage' => $deposit_pct_resolved,
675 'partialPercentage' => $partial_pct_resolved,
676 'gatewayOrder' => \Yatra\Services\SettingsService::get('gateway_order', []),
677 'autoConfirmPayLater' => \Yatra\Services\SettingsService::get('auto_confirm_pay_later', true),
678 'allowWaitlist' => \Yatra\Services\SettingsService::isEnabled('allow_waitlist'),
679 'waitlistAutoConfirm' => \Yatra\Services\SettingsService::isEnabled('waitlist_auto_confirm'),
680 'gateways' => $this->getGatewayFrontendConfigs(),
681 'enabledGateways' => $this->sanitizeGatewayConfigsForFrontend(\Yatra\Services\SettingsService::get('payment_gateways', [])),
682 // Server-side translated UI strings for booking.js. PHP __() resolves via .mo
683 // (reliable), so these stay translatable even when the JS-translation JSON
684 // chain (wp_set_script_translations) doesn't load on a given setup.
685 'i18n' => [
686 'complete_booking' => __('Complete Booking', 'yatra'),
687 'pay_now' => __('Pay Now', 'yatra'),
688 ],
689 ];
690
691 $bookingData = array_merge($bookingData, $this->getStripeFrontendBookingPayload());
692
693 wp_localize_script('yatra-booking', 'yatraBookingData', $bookingData);
694 }
695
696 /**
697 * Enqueue account page specific assets
698 *
699 * @return void
700 */
701 private function enqueueAccountAssets(): void
702 {
703 // Account bundle shares admin Vite chunks; CSS is extracted to admin/dist/css (ES modules do not auto-load it).
704 $reactVendorCss = YATRA_PLUGIN_PATH . 'assets/admin/dist/css/react-vendor.css';
705 if (file_exists($reactVendorCss)) {
706 wp_enqueue_style(
707 'yatra-account-react-vendor',
708 YATRA_PLUGIN_URL . 'assets/admin/dist/css/react-vendor.css',
709 [],
710 YATRA_VERSION . '.' . filemtime($reactVendorCss)
711 );
712 }
713
714 $accountUiCss = YATRA_PLUGIN_PATH . 'assets/admin/dist/css/index.css';
715 $accountUiDeps = file_exists($reactVendorCss) ? ['yatra-account-react-vendor'] : [];
716 if (file_exists($accountUiCss)) {
717 wp_enqueue_style(
718 'yatra-account-ui',
719 YATRA_PLUGIN_URL . 'assets/admin/dist/css/index.css',
720 $accountUiDeps,
721 YATRA_VERSION . '.' . filemtime($accountUiCss)
722 );
723 }
724
725 // Vite build outputs to assets/dist/js/account-page.js (ES module + shared chunks).
726 $accountJs = YATRA_PLUGIN_PATH . 'assets/dist/js/account-page.js';
727 if (!file_exists($accountJs)) {
728 return;
729 }
730
731 wp_enqueue_script(
732 'yatra-account-page',
733 YATRA_PLUGIN_URL . 'assets/dist/js/account-page.js',
734 [],
735 YATRA_VERSION . '.' . filemtime($accountJs),
736 true
737 );
738
739 wp_script_add_data('yatra-account-page', 'type', 'module');
740
741 wp_localize_script('yatra-account-page', 'yatraAccountPage', [
742 'apiUrl' => rest_url('yatra/v1'),
743 'nonce' => wp_create_nonce('wp_rest'),
744 'userId' => get_current_user_id(),
745 'siteUrl' => site_url(),
746 'logoutUrl' => wp_logout_url(home_url('/')),
747 'companyPhone' => \Yatra\Services\SettingsService::getString('company_phone', ''),
748 'companyName' => \Yatra\Services\SettingsService::getString('company_name', ''),
749 'companyEmail' => \Yatra\Services\SettingsService::getString('company_email', ''),
750 'currency' => \Yatra\Services\SettingsService::getCurrency(),
751 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
752 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
753 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(),
754 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
755 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
756 'locale' => get_locale(),
757 // Full ISO country map (code => name) so the account profile can show
758 // full country names and render the country dropdown. Mirrors the
759 // admin (`yatraAdmin.countries`); honours the `yatra_countries_list` filter.
760 'countries' => class_exists('\\Yatra\\Helpers\\FormatHelper')
761 ? \Yatra\Helpers\FormatHelper::getCountries()
762 : [],
763 'translations' => $this->getFrontendTranslations(),
764 'wishlistEnabled' => \Yatra\Services\SettingsService::wishlistEnabled(),
765 ]);
766
767 // Match admin React (`yatra-admin`): register Jed translations for this handle so `wp.i18n` resolves
768 // strings from PHP/Loco JSON catalogs. Without this, only keys in `translations` above work; the rest
769 // stay English because the account bundle is not in `yatra-admin`'s Jed file (different script hash).
770 if (function_exists('wp_set_script_translations')) {
771 wp_set_script_translations('yatra-account-page', 'yatra', YATRA_PLUGIN_PATH . 'i18n/languages');
772 }
773 }
774
775 /**
776 * Enqueue listing filters JavaScript
777 *
778 * @return void
779 */
780 private function enqueueListingFiltersJs(): void
781 {
782 $filtersJs = YATRA_PLUGIN_PATH . 'assets/js/listing-filters.js';
783 if (file_exists($filtersJs)) {
784 wp_enqueue_script(
785 'yatra-listing-filters',
786 YATRA_PLUGIN_URL . 'assets/js/listing-filters.js',
787 ['jquery'],
788 YATRA_VERSION . '.' . filemtime($filtersJs),
789 true
790 );
791
792 // Add currency formatting function
793 wp_add_inline_script('yatra-listing-filters', "
794 window.yatra_format_price = function(amount) {
795 if (!amount || amount == 0) return '" . esc_js(__('Contact for pricing', 'yatra')) . "';
796 const currency = window.yatraSettings?.currency || 'USD';
797 const symbol = window.yatraSettings?.currencySymbol || '$';
798 return symbol + amount.toLocaleString();
799 };
800 ");
801 }
802 }
803
804 /**
805 * Month and weekday labels for Flatpickr from {@see \WP_Locale} (site language).
806 *
807 * @return array<string, mixed>
808 */
809 private function buildFlatpickrLocalePayload(): array
810 {
811 global $wp_locale;
812
813 $first_day = (int) get_option('start_of_week', 1);
814 $first_day = max(0, min(6, $first_day));
815
816 if (!($wp_locale instanceof \WP_Locale)) {
817 return [
818 'firstDayOfWeek' => $first_day,
819 ];
820 }
821
822 // IMPORTANT — `month_abbrev` and `weekday_abbrev` are keyed by the
823 // TRANSLATED LONG NAME, not by a numeric index:
824 //
825 // $wp_locale->month['01'] = 'January' (or 'जनवरी', 'enero'…)
826 // $wp_locale->month_abbrev['January'] = 'Jan' (or 'जन', 'ene'…)
827 //
828 // An earlier version of this code mistakenly indexed
829 // month_abbrev by '01'..'12' / weekday_abbrev by 0..6, which
830 // ALWAYS returned null → flatpickr's locale.months.shorthand
831 // shipped as an array of empty strings → the `M` token in any
832 // altFormat rendered as nothing. Net effect: a date set to
833 // "19 May 2026" displayed as "19 2026" (no month) under any
834 // non-en_US locale that exposed the bug.
835 //
836 // WP_Locale exposes get_month_abbrev() / get_weekday_abbrev()
837 // which take the long name and do the right lookup. We use
838 // those so the indexing rule lives inside core, not here.
839 $months_long = [];
840 $months_short = [];
841 for ($m = 1; $m <= 12; ++$m) {
842 $key = sprintf('%02d', $m);
843 $long = $wp_locale->month[$key] ?? '';
844 $short = $long !== '' ? (string) $wp_locale->get_month_abbrev($long) : '';
845 $months_long[] = $long;
846 // Final fallback to the long name if the locale has no
847 // abbreviated form — better than shipping an empty string
848 // that flatpickr would render as blank.
849 $months_short[] = $short !== '' ? $short : $long;
850 }
851
852 $weekdays_long = [];
853 $weekdays_short = [];
854 for ($d = 0; $d <= 6; ++$d) {
855 $long = $wp_locale->weekday[$d] ?? '';
856 $short = $long !== '' ? (string) $wp_locale->get_weekday_abbrev($long) : '';
857 $weekdays_long[] = $long;
858 $weekdays_short[] = $short !== '' ? $short : $long;
859 }
860
861 $payload = [
862 'weekdays' => [
863 'shorthand' => $weekdays_short,
864 'longhand' => $weekdays_long,
865 ],
866 'months' => [
867 'shorthand' => $months_short,
868 'longhand' => $months_long,
869 ],
870 'firstDayOfWeek' => $first_day,
871 ];
872
873 return apply_filters('yatra_flatpickr_locale', $payload);
874 }
875
876 /**
877 * Get frontend translations
878 *
879 * @return array
880 */
881 private function getFrontendTranslations(): array
882 {
883 return [
884 // Account page
885 'My Account' => __('My Account', 'yatra'),
886 'My Bookings' => __('My Bookings', 'yatra'),
887 'Account Settings' => __('Account Settings', 'yatra'),
888 'Logout' => __('Logout', 'yatra'),
889 'Login' => __('Login', 'yatra'),
890 'Register' => __('Register', 'yatra'),
891
892 // Booking related
893 'Booking Details' => __('Booking Details', 'yatra'),
894 'Booking Status' => __('Booking Status', 'yatra'),
895 'Total Amount' => __('Total Amount', 'yatra'),
896 'Payment Status' => __('Payment Status', 'yatra'),
897 'View Details' => __('View Details', 'yatra'),
898
899 // Common
900 'Loading...' => __('Loading...', 'yatra'),
901 'No data available' => __('No data available', 'yatra'),
902 'Error loading data' => __('Error loading data', 'yatra'),
903 'Please try again' => __('Please try again', 'yatra'),
904
905 // Currency and pricing
906 'Contact for pricing' => __('Contact for pricing', 'yatra'),
907 'Free' => __('Free', 'yatra'),
908 'Price' => __('Price', 'yatra'),
909
910 // Trip related
911 'Trip Details' => __('Trip Details', 'yatra'),
912 'Duration' => __('Duration', 'yatra'),
913 'Difficulty' => __('Difficulty', 'yatra'),
914 'Group Size' => __('Group Size', 'yatra'),
915
916 // Navigation
917 'Home' => __('Home', 'yatra'),
918 'Trips' => __('Trips', 'yatra'),
919 'Destinations' => __('Destinations', 'yatra'),
920 'Activities' => __('Activities', 'yatra'),
921 'About Us' => __('About Us', 'yatra'),
922 'Contact' => __('Contact', 'yatra'),
923 ];
924 }
925
926 /**
927 * Enqueue assets for specific shortcodes
928 *
929 * @param array $shortcodes Array of shortcodes that need assets
930 * @return void
931 */
932 public function enqueueShortcodeAssets(array $shortcodes): void
933 {
934 global $post;
935
936 if (!$post || !has_shortcode($post->post_content, $shortcodes)) {
937 return;
938 }
939
940 // Enqueue common frontend assets for shortcodes
941 $this->enqueueCommonAssets();
942
943 // Shortcode-specific assets can be added here based on $shortcodes array
944 foreach ($shortcodes as $shortcode) {
945 switch ($shortcode) {
946 case 'yatra_trip_listing':
947 $this->enqueueTripListingAssets();
948 break;
949 case 'yatra_cart':
950 case 'yatra_checkout':
951 $this->enqueueBookingAssets();
952 break;
953 case 'yatra_my_account':
954 $this->enqueueAccountAssets();
955 break;
956 }
957 }
958 }
959
960 /**
961 * Enqueue assets conditionally based on custom conditions
962 *
963 * @param callable $condition Function that returns true if assets should be enqueued
964 * @return void
965 */
966 public function enqueueConditionalAssets(callable $condition): void
967 {
968 if ($condition()) {
969 $this->enqueueAssets();
970 }
971 }
972
973 /**
974 * Get asset URL with versioning
975 *
976 * @param string $path Relative path to asset
977 * @param string $type 'css' or 'js'
978 * @return string Asset URL or empty string if file doesn't exist
979 */
980 public function getAssetUrl(string $path, string $type = 'css'): string
981 {
982 $basePath = $type === 'css' ? 'assets/css/' : 'assets/js/';
983 $fullPath = YATRA_PLUGIN_PATH . $basePath . $path;
984
985 if (!file_exists($fullPath)) {
986 return '';
987 }
988
989 $version = YATRA_VERSION . '.' . filemtime($fullPath);
990 return YATRA_PLUGIN_URL . $basePath . $path . '?ver=' . $version;
991 }
992
993 /**
994 * Check if asset file exists
995 *
996 * @param string $path Relative path to asset
997 * @param string $type 'css' or 'js'
998 * @return bool
999 */
1000 public function assetExists(string $path, string $type = 'css'): bool
1001 {
1002 $basePath = $type === 'css' ? 'assets/css/' : 'assets/js/';
1003 $fullPath = YATRA_PLUGIN_PATH . $basePath . $path;
1004 return file_exists($fullPath);
1005 }
1006
1007 /**
1008 * Strip secret credentials from per-gateway config before it is localized
1009 * into the page (yatraBookingData). The stored payment_gateways option
1010 * holds private keys / access tokens that must NEVER reach the browser; the
1011 * checkout scripts only ever read public values (publishable keys, Square
1012 * application/location IDs, Authorize.Net public client key, the enabled
1013 * flag, etc.). This removes the known secret keys while preserving the
1014 * structure and every public field, so existing gateways/consumers are
1015 * unaffected — only secrets are dropped.
1016 *
1017 * @param mixed $gateways
1018 * @return array<string, mixed>
1019 */
1020 /**
1021 * Per-gateway PUBLIC config for the booking page, keyed by gateway id
1022 * (window.yatraBookingData.gateways.<id>). Checkout scripts read their public
1023 * settings from here — e.g. square.js → gateways.square.application_id /
1024 * location_id, authorizenet.js → gateways.authorize_net.public_client_key /
1025 * api_login_id.
1026 *
1027 * Source of truth is each ENABLED gateway's own getFrontendData(), i.e. an
1028 * allowlist the gateway itself declares. This is deliberately NOT a denylist
1029 * over the raw stored config: a denylist would leak any secret whose key we
1030 * forgot (e.g. Stripe live_secret_key / test_secret_key, Bank Transfer
1031 * account_number / routing_code). Gateways without a getFrontendData()
1032 * (Bank Transfer, PayPal, Pay Later, …) contribute nothing, so their stored
1033 * details never reach the browser. Disabled gateways are excluded.
1034 *
1035 * @return array<string, array<string, mixed>>
1036 */
1037 private function getGatewayFrontendConfigs(): array
1038 {
1039 if (!class_exists(\Yatra\PaymentGateways\PaymentGatewayRegistry::class)) {
1040 return [];
1041 }
1042
1043 $out = [];
1044 try {
1045 $registry = \Yatra\PaymentGateways\PaymentGatewayRegistry::getInstance();
1046 foreach ($registry->getEnabledGateways() as $id => $gateway) {
1047 if (!is_object($gateway) || !method_exists($gateway, 'getFrontendData')) {
1048 continue;
1049 }
1050 $data = $gateway->getFrontendData();
1051 if (is_array($data) && $data !== []) {
1052 $data['enabled'] = true;
1053 $out[(string) $id] = $data;
1054 }
1055 }
1056 } catch (\Throwable $e) {
1057 return [];
1058 }
1059
1060 return $out;
1061 }
1062
1063 private function sanitizeGatewayConfigsForFrontend($gateways): array
1064 {
1065 if (!is_array($gateways)) {
1066 return [];
1067 }
1068
1069 // Credential fields that are private to the server.
1070 $secretKeys = [
1071 'access_token',
1072 'api_key',
1073 'api_secret',
1074 'secret_key',
1075 'key_secret',
1076 'client_secret',
1077 'transaction_key',
1078 'webhook_secret',
1079 'webhook_signing_secret',
1080 'signing_secret',
1081 'private_key',
1082 'password',
1083 'secret',
1084 ];
1085
1086 $clean = [];
1087 foreach ($gateways as $id => $config) {
1088 if (is_array($config)) {
1089 foreach ($secretKeys as $secret) {
1090 unset($config[$secret]);
1091 }
1092 }
1093 $clean[$id] = $config;
1094 }
1095
1096 return $clean;
1097 }
1098
1099 /**
1100 * Stripe Elements (assets/js/stripe.js) expects publishableKey under yatraBookingData.stripe.
1101 * Mirror YatraPro StripeGateway::loadConfig() live/test selection; never expose secret keys.
1102 *
1103 * @return array<string, mixed>
1104 */
1105 private function getStripeFrontendBookingPayload(): array
1106 {
1107 $allConfigs = get_option('yatra_gateway_configs', []);
1108 if (is_string($allConfigs)) {
1109 $maybe = maybe_unserialize($allConfigs);
1110 $allConfigs = is_array($maybe) ? $maybe : [];
1111 }
1112 if (!is_array($allConfigs)) {
1113 $allConfigs = [];
1114 }
1115
1116 $stripe = isset($allConfigs['stripe']) && is_array($allConfigs['stripe'])
1117 ? $allConfigs['stripe']
1118 : [];
1119
1120 $test = filter_var(\Yatra\Services\SettingsService::get('payment_test_mode', true), FILTER_VALIDATE_BOOLEAN);
1121
1122 $livePub = trim((string) ($stripe['live_publishable_key'] ?? ''));
1123 $testPub = trim((string) ($stripe['test_publishable_key'] ?? ''));
1124
1125 $publishableKey = trim((string) ($stripe['api_key'] ?? ''));
1126 if ($test) {
1127 if ($testPub !== '') {
1128 $publishableKey = $testPub;
1129 }
1130 } elseif ($livePub !== '') {
1131 $publishableKey = $livePub;
1132 }
1133
1134 // Match StripeGateway default + admin multi-select when unset (was dropped by old sanitizer).
1135 $defaultMethods = 'card,google_pay,apple_pay';
1136 $enabledMethods = $stripe['enabled_methods'] ?? $defaultMethods;
1137 if (is_array($enabledMethods)) {
1138 // stripe.js accepts an array of method ids
1139 } elseif (!is_string($enabledMethods) || trim($enabledMethods) === '') {
1140 $enabledMethods = $defaultMethods;
1141 }
1142
1143 $companyCountry = trim((string) \Yatra\Services\SettingsService::get('company_country', ''));
1144 if ($companyCountry === '') {
1145 $companyCountry = 'US';
1146 }
1147
1148 $payload = [
1149 'stripe' => [
1150 'publishableKey' => $publishableKey,
1151 'enabledMethods' => $enabledMethods,
1152 ],
1153 'companyCountry' => $companyCountry,
1154 ];
1155
1156 return apply_filters('yatra_booking_stripe_frontend_data', $payload, $stripe, $test);
1157 }
1158 }
1159