PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5.1
Yatra – Travel Booking & Tour Operator Software v3.0.5.1
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / app / Providers / FrontendAssetsProvider.php

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

1,021 lines 40.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 * 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' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'),
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 // Enqueue booking-specific JavaScript
578 $bookingJs = YATRA_PLUGIN_PATH . 'assets/js/booking.js';
579 if (file_exists($bookingJs)) {
580 wp_enqueue_script(
581 'yatra-booking',
582 YATRA_PLUGIN_URL . 'assets/js/booking.js',
583 ['jquery'],
584 YATRA_VERSION . '.' . filemtime($bookingJs),
585 true
586 );
587 }
588
589 // Localize booking data for booking.js
590 $permalink_structure = get_option('permalink_structure') ?: '';
591 $is_plain = empty($permalink_structure);
592
593 $deposit_pct_store = (int) \Yatra\Services\SettingsService::get('deposit_percentage', 20);
594 $partial_pct_store = (int) \Yatra\Services\SettingsService::get('partial_payment_percentage', 30);
595 $deposit_pct_resolved = (int) apply_filters('yatra_deposit_percentage', $deposit_pct_store);
596 $partial_pct_resolved = (int) apply_filters('yatra_partial_payment_percentage', $partial_pct_store);
597 $flexible_payments_enabled = (bool) apply_filters('yatra_flexible_payments_enabled', false);
598 $flexible_module_on = class_exists(ModuleManager::class)
599 ? ModuleManager::isModuleEnabled('flexible_payments')
600 : false;
601
602 Logger::debug('Yatra booking localize: flexible payment snapshot', [
603 'context' => 'booking_localize',
604 'flexible_payments_enabled' => $flexible_payments_enabled,
605 'flexible_payments_module' => $flexible_module_on,
606 'partial_payment_setting' => (bool) \Yatra\Services\SettingsService::get('partial_payment', false),
607 'deposit_required_setting' => (bool) \Yatra\Services\SettingsService::get('deposit_required', false),
608 'deposit_percentage_store' => $deposit_pct_store,
609 'partial_percentage_store' => $partial_pct_store,
610 'deposit_percentage_resolved' => $deposit_pct_resolved,
611 'partial_percentage_resolved' => $partial_pct_resolved,
612 ]);
613
614 $bookingData = [
615 'apiUrl' => rest_url('yatra/v1'),
616 'restUrl' => rest_url(),
617 'siteUrl' => site_url(),
618 'bookingBase' => \Yatra\Services\SettingsService::getBookingBase(),
619 'permalinkStructure' => $is_plain ? 'plain' : $permalink_structure,
620 'nonce' => wp_create_nonce('wp_rest'),
621 // Booking-scoped CSRF nonce. See enqueueTripDetailAssets()
622 // for the rationale: the booking REST endpoint bypasses
623 // the WP REST cookie/nonce check (so guests can use it),
624 // and this token is what gates the actual booking write.
625 'bookingNonce' => wp_create_nonce('yatra_booking_action'),
626 'currency' => \Yatra\Services\SettingsService::getCurrency(),
627 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
628 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
629 'decimalPlaces' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'),
630 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
631 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
632 // Payment gateways data
633 'paymentGateways' => apply_filters('yatra_payment_gateways', \Yatra\Services\SettingsService::get('payment_gateways', [])),
634 'paymentMethods' => \Yatra\Services\SettingsService::get('payment_methods', []),
635 'paymentTestMode' => \Yatra\Services\SettingsService::get('payment_test_mode', false),
636 'partialPayment' => \Yatra\Services\SettingsService::get('partial_payment', false),
637 'partialPaymentPercentage' => \Yatra\Services\SettingsService::get('partial_payment_percentage', 0),
638 // Flexible payments (Pro) — keep these keys stable for frontend booking.js.
639 // Values are resolved via generic filters so Pro can override without hard-coding premium logic here.
640 'depositRequired' => (bool) \Yatra\Services\SettingsService::get('deposit_required', false),
641 'depositPercentage' => $deposit_pct_resolved,
642 'partialPercentage' => $partial_pct_resolved,
643 'gatewayOrder' => \Yatra\Services\SettingsService::get('gateway_order', []),
644 'autoConfirmPayLater' => \Yatra\Services\SettingsService::get('auto_confirm_pay_later', true),
645 'allowWaitlist' => \Yatra\Services\SettingsService::isEnabled('allow_waitlist'),
646 'waitlistAutoConfirm' => \Yatra\Services\SettingsService::isEnabled('waitlist_auto_confirm'),
647 'gateways' => apply_filters('yatra_payment_gateways', \Yatra\Services\SettingsService::get('payment_gateways', [])),
648 'enabledGateways' => \Yatra\Services\SettingsService::get('payment_gateways', []),
649 ];
650
651 $bookingData = array_merge($bookingData, $this->getStripeFrontendBookingPayload());
652
653 wp_localize_script('yatra-booking', 'yatraBookingData', $bookingData);
654 }
655
656 /**
657 * Enqueue account page specific assets
658 *
659 * @return void
660 */
661 private function enqueueAccountAssets(): void
662 {
663 // Account bundle shares admin Vite chunks; CSS is extracted to admin/dist/css (ES modules do not auto-load it).
664 $reactVendorCss = YATRA_PLUGIN_PATH . 'assets/admin/dist/css/react-vendor.css';
665 if (file_exists($reactVendorCss)) {
666 wp_enqueue_style(
667 'yatra-account-react-vendor',
668 YATRA_PLUGIN_URL . 'assets/admin/dist/css/react-vendor.css',
669 [],
670 YATRA_VERSION . '.' . filemtime($reactVendorCss)
671 );
672 }
673
674 $accountUiCss = YATRA_PLUGIN_PATH . 'assets/admin/dist/css/index.css';
675 $accountUiDeps = file_exists($reactVendorCss) ? ['yatra-account-react-vendor'] : [];
676 if (file_exists($accountUiCss)) {
677 wp_enqueue_style(
678 'yatra-account-ui',
679 YATRA_PLUGIN_URL . 'assets/admin/dist/css/index.css',
680 $accountUiDeps,
681 YATRA_VERSION . '.' . filemtime($accountUiCss)
682 );
683 }
684
685 // Vite build outputs to assets/dist/js/account-page.js (ES module + shared chunks).
686 $accountJs = YATRA_PLUGIN_PATH . 'assets/dist/js/account-page.js';
687 if (!file_exists($accountJs)) {
688 return;
689 }
690
691 wp_enqueue_script(
692 'yatra-account-page',
693 YATRA_PLUGIN_URL . 'assets/dist/js/account-page.js',
694 [],
695 YATRA_VERSION . '.' . filemtime($accountJs),
696 true
697 );
698
699 wp_script_add_data('yatra-account-page', 'type', 'module');
700
701 wp_localize_script('yatra-account-page', 'yatraAccountPage', [
702 'apiUrl' => rest_url('yatra/v1'),
703 'nonce' => wp_create_nonce('wp_rest'),
704 'userId' => get_current_user_id(),
705 'siteUrl' => site_url(),
706 'logoutUrl' => wp_logout_url(home_url('/')),
707 'companyPhone' => \Yatra\Services\SettingsService::getString('company_phone', ''),
708 'companyName' => \Yatra\Services\SettingsService::getString('company_name', ''),
709 'companyEmail' => \Yatra\Services\SettingsService::getString('company_email', ''),
710 'currency' => \Yatra\Services\SettingsService::getCurrency(),
711 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
712 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
713 'decimalPlaces' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'),
714 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
715 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
716 'locale' => get_locale(),
717 'translations' => $this->getFrontendTranslations(),
718 'wishlistEnabled' => \Yatra\Services\SettingsService::wishlistEnabled(),
719 ]);
720
721 // Match admin React (`yatra-admin`): register Jed translations for this handle so `wp.i18n` resolves
722 // strings from PHP/Loco JSON catalogs. Without this, only keys in `translations` above work; the rest
723 // stay English because the account bundle is not in `yatra-admin`'s Jed file (different script hash).
724 if (function_exists('wp_set_script_translations')) {
725 wp_set_script_translations('yatra-account-page', 'yatra', YATRA_PLUGIN_PATH . 'i18n/languages');
726 }
727 }
728
729 /**
730 * Enqueue listing filters JavaScript
731 *
732 * @return void
733 */
734 private function enqueueListingFiltersJs(): void
735 {
736 $filtersJs = YATRA_PLUGIN_PATH . 'assets/js/listing-filters.js';
737 if (file_exists($filtersJs)) {
738 wp_enqueue_script(
739 'yatra-listing-filters',
740 YATRA_PLUGIN_URL . 'assets/js/listing-filters.js',
741 ['jquery'],
742 YATRA_VERSION . '.' . filemtime($filtersJs),
743 true
744 );
745
746 // Add currency formatting function
747 wp_add_inline_script('yatra-listing-filters', "
748 window.yatra_format_price = function(amount) {
749 if (!amount || amount == 0) return '" . esc_js(__('Contact for pricing', 'yatra')) . "';
750 const currency = window.yatraSettings?.currency || 'USD';
751 const symbol = window.yatraSettings?.currencySymbol || '$';
752 return symbol + amount.toLocaleString();
753 };
754 ");
755 }
756 }
757
758 /**
759 * Month and weekday labels for Flatpickr from {@see \WP_Locale} (site language).
760 *
761 * @return array<string, mixed>
762 */
763 private function buildFlatpickrLocalePayload(): array
764 {
765 global $wp_locale;
766
767 $first_day = (int) get_option('start_of_week', 1);
768 $first_day = max(0, min(6, $first_day));
769
770 if (!($wp_locale instanceof \WP_Locale)) {
771 return [
772 'firstDayOfWeek' => $first_day,
773 ];
774 }
775
776 // IMPORTANT — `month_abbrev` and `weekday_abbrev` are keyed by the
777 // TRANSLATED LONG NAME, not by a numeric index:
778 //
779 // $wp_locale->month['01'] = 'January' (or 'जनवरी', 'enero'…)
780 // $wp_locale->month_abbrev['January'] = 'Jan' (or 'जन', 'ene'…)
781 //
782 // An earlier version of this code mistakenly indexed
783 // month_abbrev by '01'..'12' / weekday_abbrev by 0..6, which
784 // ALWAYS returned null → flatpickr's locale.months.shorthand
785 // shipped as an array of empty strings → the `M` token in any
786 // altFormat rendered as nothing. Net effect: a date set to
787 // "19 May 2026" displayed as "19 2026" (no month) under any
788 // non-en_US locale that exposed the bug.
789 //
790 // WP_Locale exposes get_month_abbrev() / get_weekday_abbrev()
791 // which take the long name and do the right lookup. We use
792 // those so the indexing rule lives inside core, not here.
793 $months_long = [];
794 $months_short = [];
795 for ($m = 1; $m <= 12; ++$m) {
796 $key = sprintf('%02d', $m);
797 $long = $wp_locale->month[$key] ?? '';
798 $short = $long !== '' ? (string) $wp_locale->get_month_abbrev($long) : '';
799 $months_long[] = $long;
800 // Final fallback to the long name if the locale has no
801 // abbreviated form — better than shipping an empty string
802 // that flatpickr would render as blank.
803 $months_short[] = $short !== '' ? $short : $long;
804 }
805
806 $weekdays_long = [];
807 $weekdays_short = [];
808 for ($d = 0; $d <= 6; ++$d) {
809 $long = $wp_locale->weekday[$d] ?? '';
810 $short = $long !== '' ? (string) $wp_locale->get_weekday_abbrev($long) : '';
811 $weekdays_long[] = $long;
812 $weekdays_short[] = $short !== '' ? $short : $long;
813 }
814
815 $payload = [
816 'weekdays' => [
817 'shorthand' => $weekdays_short,
818 'longhand' => $weekdays_long,
819 ],
820 'months' => [
821 'shorthand' => $months_short,
822 'longhand' => $months_long,
823 ],
824 'firstDayOfWeek' => $first_day,
825 ];
826
827 return apply_filters('yatra_flatpickr_locale', $payload);
828 }
829
830 /**
831 * Get frontend translations
832 *
833 * @return array
834 */
835 private function getFrontendTranslations(): array
836 {
837 return [
838 // Account page
839 'My Account' => __('My Account', 'yatra'),
840 'My Bookings' => __('My Bookings', 'yatra'),
841 'Account Settings' => __('Account Settings', 'yatra'),
842 'Logout' => __('Logout', 'yatra'),
843 'Login' => __('Login', 'yatra'),
844 'Register' => __('Register', 'yatra'),
845
846 // Booking related
847 'Booking Details' => __('Booking Details', 'yatra'),
848 'Booking Status' => __('Booking Status', 'yatra'),
849 'Total Amount' => __('Total Amount', 'yatra'),
850 'Payment Status' => __('Payment Status', 'yatra'),
851 'View Details' => __('View Details', 'yatra'),
852
853 // Common
854 'Loading...' => __('Loading...', 'yatra'),
855 'No data available' => __('No data available', 'yatra'),
856 'Error loading data' => __('Error loading data', 'yatra'),
857 'Please try again' => __('Please try again', 'yatra'),
858
859 // Currency and pricing
860 'Contact for pricing' => __('Contact for pricing', 'yatra'),
861 'Free' => __('Free', 'yatra'),
862 'Price' => __('Price', 'yatra'),
863
864 // Trip related
865 'Trip Details' => __('Trip Details', 'yatra'),
866 'Duration' => __('Duration', 'yatra'),
867 'Difficulty' => __('Difficulty', 'yatra'),
868 'Group Size' => __('Group Size', 'yatra'),
869
870 // Navigation
871 'Home' => __('Home', 'yatra'),
872 'Trips' => __('Trips', 'yatra'),
873 'Destinations' => __('Destinations', 'yatra'),
874 'Activities' => __('Activities', 'yatra'),
875 'About Us' => __('About Us', 'yatra'),
876 'Contact' => __('Contact', 'yatra'),
877 ];
878 }
879
880 /**
881 * Enqueue assets for specific shortcodes
882 *
883 * @param array $shortcodes Array of shortcodes that need assets
884 * @return void
885 */
886 public function enqueueShortcodeAssets(array $shortcodes): void
887 {
888 global $post;
889
890 if (!$post || !has_shortcode($post->post_content, $shortcodes)) {
891 return;
892 }
893
894 // Enqueue common frontend assets for shortcodes
895 $this->enqueueCommonAssets();
896
897 // Shortcode-specific assets can be added here based on $shortcodes array
898 foreach ($shortcodes as $shortcode) {
899 switch ($shortcode) {
900 case 'yatra_trip_listing':
901 $this->enqueueTripListingAssets();
902 break;
903 case 'yatra_cart':
904 case 'yatra_checkout':
905 $this->enqueueBookingAssets();
906 break;
907 case 'yatra_my_account':
908 $this->enqueueAccountAssets();
909 break;
910 }
911 }
912 }
913
914 /**
915 * Enqueue assets conditionally based on custom conditions
916 *
917 * @param callable $condition Function that returns true if assets should be enqueued
918 * @return void
919 */
920 public function enqueueConditionalAssets(callable $condition): void
921 {
922 if ($condition()) {
923 $this->enqueueAssets();
924 }
925 }
926
927 /**
928 * Get asset URL with versioning
929 *
930 * @param string $path Relative path to asset
931 * @param string $type 'css' or 'js'
932 * @return string Asset URL or empty string if file doesn't exist
933 */
934 public function getAssetUrl(string $path, string $type = 'css'): string
935 {
936 $basePath = $type === 'css' ? 'assets/css/' : 'assets/js/';
937 $fullPath = YATRA_PLUGIN_PATH . $basePath . $path;
938
939 if (!file_exists($fullPath)) {
940 return '';
941 }
942
943 $version = YATRA_VERSION . '.' . filemtime($fullPath);
944 return YATRA_PLUGIN_URL . $basePath . $path . '?ver=' . $version;
945 }
946
947 /**
948 * Check if asset file exists
949 *
950 * @param string $path Relative path to asset
951 * @param string $type 'css' or 'js'
952 * @return bool
953 */
954 public function assetExists(string $path, string $type = 'css'): bool
955 {
956 $basePath = $type === 'css' ? 'assets/css/' : 'assets/js/';
957 $fullPath = YATRA_PLUGIN_PATH . $basePath . $path;
958 return file_exists($fullPath);
959 }
960
961 /**
962 * Stripe Elements (assets/js/stripe.js) expects publishableKey under yatraBookingData.stripe.
963 * Mirror YatraPro StripeGateway::loadConfig() live/test selection; never expose secret keys.
964 *
965 * @return array<string, mixed>
966 */
967 private function getStripeFrontendBookingPayload(): array
968 {
969 $allConfigs = get_option('yatra_gateway_configs', []);
970 if (is_string($allConfigs)) {
971 $maybe = maybe_unserialize($allConfigs);
972 $allConfigs = is_array($maybe) ? $maybe : [];
973 }
974 if (!is_array($allConfigs)) {
975 $allConfigs = [];
976 }
977
978 $stripe = isset($allConfigs['stripe']) && is_array($allConfigs['stripe'])
979 ? $allConfigs['stripe']
980 : [];
981
982 $test = filter_var(\Yatra\Services\SettingsService::get('payment_test_mode', true), FILTER_VALIDATE_BOOLEAN);
983
984 $livePub = trim((string) ($stripe['live_publishable_key'] ?? ''));
985 $testPub = trim((string) ($stripe['test_publishable_key'] ?? ''));
986
987 $publishableKey = trim((string) ($stripe['api_key'] ?? ''));
988 if ($test) {
989 if ($testPub !== '') {
990 $publishableKey = $testPub;
991 }
992 } elseif ($livePub !== '') {
993 $publishableKey = $livePub;
994 }
995
996 // Match StripeGateway default + admin multi-select when unset (was dropped by old sanitizer).
997 $defaultMethods = 'card,google_pay,apple_pay';
998 $enabledMethods = $stripe['enabled_methods'] ?? $defaultMethods;
999 if (is_array($enabledMethods)) {
1000 // stripe.js accepts an array of method ids
1001 } elseif (!is_string($enabledMethods) || trim($enabledMethods) === '') {
1002 $enabledMethods = $defaultMethods;
1003 }
1004
1005 $companyCountry = trim((string) \Yatra\Services\SettingsService::get('company_country', ''));
1006 if ($companyCountry === '') {
1007 $companyCountry = 'US';
1008 }
1009
1010 $payload = [
1011 'stripe' => [
1012 'publishableKey' => $publishableKey,
1013 'enabledMethods' => $enabledMethods,
1014 ],
1015 'companyCountry' => $companyCountry,
1016 ];
1017
1018 return apply_filters('yatra_booking_stripe_frontend_data', $payload, $stripe, $test);
1019 }
1020 }
1021