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

1,133 lines 44.6 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 // 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 // Load each available gateway's own client scripts on the checkout page
590 // (e.g. Square Web Payments SDK + square.js, Authorize.Net Accept.js +
591 // its handler, Razorpay SDK + its handler). Every gateway's
592 // enqueueScripts() self-guards on isAvailable(), so only enabled +
593 // configured gateways load anything. This call was previously missing,
594 // so Pro gateways that render an inline card form shipped no JS to
595 // checkout and clicking "Pay" just span the button forever. It is
596 // additive and safe for the others: Stripe's enqueueScripts() is a
597 // no-op (Stripe is loaded via enqueueCommonJs), and PayPal/Pay Later
598 // have no client scripts.
599 if (class_exists(\Yatra\PaymentGateways\PaymentGatewayRegistry::class)) {
600 \Yatra\PaymentGateways\PaymentGatewayRegistry::getInstance()->enqueueScripts();
601 }
602
603 // Localize booking data for booking.js
604 $permalink_structure = get_option('permalink_structure') ?: '';
605 $is_plain = empty($permalink_structure);
606
607 $deposit_pct_store = (int) \Yatra\Services\SettingsService::get('deposit_percentage', 20);
608 $partial_pct_store = (int) \Yatra\Services\SettingsService::get('partial_payment_percentage', 30);
609 $deposit_pct_resolved = (int) apply_filters('yatra_deposit_percentage', $deposit_pct_store);
610 $partial_pct_resolved = (int) apply_filters('yatra_partial_payment_percentage', $partial_pct_store);
611 $flexible_payments_enabled = (bool) apply_filters('yatra_flexible_payments_enabled', false);
612 $flexible_module_on = class_exists(ModuleManager::class)
613 ? ModuleManager::isModuleEnabled('flexible_payments')
614 : false;
615
616 Logger::debug('Yatra booking localize: flexible payment snapshot', [
617 'context' => 'booking_localize',
618 'flexible_payments_enabled' => $flexible_payments_enabled,
619 'flexible_payments_module' => $flexible_module_on,
620 'partial_payment_setting' => (bool) \Yatra\Services\SettingsService::get('partial_payment', false),
621 'deposit_required_setting' => (bool) \Yatra\Services\SettingsService::get('deposit_required', false),
622 'deposit_percentage_store' => $deposit_pct_store,
623 'partial_percentage_store' => $partial_pct_store,
624 'deposit_percentage_resolved' => $deposit_pct_resolved,
625 'partial_percentage_resolved' => $partial_pct_resolved,
626 ]);
627
628 $bookingData = [
629 'apiUrl' => rest_url('yatra/v1'),
630 'restUrl' => rest_url(),
631 'siteUrl' => site_url(),
632 'bookingBase' => \Yatra\Services\SettingsService::getBookingBase(),
633 'permalinkStructure' => $is_plain ? 'plain' : $permalink_structure,
634 'nonce' => wp_create_nonce('wp_rest'),
635 // Booking-scoped CSRF nonce. See enqueueTripDetailAssets()
636 // for the rationale: the booking REST endpoint bypasses
637 // the WP REST cookie/nonce check (so guests can use it),
638 // and this token is what gates the actual booking write.
639 'bookingNonce' => wp_create_nonce('yatra_booking_action'),
640 'currency' => \Yatra\Services\SettingsService::getCurrency(),
641 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
642 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
643 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(),
644 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
645 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
646 // Payment gateways data
647 'paymentGateways' => $this->sanitizeGatewayConfigsForFrontend(apply_filters('yatra_payment_gateways', \Yatra\Services\SettingsService::get('payment_gateways', []))),
648 'paymentMethods' => \Yatra\Services\SettingsService::get('payment_methods', []),
649 'paymentTestMode' => \Yatra\Services\SettingsService::get('payment_test_mode', false),
650 'partialPayment' => \Yatra\Services\SettingsService::get('partial_payment', false),
651 'partialPaymentPercentage' => \Yatra\Services\SettingsService::get('partial_payment_percentage', 0),
652 // Flexible payments (Pro) — keep these keys stable for frontend booking.js.
653 // Values are resolved via generic filters so Pro can override without hard-coding premium logic here.
654 'depositRequired' => (bool) \Yatra\Services\SettingsService::get('deposit_required', false),
655 'depositPercentage' => $deposit_pct_resolved,
656 'partialPercentage' => $partial_pct_resolved,
657 'gatewayOrder' => \Yatra\Services\SettingsService::get('gateway_order', []),
658 'autoConfirmPayLater' => \Yatra\Services\SettingsService::get('auto_confirm_pay_later', true),
659 'allowWaitlist' => \Yatra\Services\SettingsService::isEnabled('allow_waitlist'),
660 'waitlistAutoConfirm' => \Yatra\Services\SettingsService::isEnabled('waitlist_auto_confirm'),
661 'gateways' => $this->getGatewayFrontendConfigs(),
662 'enabledGateways' => $this->sanitizeGatewayConfigsForFrontend(\Yatra\Services\SettingsService::get('payment_gateways', [])),
663 ];
664
665 $bookingData = array_merge($bookingData, $this->getStripeFrontendBookingPayload());
666
667 wp_localize_script('yatra-booking', 'yatraBookingData', $bookingData);
668 }
669
670 /**
671 * Enqueue account page specific assets
672 *
673 * @return void
674 */
675 private function enqueueAccountAssets(): void
676 {
677 // Account bundle shares admin Vite chunks; CSS is extracted to admin/dist/css (ES modules do not auto-load it).
678 $reactVendorCss = YATRA_PLUGIN_PATH . 'assets/admin/dist/css/react-vendor.css';
679 if (file_exists($reactVendorCss)) {
680 wp_enqueue_style(
681 'yatra-account-react-vendor',
682 YATRA_PLUGIN_URL . 'assets/admin/dist/css/react-vendor.css',
683 [],
684 YATRA_VERSION . '.' . filemtime($reactVendorCss)
685 );
686 }
687
688 $accountUiCss = YATRA_PLUGIN_PATH . 'assets/admin/dist/css/index.css';
689 $accountUiDeps = file_exists($reactVendorCss) ? ['yatra-account-react-vendor'] : [];
690 if (file_exists($accountUiCss)) {
691 wp_enqueue_style(
692 'yatra-account-ui',
693 YATRA_PLUGIN_URL . 'assets/admin/dist/css/index.css',
694 $accountUiDeps,
695 YATRA_VERSION . '.' . filemtime($accountUiCss)
696 );
697 }
698
699 // Vite build outputs to assets/dist/js/account-page.js (ES module + shared chunks).
700 $accountJs = YATRA_PLUGIN_PATH . 'assets/dist/js/account-page.js';
701 if (!file_exists($accountJs)) {
702 return;
703 }
704
705 wp_enqueue_script(
706 'yatra-account-page',
707 YATRA_PLUGIN_URL . 'assets/dist/js/account-page.js',
708 [],
709 YATRA_VERSION . '.' . filemtime($accountJs),
710 true
711 );
712
713 wp_script_add_data('yatra-account-page', 'type', 'module');
714
715 wp_localize_script('yatra-account-page', 'yatraAccountPage', [
716 'apiUrl' => rest_url('yatra/v1'),
717 'nonce' => wp_create_nonce('wp_rest'),
718 'userId' => get_current_user_id(),
719 'siteUrl' => site_url(),
720 'logoutUrl' => wp_logout_url(home_url('/')),
721 'companyPhone' => \Yatra\Services\SettingsService::getString('company_phone', ''),
722 'companyName' => \Yatra\Services\SettingsService::getString('company_name', ''),
723 'companyEmail' => \Yatra\Services\SettingsService::getString('company_email', ''),
724 'currency' => \Yatra\Services\SettingsService::getCurrency(),
725 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
726 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
727 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(),
728 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
729 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
730 'locale' => get_locale(),
731 // Full ISO country map (code => name) so the account profile can show
732 // full country names and render the country dropdown. Mirrors the
733 // admin (`yatraAdmin.countries`); honours the `yatra_countries_list` filter.
734 'countries' => class_exists('\\Yatra\\Helpers\\FormatHelper')
735 ? \Yatra\Helpers\FormatHelper::getCountries()
736 : [],
737 'translations' => $this->getFrontendTranslations(),
738 'wishlistEnabled' => \Yatra\Services\SettingsService::wishlistEnabled(),
739 ]);
740
741 // Match admin React (`yatra-admin`): register Jed translations for this handle so `wp.i18n` resolves
742 // strings from PHP/Loco JSON catalogs. Without this, only keys in `translations` above work; the rest
743 // stay English because the account bundle is not in `yatra-admin`'s Jed file (different script hash).
744 if (function_exists('wp_set_script_translations')) {
745 wp_set_script_translations('yatra-account-page', 'yatra', YATRA_PLUGIN_PATH . 'i18n/languages');
746 }
747 }
748
749 /**
750 * Enqueue listing filters JavaScript
751 *
752 * @return void
753 */
754 private function enqueueListingFiltersJs(): void
755 {
756 $filtersJs = YATRA_PLUGIN_PATH . 'assets/js/listing-filters.js';
757 if (file_exists($filtersJs)) {
758 wp_enqueue_script(
759 'yatra-listing-filters',
760 YATRA_PLUGIN_URL . 'assets/js/listing-filters.js',
761 ['jquery'],
762 YATRA_VERSION . '.' . filemtime($filtersJs),
763 true
764 );
765
766 // Add currency formatting function
767 wp_add_inline_script('yatra-listing-filters', "
768 window.yatra_format_price = function(amount) {
769 if (!amount || amount == 0) return '" . esc_js(__('Contact for pricing', 'yatra')) . "';
770 const currency = window.yatraSettings?.currency || 'USD';
771 const symbol = window.yatraSettings?.currencySymbol || '$';
772 return symbol + amount.toLocaleString();
773 };
774 ");
775 }
776 }
777
778 /**
779 * Month and weekday labels for Flatpickr from {@see \WP_Locale} (site language).
780 *
781 * @return array<string, mixed>
782 */
783 private function buildFlatpickrLocalePayload(): array
784 {
785 global $wp_locale;
786
787 $first_day = (int) get_option('start_of_week', 1);
788 $first_day = max(0, min(6, $first_day));
789
790 if (!($wp_locale instanceof \WP_Locale)) {
791 return [
792 'firstDayOfWeek' => $first_day,
793 ];
794 }
795
796 // IMPORTANT — `month_abbrev` and `weekday_abbrev` are keyed by the
797 // TRANSLATED LONG NAME, not by a numeric index:
798 //
799 // $wp_locale->month['01'] = 'January' (or 'जनवरी', 'enero'…)
800 // $wp_locale->month_abbrev['January'] = 'Jan' (or 'जन', 'ene'…)
801 //
802 // An earlier version of this code mistakenly indexed
803 // month_abbrev by '01'..'12' / weekday_abbrev by 0..6, which
804 // ALWAYS returned null → flatpickr's locale.months.shorthand
805 // shipped as an array of empty strings → the `M` token in any
806 // altFormat rendered as nothing. Net effect: a date set to
807 // "19 May 2026" displayed as "19 2026" (no month) under any
808 // non-en_US locale that exposed the bug.
809 //
810 // WP_Locale exposes get_month_abbrev() / get_weekday_abbrev()
811 // which take the long name and do the right lookup. We use
812 // those so the indexing rule lives inside core, not here.
813 $months_long = [];
814 $months_short = [];
815 for ($m = 1; $m <= 12; ++$m) {
816 $key = sprintf('%02d', $m);
817 $long = $wp_locale->month[$key] ?? '';
818 $short = $long !== '' ? (string) $wp_locale->get_month_abbrev($long) : '';
819 $months_long[] = $long;
820 // Final fallback to the long name if the locale has no
821 // abbreviated form — better than shipping an empty string
822 // that flatpickr would render as blank.
823 $months_short[] = $short !== '' ? $short : $long;
824 }
825
826 $weekdays_long = [];
827 $weekdays_short = [];
828 for ($d = 0; $d <= 6; ++$d) {
829 $long = $wp_locale->weekday[$d] ?? '';
830 $short = $long !== '' ? (string) $wp_locale->get_weekday_abbrev($long) : '';
831 $weekdays_long[] = $long;
832 $weekdays_short[] = $short !== '' ? $short : $long;
833 }
834
835 $payload = [
836 'weekdays' => [
837 'shorthand' => $weekdays_short,
838 'longhand' => $weekdays_long,
839 ],
840 'months' => [
841 'shorthand' => $months_short,
842 'longhand' => $months_long,
843 ],
844 'firstDayOfWeek' => $first_day,
845 ];
846
847 return apply_filters('yatra_flatpickr_locale', $payload);
848 }
849
850 /**
851 * Get frontend translations
852 *
853 * @return array
854 */
855 private function getFrontendTranslations(): array
856 {
857 return [
858 // Account page
859 'My Account' => __('My Account', 'yatra'),
860 'My Bookings' => __('My Bookings', 'yatra'),
861 'Account Settings' => __('Account Settings', 'yatra'),
862 'Logout' => __('Logout', 'yatra'),
863 'Login' => __('Login', 'yatra'),
864 'Register' => __('Register', 'yatra'),
865
866 // Booking related
867 'Booking Details' => __('Booking Details', 'yatra'),
868 'Booking Status' => __('Booking Status', 'yatra'),
869 'Total Amount' => __('Total Amount', 'yatra'),
870 'Payment Status' => __('Payment Status', 'yatra'),
871 'View Details' => __('View Details', 'yatra'),
872
873 // Common
874 'Loading...' => __('Loading...', 'yatra'),
875 'No data available' => __('No data available', 'yatra'),
876 'Error loading data' => __('Error loading data', 'yatra'),
877 'Please try again' => __('Please try again', 'yatra'),
878
879 // Currency and pricing
880 'Contact for pricing' => __('Contact for pricing', 'yatra'),
881 'Free' => __('Free', 'yatra'),
882 'Price' => __('Price', 'yatra'),
883
884 // Trip related
885 'Trip Details' => __('Trip Details', 'yatra'),
886 'Duration' => __('Duration', 'yatra'),
887 'Difficulty' => __('Difficulty', 'yatra'),
888 'Group Size' => __('Group Size', 'yatra'),
889
890 // Navigation
891 'Home' => __('Home', 'yatra'),
892 'Trips' => __('Trips', 'yatra'),
893 'Destinations' => __('Destinations', 'yatra'),
894 'Activities' => __('Activities', 'yatra'),
895 'About Us' => __('About Us', 'yatra'),
896 'Contact' => __('Contact', 'yatra'),
897 ];
898 }
899
900 /**
901 * Enqueue assets for specific shortcodes
902 *
903 * @param array $shortcodes Array of shortcodes that need assets
904 * @return void
905 */
906 public function enqueueShortcodeAssets(array $shortcodes): void
907 {
908 global $post;
909
910 if (!$post || !has_shortcode($post->post_content, $shortcodes)) {
911 return;
912 }
913
914 // Enqueue common frontend assets for shortcodes
915 $this->enqueueCommonAssets();
916
917 // Shortcode-specific assets can be added here based on $shortcodes array
918 foreach ($shortcodes as $shortcode) {
919 switch ($shortcode) {
920 case 'yatra_trip_listing':
921 $this->enqueueTripListingAssets();
922 break;
923 case 'yatra_cart':
924 case 'yatra_checkout':
925 $this->enqueueBookingAssets();
926 break;
927 case 'yatra_my_account':
928 $this->enqueueAccountAssets();
929 break;
930 }
931 }
932 }
933
934 /**
935 * Enqueue assets conditionally based on custom conditions
936 *
937 * @param callable $condition Function that returns true if assets should be enqueued
938 * @return void
939 */
940 public function enqueueConditionalAssets(callable $condition): void
941 {
942 if ($condition()) {
943 $this->enqueueAssets();
944 }
945 }
946
947 /**
948 * Get asset URL with versioning
949 *
950 * @param string $path Relative path to asset
951 * @param string $type 'css' or 'js'
952 * @return string Asset URL or empty string if file doesn't exist
953 */
954 public function getAssetUrl(string $path, string $type = 'css'): string
955 {
956 $basePath = $type === 'css' ? 'assets/css/' : 'assets/js/';
957 $fullPath = YATRA_PLUGIN_PATH . $basePath . $path;
958
959 if (!file_exists($fullPath)) {
960 return '';
961 }
962
963 $version = YATRA_VERSION . '.' . filemtime($fullPath);
964 return YATRA_PLUGIN_URL . $basePath . $path . '?ver=' . $version;
965 }
966
967 /**
968 * Check if asset file exists
969 *
970 * @param string $path Relative path to asset
971 * @param string $type 'css' or 'js'
972 * @return bool
973 */
974 public function assetExists(string $path, string $type = 'css'): bool
975 {
976 $basePath = $type === 'css' ? 'assets/css/' : 'assets/js/';
977 $fullPath = YATRA_PLUGIN_PATH . $basePath . $path;
978 return file_exists($fullPath);
979 }
980
981 /**
982 * Strip secret credentials from per-gateway config before it is localized
983 * into the page (yatraBookingData). The stored payment_gateways option
984 * holds private keys / access tokens that must NEVER reach the browser; the
985 * checkout scripts only ever read public values (publishable keys, Square
986 * application/location IDs, Authorize.Net public client key, the enabled
987 * flag, etc.). This removes the known secret keys while preserving the
988 * structure and every public field, so existing gateways/consumers are
989 * unaffected — only secrets are dropped.
990 *
991 * @param mixed $gateways
992 * @return array<string, mixed>
993 */
994 /**
995 * Per-gateway PUBLIC config for the booking page, keyed by gateway id
996 * (window.yatraBookingData.gateways.<id>). Checkout scripts read their public
997 * settings from here — e.g. square.js → gateways.square.application_id /
998 * location_id, authorizenet.js → gateways.authorize_net.public_client_key /
999 * api_login_id.
1000 *
1001 * Source of truth is each ENABLED gateway's own getFrontendData(), i.e. an
1002 * allowlist the gateway itself declares. This is deliberately NOT a denylist
1003 * over the raw stored config: a denylist would leak any secret whose key we
1004 * forgot (e.g. Stripe live_secret_key / test_secret_key, Bank Transfer
1005 * account_number / routing_code). Gateways without a getFrontendData()
1006 * (Bank Transfer, PayPal, Pay Later, …) contribute nothing, so their stored
1007 * details never reach the browser. Disabled gateways are excluded.
1008 *
1009 * @return array<string, array<string, mixed>>
1010 */
1011 private function getGatewayFrontendConfigs(): array
1012 {
1013 if (!class_exists(\Yatra\PaymentGateways\PaymentGatewayRegistry::class)) {
1014 return [];
1015 }
1016
1017 $out = [];
1018 try {
1019 $registry = \Yatra\PaymentGateways\PaymentGatewayRegistry::getInstance();
1020 foreach ($registry->getEnabledGateways() as $id => $gateway) {
1021 if (!is_object($gateway) || !method_exists($gateway, 'getFrontendData')) {
1022 continue;
1023 }
1024 $data = $gateway->getFrontendData();
1025 if (is_array($data) && $data !== []) {
1026 $data['enabled'] = true;
1027 $out[(string) $id] = $data;
1028 }
1029 }
1030 } catch (\Throwable $e) {
1031 return [];
1032 }
1033
1034 return $out;
1035 }
1036
1037 private function sanitizeGatewayConfigsForFrontend($gateways): array
1038 {
1039 if (!is_array($gateways)) {
1040 return [];
1041 }
1042
1043 // Credential fields that are private to the server.
1044 $secretKeys = [
1045 'access_token',
1046 'api_key',
1047 'api_secret',
1048 'secret_key',
1049 'key_secret',
1050 'client_secret',
1051 'transaction_key',
1052 'webhook_secret',
1053 'webhook_signing_secret',
1054 'signing_secret',
1055 'private_key',
1056 'password',
1057 'secret',
1058 ];
1059
1060 $clean = [];
1061 foreach ($gateways as $id => $config) {
1062 if (is_array($config)) {
1063 foreach ($secretKeys as $secret) {
1064 unset($config[$secret]);
1065 }
1066 }
1067 $clean[$id] = $config;
1068 }
1069
1070 return $clean;
1071 }
1072
1073 /**
1074 * Stripe Elements (assets/js/stripe.js) expects publishableKey under yatraBookingData.stripe.
1075 * Mirror YatraPro StripeGateway::loadConfig() live/test selection; never expose secret keys.
1076 *
1077 * @return array<string, mixed>
1078 */
1079 private function getStripeFrontendBookingPayload(): array
1080 {
1081 $allConfigs = get_option('yatra_gateway_configs', []);
1082 if (is_string($allConfigs)) {
1083 $maybe = maybe_unserialize($allConfigs);
1084 $allConfigs = is_array($maybe) ? $maybe : [];
1085 }
1086 if (!is_array($allConfigs)) {
1087 $allConfigs = [];
1088 }
1089
1090 $stripe = isset($allConfigs['stripe']) && is_array($allConfigs['stripe'])
1091 ? $allConfigs['stripe']
1092 : [];
1093
1094 $test = filter_var(\Yatra\Services\SettingsService::get('payment_test_mode', true), FILTER_VALIDATE_BOOLEAN);
1095
1096 $livePub = trim((string) ($stripe['live_publishable_key'] ?? ''));
1097 $testPub = trim((string) ($stripe['test_publishable_key'] ?? ''));
1098
1099 $publishableKey = trim((string) ($stripe['api_key'] ?? ''));
1100 if ($test) {
1101 if ($testPub !== '') {
1102 $publishableKey = $testPub;
1103 }
1104 } elseif ($livePub !== '') {
1105 $publishableKey = $livePub;
1106 }
1107
1108 // Match StripeGateway default + admin multi-select when unset (was dropped by old sanitizer).
1109 $defaultMethods = 'card,google_pay,apple_pay';
1110 $enabledMethods = $stripe['enabled_methods'] ?? $defaultMethods;
1111 if (is_array($enabledMethods)) {
1112 // stripe.js accepts an array of method ids
1113 } elseif (!is_string($enabledMethods) || trim($enabledMethods) === '') {
1114 $enabledMethods = $defaultMethods;
1115 }
1116
1117 $companyCountry = trim((string) \Yatra\Services\SettingsService::get('company_country', ''));
1118 if ($companyCountry === '') {
1119 $companyCountry = 'US';
1120 }
1121
1122 $payload = [
1123 'stripe' => [
1124 'publishableKey' => $publishableKey,
1125 'enabledMethods' => $enabledMethods,
1126 ],
1127 'companyCountry' => $companyCountry,
1128 ];
1129
1130 return apply_filters('yatra_booking_stripe_frontend_data', $payload, $stripe, $test);
1131 }
1132 }
1133