| @@ -24,13 +24,98 @@ | ||
| 24 | 24 | * @return void |
| 25 | 25 | */ |
| 26 | 26 | public function register(): void |
| 27 | 27 | { |
| 28 | + add_action('init', [self::class, 'registerCoreFrontendStylesheets'], 5); | |
| 28 | 29 | // Hook into WordPress to enqueue assets |
| 29 | 30 | add_action('wp_enqueue_scripts', [$this, 'enqueueAssets']); |
| 30 | 31 | } |
| 31 | 32 | |
| 32 | 33 | /** |
| 34 | + * Resolve the effective trip-listing card layout for one listing. | |
| 35 | + * | |
| 36 | + * The site-wide Design setting (`frontend_listing_card_layout`) is the | |
| 37 | + * default; a shortcode/block may pass a per-instance override. Anything that | |
| 38 | + * is not a concrete layout (empty, "inherit", or an unknown value) falls | |
| 39 | + * back to the global setting, so existing shortcodes/blocks are unchanged. | |
| 40 | + * | |
| 41 | + * @param string $override Per-instance override (card_layout / cardLayout). | |
| 42 | + * @return string One of: standard | compact_mobile | compact_all. | |
| 43 | + */ | |
| 44 | + public static function resolveListingLayout(string $override = ''): string | |
| 45 | + { | |
| 46 | + $override = strtolower(trim($override)); | |
| 47 | + if (in_array($override, ['standard', 'compact_mobile', 'compact_all'], true)) { | |
| 48 | + return $override; | |
| 49 | + } | |
| 50 | + | |
| 51 | + $global = \Yatra\Services\SettingsService::getString('frontend_listing_card_layout', 'standard'); | |
| 52 | + return in_array($global, ['standard', 'compact_mobile', 'compact_all'], true) ? $global : 'standard'; | |
| 53 | + } | |
| 54 | + | |
| 55 | + /** | |
| 56 | + * Map a resolved layout to the container CSS class(es) the listing CSS keys | |
| 57 | + * off. Placed on the listing container (archive .yatra-listing-page, or the | |
| 58 | + * shortcode/block wrapper) so each listing can carry its own layout and an | |
| 59 | + * override never leaks into other listings on the same page. | |
| 60 | + * | |
| 61 | + * @param string $layout standard | compact_mobile | compact_all | |
| 62 | + * @return string Space-separated class list ('' for standard). | |
| 63 | + */ | |
| 64 | + public static function listingLayoutClasses(string $layout): string | |
| 65 | + { | |
| 66 | + if ($layout === 'compact_mobile') { | |
| 67 | + return 'yatra-listing-compact'; | |
| 68 | + } | |
| 69 | + if ($layout === 'compact_all') { | |
| 70 | + return 'yatra-listing-compact yatra-listing-compact--all'; | |
| 71 | + } | |
| 72 | + return ''; | |
| 73 | + } | |
| 74 | + | |
| 75 | + /** | |
| 76 | + * Register Font Awesome (optional) and common.css so block editor + shortcode styles can | |
| 77 | + * depend on `yatra-common` (shared @keyframes: yatra-spin, yatra-shimmer, etc.). | |
| 78 | + */ | |
| 79 | + /** | |
| 80 | + * @return list<string> | |
| 81 | + */ | |
| 82 | + public static function shortcodeStyleDependencies(): array | |
| 83 | + { | |
| 84 | + self::registerCoreFrontendStylesheets(); | |
| 85 | + | |
| 86 | + return wp_style_is('yatra-common', 'registered') ? ['yatra-common'] : []; | |
| 87 | + } | |
| 88 | + | |
| 89 | + public static function registerCoreFrontendStylesheets(): void | |
| 90 | + { | |
| 91 | + $faPath = YATRA_PLUGIN_PATH . 'assets/vendor/fontawesome/css/all.min.css'; | |
| 92 | + if (file_exists($faPath) && !wp_style_is('yatra-fontawesome-6', 'registered')) { | |
| 93 | + wp_register_style( | |
| 94 | + 'yatra-fontawesome-6', | |
| 95 | + YATRA_PLUGIN_URL . 'assets/vendor/fontawesome/css/all.min.css', | |
| 96 | + [], | |
| 97 | + '6.7.2.' . filemtime($faPath) | |
| 98 | + ); | |
| 99 | + } | |
| 100 | + | |
| 101 | + if (wp_style_is('yatra-common', 'registered')) { | |
| 102 | + return; | |
| 103 | + } | |
| 104 | + $path = YATRA_PLUGIN_PATH . 'assets/css/common.css'; | |
| 105 | + if (!is_readable($path)) { | |
| 106 | + return; | |
| 107 | + } | |
| 108 | + $commonDeps = wp_style_is('yatra-fontawesome-6', 'registered') ? ['yatra-fontawesome-6'] : []; | |
| 109 | + wp_register_style( | |
| 110 | + 'yatra-common', | |
| 111 | + YATRA_PLUGIN_URL . 'assets/css/common.css', | |
| 112 | + $commonDeps, | |
| 113 | + YATRA_VERSION . '.' . filemtime($path) | |
| 114 | + ); | |
| 115 | + } | |
| 116 | + | |
| 117 | + /** | |
| 33 | 118 | * Enqueue frontend assets based on context |
| 34 | 119 | * |
| 35 | 120 | * @return void |
| 36 | 121 | */ |
| @@ -80,22 +165,93 @@ | ||
| 80 | 165 | 'video-player' => 'video-player.css', |
| 81 | 166 | 'tour-viewer' => 'tour-viewer.css', |
| 82 | 167 | ]; |
| 83 | 168 | |
| 169 | + self::registerCoreFrontendStylesheets(); | |
| 170 | + | |
| 171 | + if (wp_style_is('yatra-fontawesome-6', 'registered')) { | |
| 172 | + wp_enqueue_style('yatra-fontawesome-6'); | |
| 173 | + } | |
| 174 | + | |
| 84 | 175 | foreach ($cssFiles as $handle => $filename) { |
| 85 | 176 | $filePath = YATRA_PLUGIN_PATH . "assets/css/{$filename}"; |
| 86 | - if (file_exists($filePath)) { | |
| 87 | - wp_enqueue_style( | |
| 88 | - "yatra-{$handle}", | |
| 89 | - YATRA_PLUGIN_URL . "assets/css/{$filename}", | |
| 90 | - [], | |
| 91 | - YATRA_VERSION . '.' . filemtime($filePath) | |
| 92 | - ); | |
| 177 | + if (!file_exists($filePath)) { | |
| 178 | + continue; | |
| 93 | 179 | } |
| 180 | + $styleHandle = 'yatra-' . $handle; | |
| 181 | + $ver = YATRA_VERSION . '.' . filemtime($filePath); | |
| 182 | + | |
| 183 | + if ($handle === 'common') { | |
| 184 | + if (wp_style_is('yatra-common', 'registered')) { | |
| 185 | + wp_enqueue_style('yatra-common'); | |
| 186 | + } else { | |
| 187 | + $deps = wp_style_is('yatra-fontawesome-6', 'registered') ? ['yatra-fontawesome-6'] : []; | |
| 188 | + wp_enqueue_style($styleHandle, YATRA_PLUGIN_URL . "assets/css/{$filename}", $deps, $ver); | |
| 189 | + } | |
| 190 | + continue; | |
| 191 | + } | |
| 192 | + | |
| 193 | + $deps = []; | |
| 194 | + if ($handle !== 'common' && wp_style_is('yatra-common', 'registered')) { | |
| 195 | + $deps[] = 'yatra-common'; | |
| 196 | + } | |
| 197 | + | |
| 198 | + wp_enqueue_style( | |
| 199 | + $styleHandle, | |
| 200 | + YATRA_PLUGIN_URL . "assets/css/{$filename}", | |
| 201 | + $deps, | |
| 202 | + $ver | |
| 203 | + ); | |
| 94 | 204 | } |
| 205 | + | |
| 206 | + $this->enqueueFrontendThemeVariables(); | |
| 207 | + $this->enqueueFrontendLayoutVariables(); | |
| 95 | 208 | } |
| 96 | 209 | |
| 97 | 210 | /** |
| 211 | + * Override design tokens from Settings (single primary color → related shades). | |
| 212 | + */ | |
| 213 | + private function enqueueFrontendThemeVariables(): void | |
| 214 | + { | |
| 215 | + if (!wp_style_is('yatra-common', 'enqueued')) { | |
| 216 | + return; | |
| 217 | + } | |
| 218 | + $primary = \Yatra\Services\SettingsService::getString( | |
| 219 | + 'frontend_primary_color', | |
| 220 | + \Yatra\Utils\FrontendThemeCss::DEFAULT_PRIMARY | |
| 221 | + ); | |
| 222 | + $primary = \Yatra\Utils\FrontendThemeCss::sanitizePrimaryColor($primary); | |
| 223 | + if (strtolower($primary) === strtolower(\Yatra\Utils\FrontendThemeCss::DEFAULT_PRIMARY)) { | |
| 224 | + return; | |
| 225 | + } | |
| 226 | + $css = \Yatra\Utils\FrontendThemeCss::buildInlineRootCss($primary); | |
| 227 | + if ($css !== '') { | |
| 228 | + wp_add_inline_style('yatra-common', $css); | |
| 229 | + } | |
| 230 | + } | |
| 231 | + | |
| 232 | + /** | |
| 233 | + * Align --yatra-container-max-width with the active theme (theme.json wide/content size or $content_width). | |
| 234 | + */ | |
| 235 | + private function enqueueFrontendLayoutVariables(): void | |
| 236 | + { | |
| 237 | + if (!wp_style_is('yatra-common', 'enqueued')) { | |
| 238 | + return; | |
| 239 | + } | |
| 240 | + $fromSetting = \Yatra\Utils\FrontendThemeCss::sanitizeContainerMaxWidthSetting( | |
| 241 | + \Yatra\Services\SettingsService::getString('frontend_container_max_width', '') | |
| 242 | + ); | |
| 243 | + $max = $fromSetting !== '' | |
| 244 | + ? $fromSetting | |
| 245 | + : \Yatra\Utils\FrontendThemeCss::resolveThemeContainerMaxWidth(); | |
| 246 | + if ($max === null || $max === '') { | |
| 247 | + return; | |
| 248 | + } | |
| 249 | + $maxEsc = esc_attr($max); | |
| 250 | + wp_add_inline_style('yatra-common', ':root{--yatra-container-max-width:' . $maxEsc . ';}'); | |
| 251 | + } | |
| 252 | + | |
| 253 | + /** | |
| 98 | 254 | * Enqueue common JavaScript files |
| 99 | 255 | * |
| 100 | 256 | * @return void |
| 101 | 257 | */ |
| @@ -118,9 +274,21 @@ | ||
| 118 | 274 | $dependencies = ['jquery']; |
| 119 | 275 | if ($handle === 'trip') { |
| 120 | 276 | $dependencies[] = 'yatra-api-helper'; |
| 121 | 277 | } |
| 122 | - | |
| 278 | + // Scripts that call `wp.i18n.__()` for user-facing | |
| 279 | + // strings need wp-i18n as a dependency so the global | |
| 280 | + // exists before they run AND a `wp_set_script_translations` | |
| 281 | + // call below so WordPress loads each one's Jed JSON | |
| 282 | + // catalog (each handle has its own md5-named JSON | |
| 283 | + // because the .po references their respective source | |
| 284 | + // file paths). Add a handle here whenever you wrap a | |
| 285 | + // new string in __() inside its file. | |
| 286 | + $i18nHandles = ['trip', 'listing', 'stripe', 'tour-viewer', 'video-player']; | |
| 287 | + if (in_array($handle, $i18nHandles, true)) { | |
| 288 | + $dependencies[] = 'wp-i18n'; | |
| 289 | + } | |
| 290 | + | |
| 123 | 291 | wp_enqueue_script( |
| 124 | 292 | "yatra-{$handle}", |
| 125 | 293 | YATRA_PLUGIN_URL . "assets/js/{$filename}", |
| 126 | 294 | $dependencies, |
| @@ -126,8 +294,22 @@ | ||
| 126 | 294 | $dependencies, |
| 127 | 295 | YATRA_VERSION . '.' . filemtime($filePath), |
| 128 | 296 | true |
| 129 | 297 | ); |
| 298 | + | |
| 299 | + // Mirror the wp-i18n dep list above. wp_set_script_translations | |
| 300 | + // tells WordPress where to look for this script's Jed JSON | |
| 301 | + // catalog (path = plugin's i18n/languages/) and the loader | |
| 302 | + // hashes md5(handle src) to find the right file. | |
| 303 | + if (in_array($handle, $i18nHandles, true) | |
| 304 | + && function_exists('wp_set_script_translations') | |
| 305 | + ) { | |
| 306 | + wp_set_script_translations( | |
| 307 | + "yatra-{$handle}", | |
| 308 | + 'yatra', | |
| 309 | + YATRA_PLUGIN_PATH . 'i18n/languages' | |
| 310 | + ); | |
| 311 | + } | |
| 130 | 312 | } |
| 131 | 313 | } |
| 132 | 314 | |
| 133 | 315 | if (\Yatra\Services\SettingsService::wishlistEnabled()) { |
| @@ -135,17 +317,46 @@ | ||
| 135 | 317 | if (file_exists($wishPath)) { |
| 136 | 318 | wp_enqueue_script( |
| 137 | 319 | 'yatra-listing-wishlist', |
| 138 | 320 | YATRA_PLUGIN_URL . 'assets/js/listing-wishlist.js', |
| 139 | - ['jquery'], | |
| 321 | + ['jquery', 'wp-i18n'], | |
| 140 | 322 | YATRA_VERSION . '.' . filemtime($wishPath), |
| 141 | 323 | true |
| 142 | 324 | ); |
| 325 | + if (function_exists('wp_set_script_translations')) { | |
| 326 | + wp_set_script_translations( | |
| 327 | + 'yatra-listing-wishlist', | |
| 328 | + 'yatra', | |
| 329 | + YATRA_PLUGIN_PATH . 'i18n/languages' | |
| 330 | + ); | |
| 331 | + } | |
| 332 | + // Wishlist "Login" should send guests to the configured | |
| 333 | + // My Account page (Settings → Permalink slug), not the raw | |
| 334 | + // wp-login.php screen. Fall back to wp_login_url() only when | |
| 335 | + // no account base is configured so the button never dead-ends. | |
| 336 | + $yatraAccountBase = \Yatra\Services\SettingsService::getAccountBase(); | |
| 337 | + $yatraAccountLoginUrl = $yatraAccountBase !== '' | |
| 338 | + ? home_url('/' . trim($yatraAccountBase, '/') . '/') | |
| 339 | + : wp_login_url(); | |
| 143 | 340 | wp_localize_script('yatra-listing-wishlist', 'yatraWishlistConfig', [ |
| 144 | 341 | 'enabled' => true, |
| 145 | 342 | 'restUrl' => rest_url('yatra/v1'), |
| 146 | 343 | 'nonce' => wp_create_nonce('wp_rest'), |
| 147 | 344 | 'isLoggedIn' => is_user_logged_in(), |
| 345 | + 'loginUrl' => $yatraAccountLoginUrl, | |
| 346 | + 'i18n' => [ | |
| 347 | + 'loginRequired' => __('Login Required', 'yatra'), | |
| 348 | + 'loginPrompt' => __('Please login to save trips to your wishlist.', 'yatra'), | |
| 349 | + 'login' => __('Login', 'yatra'), | |
| 350 | + 'cancel' => __('Cancel', 'yatra'), | |
| 351 | + 'genericError' => __('An error occurred. Please try again.', 'yatra'), | |
| 352 | + 'saved' => __('Trip saved to wishlist', 'yatra'), | |
| 353 | + 'removed' => __('Trip removed from wishlist', 'yatra'), | |
| 354 | + 'saveFailed' => __('Failed to save trip', 'yatra'), | |
| 355 | + 'removeFailed' => __('Failed to remove trip', 'yatra'), | |
| 356 | + 'addAria' => __('Add to favorites', 'yatra'), | |
| 357 | + 'removeAria' => __('Remove from favorites', 'yatra'), | |
| 358 | + ], | |
| 148 | 359 | ]); |
| 149 | 360 | } |
| 150 | 361 | } |
| 151 | 362 | } |
| @@ -212,13 +423,49 @@ | ||
| 212 | 423 | if (file_exists($bookingJs)) { |
| 213 | 424 | wp_enqueue_script( |
| 214 | 425 | 'yatra-booking', |
| 215 | 426 | YATRA_PLUGIN_URL . 'assets/js/booking.js', |
| 216 | - ['jquery'], | |
| 427 | + ['jquery', 'wp-i18n'], | |
| 217 | 428 | YATRA_VERSION . '.' . filemtime($bookingJs), |
| 218 | 429 | true |
| 219 | 430 | ); |
| 431 | + if (function_exists('wp_set_script_translations')) { | |
| 432 | + wp_set_script_translations( | |
| 433 | + 'yatra-booking', | |
| 434 | + 'yatra', | |
| 435 | + YATRA_PLUGIN_PATH . 'i18n/languages' | |
| 436 | + ); | |
| 437 | + } | |
| 220 | 438 | } |
| 439 | + | |
| 440 | + // International phone-number widget (country flag + dial code) used by | |
| 441 | + // the booking form's tel fields. | |
| 442 | + $this->enqueuePhoneInputAssets(); | |
| 443 | + $this->enqueueCountrySelectAssets(); | |
| 444 | + | |
| 445 | + // Mobile sticky-sidebar + flatpickr init for the single-trip page. Lives in a | |
| 446 | + // dedicated file rather than as inline <script> in the partial because | |
| 447 | + // WordPress core's `convert_chars` filter (hooked to the_content) rewrites the | |
| 448 | + // `&&` operators inside inline scripts as `&&` — JS parsers don't | |
| 449 | + // decode HTML entities inside <script>, producing a SyntaxError. As a properly | |
| 450 | + // enqueued external file, the source is delivered verbatim. | |
| 451 | + $sidebarJs = YATRA_PLUGIN_PATH . 'assets/js/single-trip-sidebar.js'; | |
| 452 | + if (file_exists($sidebarJs)) { | |
| 453 | + wp_enqueue_script( | |
| 454 | + 'yatra-single-trip-sidebar', | |
| 455 | + YATRA_PLUGIN_URL . 'assets/js/single-trip-sidebar.js', | |
| 456 | + ['yatra-trip', 'wp-i18n'], // depends on window.yatraTripData from yatra-trip | |
| 457 | + YATRA_VERSION . '.' . filemtime($sidebarJs), | |
| 458 | + true | |
| 459 | + ); | |
| 460 | + if (function_exists('wp_set_script_translations')) { | |
| 461 | + wp_set_script_translations( | |
| 462 | + 'yatra-single-trip-sidebar', | |
| 463 | + 'yatra', | |
| 464 | + YATRA_PLUGIN_PATH . 'i18n/languages' | |
| 465 | + ); | |
| 466 | + } | |
| 467 | + } | |
| 221 | 468 | |
| 222 | 469 | // Localize trip page data for JS (trip.js, booking.js) |
| 223 | 470 | global $trip; |
| 224 | 471 | |
| @@ -245,12 +492,25 @@ | ||
| 245 | 492 | 'tripId' => $trip_id, |
| 246 | 493 | 'tripSlug' => $trip_slug, |
| 247 | 494 | 'wishlistEnabled' => \Yatra\Services\SettingsService::wishlistEnabled(), |
| 248 | 495 | 'isLoggedIn' => is_user_logged_in(), |
| 496 | + // Wishlist "Login" (guest) → the configured My Account page | |
| 497 | + // (Settings → Permalink slug), NOT wp-login.php. Without this key | |
| 498 | + // trip.js falls back to a hardcoded '/wp-login.php'. Falls back to | |
| 499 | + // wp_login_url() only when no account base slug is configured. | |
| 500 | + 'loginUrl' => (function () { | |
| 501 | + $base = \Yatra\Services\SettingsService::getAccountBase(); | |
| 502 | + return $base !== '' ? home_url('/' . trim($base, '/') . '/') : wp_login_url(); | |
| 503 | + })(), | |
| 504 | + // Regional settings | |
| 505 | + 'timezone' => \Yatra\Services\SettingsService::getString('timezone', 'UTC'), | |
| 506 | + 'dateFormat' => \Yatra\Services\SettingsService::getString('date_format', 'Y-m-d'), | |
| 507 | + 'timeFormat' => \Yatra\Services\SettingsService::getString('time_format', 'H:i'), | |
| 249 | 508 | // Currency/settings |
| 250 | 509 | 'currency' => \Yatra\Services\SettingsService::getCurrency(), |
| 251 | - 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'before'), | |
| 252 | - 'decimalPlaces' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'), | |
| 510 | + 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'), | |
| 511 | + 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'), | |
| 512 | + 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(), | |
| 253 | 513 | 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','), |
| 254 | 514 | 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'), |
| 255 | 515 | 'basePrice' => 0.0, |
| 256 | 516 | 'currencySymbol' => function_exists('yatra_get_currency_symbol') |
| @@ -257,8 +517,17 @@ | ||
| 257 | 517 | ? yatra_get_currency_symbol(\Yatra\Services\SettingsService::getCurrency()) |
| 258 | 518 | : '$', |
| 259 | 519 | 'availabilityDates' => [], |
| 260 | 520 | 'groupDiscountsUrl' => rest_url('yatra/v1/discounts/group-discounts'), |
| 521 | + 'dynamicPricingDisplay' => apply_filters('yatra_get_dynamic_pricing_display_settings', [ | |
| 522 | + 'show_original_price' => true, | |
| 523 | + 'show_savings_badge' => true, | |
| 524 | + 'show_urgency_messages' => false, | |
| 525 | + ]), | |
| 526 | + 'pricingType' => 'regular', | |
| 527 | + 'sidebarAvailability' => [], | |
| 528 | + 'sidebarGroupDiscounts' => [], | |
| 529 | + 'flatpickrLocale' => $this->buildFlatpickrLocalePayload(), | |
| 261 | 530 | ]; |
| 262 | 531 | |
| 263 | 532 | if ($has_trip) { |
| 264 | 533 | if (function_exists('yatra_single_trip_calculate_base_price')) { |
| @@ -276,8 +545,14 @@ | ||
| 276 | 545 | |
| 277 | 546 | return null; |
| 278 | 547 | }, $trip->getAvailabilityDates()))); |
| 279 | 548 | } |
| 549 | + if (function_exists('yatra_single_trip_get_client_booking_payload')) { | |
| 550 | + $bookingPayload = yatra_single_trip_get_client_booking_payload($trip); | |
| 551 | + $tripData['pricingType'] = $bookingPayload['pricingType']; | |
| 552 | + $tripData['sidebarAvailability'] = $bookingPayload['sidebarAvailability']; | |
| 553 | + $tripData['sidebarGroupDiscounts'] = $bookingPayload['sidebarGroupDiscounts']; | |
| 554 | + } | |
| 280 | 555 | } |
| 281 | 556 | |
| 282 | 557 | wp_localize_script('yatra-trip', 'yatraTripData', $tripData); |
| 283 | 558 | $tripTitle = ''; |
| @@ -293,13 +568,119 @@ | ||
| 293 | 568 | 'isRemainingPayment' => false, |
| 294 | 569 | 'remainingAmount' => 0, |
| 295 | 570 | 'totalAmount' => 0, |
| 296 | 571 | 'amountPaid' => 0, |
| 572 | + // Booking-scoped CSRF nonce — covers BOTH logged-in and | |
| 573 | + // guest checkouts. The REST endpoint's public | |
| 574 | + // permission_callback intentionally bypasses the WP REST | |
| 575 | + // cookie/nonce check (so guests can hit it at all); | |
| 576 | + // this token is what gates the actual write. The JS | |
| 577 | + // forwards it in the `X-Yatra-Booking-Nonce` header on | |
| 578 | + // every booking-create / booking-update POST. | |
| 579 | + 'bookingNonce' => wp_create_nonce('yatra_booking_action'), | |
| 297 | 580 | ] |
| 298 | 581 | )); |
| 299 | 582 | } |
| 300 | 583 | |
| 301 | 584 | /** |
| 585 | + * Enqueue the country selector widget (assets/js/country-select.js + | |
| 586 | + * assets/css/country-select.css). | |
| 587 | + * | |
| 588 | + * Upgrades every `type => country` field (Country, Nationality, on both the | |
| 589 | + * contact and traveler sections) into a searchable dropdown showing the | |
| 590 | + * national flag, matching the phone country-code control. Purely additive: | |
| 591 | + * the underlying <select> still renders and submits, so a site that never | |
| 592 | + * loads this script behaves exactly as before. | |
| 593 | + * | |
| 594 | + * Idempotent, so it is safe to call from every path that renders the form. | |
| 595 | + * | |
| 596 | + * @return void | |
| 597 | + */ | |
| 598 | + private function enqueueCountrySelectAssets(): void | |
| 599 | + { | |
| 600 | + if (wp_script_is('yatra-country-select', 'enqueued')) { | |
| 601 | + return; | |
| 602 | + } | |
| 603 | + | |
| 604 | + $css = YATRA_PLUGIN_PATH . 'assets/css/country-select.css'; | |
| 605 | + if (file_exists($css)) { | |
| 606 | + wp_enqueue_style( | |
| 607 | + 'yatra-country-select', | |
| 608 | + YATRA_PLUGIN_URL . 'assets/css/country-select.css', | |
| 609 | + [], | |
| 610 | + YATRA_VERSION . '.' . filemtime($css) | |
| 611 | + ); | |
| 612 | + } | |
| 613 | + | |
| 614 | + $js = YATRA_PLUGIN_PATH . 'assets/js/country-select.js'; | |
| 615 | + if (!file_exists($js)) { | |
| 616 | + return; | |
| 617 | + } | |
| 618 | + | |
| 619 | + wp_enqueue_script( | |
| 620 | + 'yatra-country-select', | |
| 621 | + YATRA_PLUGIN_URL . 'assets/js/country-select.js', | |
| 622 | + [], | |
| 623 | + YATRA_VERSION . '.' . filemtime($js), | |
| 624 | + true | |
| 625 | + ); | |
| 626 | + | |
| 627 | + wp_localize_script('yatra-country-select', 'yatraCountrySelectData', [ | |
| 628 | + 'i18n' => [ | |
| 629 | + 'search' => __('Search country', 'yatra'), | |
| 630 | + 'noResults' => __('No matches', 'yatra'), | |
| 631 | + ], | |
| 632 | + ]); | |
| 633 | + } | |
| 634 | + | |
| 635 | + /** | |
| 636 | + * Enqueue the international phone-number widget (assets/js/phone-input.js + | |
| 637 | + * assets/css/phone-input.css) and localize its country + dial-code dataset. | |
| 638 | + * | |
| 639 | + * Self-contained (reads its own `yatraPhoneData` global) and idempotent, so | |
| 640 | + * it can be called from every path that renders the booking form. Country | |
| 641 | + * data is the single source of truth in {@see FormatHelper}. | |
| 642 | + * | |
| 643 | + * @return void | |
| 644 | + */ | |
| 645 | + private function enqueuePhoneInputAssets(): void | |
| 646 | + { | |
| 647 | + if (wp_script_is('yatra-phone-input', 'enqueued')) { | |
| 648 | + return; | |
| 649 | + } | |
| 650 | + | |
| 651 | + $css = YATRA_PLUGIN_PATH . 'assets/css/phone-input.css'; | |
| 652 | + if (file_exists($css)) { | |
| 653 | + wp_enqueue_style( | |
| 654 | + 'yatra-phone-input', | |
| 655 | + YATRA_PLUGIN_URL . 'assets/css/phone-input.css', | |
| 656 | + [], | |
| 657 | + YATRA_VERSION . '.' . filemtime($css) | |
| 658 | + ); | |
| 659 | + } | |
| 660 | + | |
| 661 | + $js = YATRA_PLUGIN_PATH . 'assets/js/phone-input.js'; | |
| 662 | + if (!file_exists($js)) { | |
| 663 | + return; | |
| 664 | + } | |
| 665 | + wp_enqueue_script( | |
| 666 | + 'yatra-phone-input', | |
| 667 | + YATRA_PLUGIN_URL . 'assets/js/phone-input.js', | |
| 668 | + [], | |
| 669 | + YATRA_VERSION . '.' . filemtime($js), | |
| 670 | + true | |
| 671 | + ); | |
| 672 | + wp_localize_script('yatra-phone-input', 'yatraPhoneData', [ | |
| 673 | + 'countries' => \Yatra\Helpers\FormatHelper::getPhoneCountries(), | |
| 674 | + 'priority' => \Yatra\Helpers\FormatHelper::getPhonePriority(), | |
| 675 | + 'i18n' => [ | |
| 676 | + 'search' => __('Search country', 'yatra'), | |
| 677 | + 'noResults' => __('No matches', 'yatra'), | |
| 678 | + ], | |
| 679 | + ]); | |
| 680 | + } | |
| 681 | + | |
| 682 | + /** | |
| 302 | 683 | * Enqueue activity listing specific assets |
| 303 | 684 | * |
| 304 | 685 | * @return void |
| 305 | 686 | */ |
| @@ -348,25 +729,76 @@ | ||
| 348 | 729 | if (file_exists($bookingCss)) { |
| 349 | 730 | wp_enqueue_style( |
| 350 | 731 | 'yatra-booking', |
| 351 | 732 | YATRA_PLUGIN_URL . 'assets/css/booking.css', |
| 352 | - [], | |
| 733 | + ['yatra-common'], | |
| 353 | 734 | YATRA_VERSION . '.' . filemtime($bookingCss) |
| 354 | 735 | ); |
| 355 | 736 | } |
| 356 | 737 | |
| 738 | + // Flatpickr — used by booking.js to upgrade Date-of-Birth (and other | |
| 739 | + // date) inputs to a picker with fast, typeable year navigation. The | |
| 740 | + // single-trip page already ships flatpickr (see single-trip.php); the | |
| 741 | + // dedicated booking page did not, so enqueue it here. booking.js | |
| 742 | + // self-guards on `typeof flatpickr`, so this is safe either way. | |
| 743 | + wp_enqueue_style( | |
| 744 | + 'yatra-flatpickr', | |
| 745 | + 'https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css', | |
| 746 | + [], | |
| 747 | + YATRA_VERSION | |
| 748 | + ); | |
| 749 | + wp_enqueue_script( | |
| 750 | + 'yatra-flatpickr', | |
| 751 | + 'https://cdn.jsdelivr.net/npm/flatpickr', | |
| 752 | + [], | |
| 753 | + YATRA_VERSION, | |
| 754 | + true | |
| 755 | + ); | |
| 756 | + | |
| 357 | 757 | // Enqueue booking-specific JavaScript |
| 358 | 758 | $bookingJs = YATRA_PLUGIN_PATH . 'assets/js/booking.js'; |
| 359 | 759 | if (file_exists($bookingJs)) { |
| 760 | + // booking.js renders user-facing strings via wp.i18n.__() (the | |
| 761 | + // "Processing..." button label and the per-gateway info messages | |
| 762 | + // shown when a payment method is selected). It therefore needs | |
| 763 | + // wp-i18n as a dependency AND wp_set_script_translations so its | |
| 764 | + // Jed catalog loads — mirroring the trip-detail enqueue above. | |
| 765 | + // Without these, those strings stay English on the standalone | |
| 766 | + // booking page regardless of site locale. | |
| 360 | 767 | wp_enqueue_script( |
| 361 | 768 | 'yatra-booking', |
| 362 | 769 | YATRA_PLUGIN_URL . 'assets/js/booking.js', |
| 363 | - ['jquery'], | |
| 770 | + ['jquery', 'yatra-flatpickr', 'wp-i18n'], | |
| 364 | 771 | YATRA_VERSION . '.' . filemtime($bookingJs), |
| 365 | 772 | true |
| 366 | 773 | ); |
| 774 | + if (function_exists('wp_set_script_translations')) { | |
| 775 | + wp_set_script_translations( | |
| 776 | + 'yatra-booking', | |
| 777 | + 'yatra', | |
| 778 | + YATRA_PLUGIN_PATH . 'i18n/languages' | |
| 779 | + ); | |
| 780 | + } | |
| 367 | 781 | } |
| 368 | - | |
| 782 | + | |
| 783 | + // International phone-number widget (country flag + dial code). | |
| 784 | + $this->enqueuePhoneInputAssets(); | |
| 785 | + $this->enqueueCountrySelectAssets(); | |
| 786 | + | |
| 787 | + // Load each available gateway's own client scripts on the checkout page | |
| 788 | + // (e.g. Square Web Payments SDK + square.js, Authorize.Net Accept.js + | |
| 789 | + // its handler, Razorpay SDK + its handler). Every gateway's | |
| 790 | + // enqueueScripts() self-guards on isAvailable(), so only enabled + | |
| 791 | + // configured gateways load anything. This call was previously missing, | |
| 792 | + // so Pro gateways that render an inline card form shipped no JS to | |
| 793 | + // checkout and clicking "Pay" just span the button forever. It is | |
| 794 | + // additive and safe for the others: Stripe's enqueueScripts() is a | |
| 795 | + // no-op (Stripe is loaded via enqueueCommonJs), and PayPal/Pay Later | |
| 796 | + // have no client scripts. | |
| 797 | + if (class_exists(\Yatra\PaymentGateways\PaymentGatewayRegistry::class)) { | |
| 798 | + \Yatra\PaymentGateways\PaymentGatewayRegistry::getInstance()->enqueueScripts(); | |
| 799 | + } | |
| 800 | + | |
| 369 | 801 | // Localize booking data for booking.js |
| 370 | 802 | $permalink_structure = get_option('permalink_structure') ?: ''; |
| 371 | 803 | $is_plain = empty($permalink_structure); |
| 372 | 804 | |
| @@ -397,15 +829,21 @@ | ||
| 397 | 829 | 'siteUrl' => site_url(), |
| 398 | 830 | 'bookingBase' => \Yatra\Services\SettingsService::getBookingBase(), |
| 399 | 831 | 'permalinkStructure' => $is_plain ? 'plain' : $permalink_structure, |
| 400 | 832 | 'nonce' => wp_create_nonce('wp_rest'), |
| 833 | + // Booking-scoped CSRF nonce. See enqueueTripDetailAssets() | |
| 834 | + // for the rationale: the booking REST endpoint bypasses | |
| 835 | + // the WP REST cookie/nonce check (so guests can use it), | |
| 836 | + // and this token is what gates the actual booking write. | |
| 837 | + 'bookingNonce' => wp_create_nonce('yatra_booking_action'), | |
| 401 | 838 | 'currency' => \Yatra\Services\SettingsService::getCurrency(), |
| 402 | - 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'before'), | |
| 403 | - 'decimalPlaces' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'), | |
| 839 | + 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'), | |
| 840 | + 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'), | |
| 841 | + 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(), | |
| 404 | 842 | 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','), |
| 405 | 843 | 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'), |
| 406 | 844 | // Payment gateways data |
| 407 | - 'paymentGateways' => apply_filters('yatra_payment_gateways', \Yatra\Services\SettingsService::get('payment_gateways', [])), | |
| 845 | + 'paymentGateways' => $this->sanitizeGatewayConfigsForFrontend(apply_filters('yatra_payment_gateways', \Yatra\Services\SettingsService::get('payment_gateways', []))), | |
| 408 | 846 | 'paymentMethods' => \Yatra\Services\SettingsService::get('payment_methods', []), |
| 409 | 847 | 'paymentTestMode' => \Yatra\Services\SettingsService::get('payment_test_mode', false), |
| 410 | 848 | 'partialPayment' => \Yatra\Services\SettingsService::get('partial_payment', false), |
| 411 | 849 | 'partialPaymentPercentage' => \Yatra\Services\SettingsService::get('partial_payment_percentage', 0), |
| @@ -417,10 +855,17 @@ | ||
| 417 | 855 | 'gatewayOrder' => \Yatra\Services\SettingsService::get('gateway_order', []), |
| 418 | 856 | 'autoConfirmPayLater' => \Yatra\Services\SettingsService::get('auto_confirm_pay_later', true), |
| 419 | 857 | 'allowWaitlist' => \Yatra\Services\SettingsService::isEnabled('allow_waitlist'), |
| 420 | 858 | 'waitlistAutoConfirm' => \Yatra\Services\SettingsService::isEnabled('waitlist_auto_confirm'), |
| 421 | - 'gateways' => apply_filters('yatra_payment_gateways', \Yatra\Services\SettingsService::get('payment_gateways', [])), | |
| 422 | - 'enabledGateways' => \Yatra\Services\SettingsService::get('payment_gateways', []), | |
| 859 | + 'gateways' => $this->getGatewayFrontendConfigs(), | |
| 860 | + 'enabledGateways' => $this->sanitizeGatewayConfigsForFrontend(\Yatra\Services\SettingsService::get('payment_gateways', [])), | |
| 861 | + // Server-side translated UI strings for booking.js. PHP __() resolves via .mo | |
| 862 | + // (reliable), so these stay translatable even when the JS-translation JSON | |
| 863 | + // chain (wp_set_script_translations) doesn't load on a given setup. | |
| 864 | + 'i18n' => [ | |
| 865 | + 'complete_booking' => __('Complete Booking', 'yatra'), | |
| 866 | + 'pay_now' => __('Pay Now', 'yatra'), | |
| 867 | + ], | |
| 423 | 868 | ]; |
| 424 | 869 | |
| 425 | 870 | $bookingData = array_merge($bookingData, $this->getStripeFrontendBookingPayload()); |
| 426 | 871 | |
| @@ -481,16 +926,36 @@ | ||
| 481 | 926 | 'companyPhone' => \Yatra\Services\SettingsService::getString('company_phone', ''), |
| 482 | 927 | 'companyName' => \Yatra\Services\SettingsService::getString('company_name', ''), |
| 483 | 928 | 'companyEmail' => \Yatra\Services\SettingsService::getString('company_email', ''), |
| 484 | 929 | 'currency' => \Yatra\Services\SettingsService::getCurrency(), |
| 485 | - 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'before'), | |
| 486 | - 'decimalPlaces' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'), | |
| 930 | + 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'), | |
| 931 | + 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'), | |
| 932 | + 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(), | |
| 487 | 933 | 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','), |
| 488 | 934 | 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'), |
| 489 | 935 | 'locale' => get_locale(), |
| 936 | + // Global date/time format so the customer account pages render dates | |
| 937 | + // in the operator's configured format (Settings → General), not a | |
| 938 | + // hardcoded browser style. Keys mirror what the admin app receives. | |
| 939 | + 'date_format' => \Yatra\Services\SettingsService::getString('date_format', 'Y-m-d'), | |
| 940 | + 'time_format' => \Yatra\Services\SettingsService::getString('time_format', 'H:i'), | |
| 941 | + 'timezone' => \Yatra\Services\SettingsService::getString('timezone', 'UTC'), | |
| 942 | + // Full ISO country map (code => name) so the account profile can show | |
| 943 | + // full country names and render the country dropdown. Mirrors the | |
| 944 | + // admin (`yatraAdmin.countries`); honours the `yatra_countries_list` filter. | |
| 945 | + 'countries' => class_exists('\\Yatra\\Helpers\\FormatHelper') | |
| 946 | + ? \Yatra\Helpers\FormatHelper::getCountries() | |
| 947 | + : [], | |
| 490 | 948 | 'translations' => $this->getFrontendTranslations(), |
| 491 | 949 | 'wishlistEnabled' => \Yatra\Services\SettingsService::wishlistEnabled(), |
| 492 | 950 | ]); |
| 951 | + | |
| 952 | + // Match admin React (`yatra-admin`): register Jed translations for this handle so `wp.i18n` resolves | |
| 953 | + // strings from PHP/Loco JSON catalogs. Without this, only keys in `translations` above work; the rest | |
| 954 | + // stay English because the account bundle is not in `yatra-admin`'s Jed file (different script hash). | |
| 955 | + if (function_exists('wp_set_script_translations')) { | |
| 956 | + wp_set_script_translations('yatra-account-page', 'yatra', YATRA_PLUGIN_PATH . 'i18n/languages'); | |
| 957 | + } | |
| 493 | 958 | } |
| 494 | 959 | |
| 495 | 960 | /** |
| 496 | 961 | * Enqueue listing filters JavaScript |
| @@ -521,8 +986,80 @@ | ||
| 521 | 986 | } |
| 522 | 987 | } |
| 523 | 988 | |
| 524 | 989 | /** |
| 990 | + * Month and weekday labels for Flatpickr from {@see \WP_Locale} (site language). | |
| 991 | + * | |
| 992 | + * @return array<string, mixed> | |
| 993 | + */ | |
| 994 | + private function buildFlatpickrLocalePayload(): array | |
| 995 | + { | |
| 996 | + global $wp_locale; | |
| 997 | + | |
| 998 | + $first_day = (int) get_option('start_of_week', 1); | |
| 999 | + $first_day = max(0, min(6, $first_day)); | |
| 1000 | + | |
| 1001 | + if (!($wp_locale instanceof \WP_Locale)) { | |
| 1002 | + return [ | |
| 1003 | + 'firstDayOfWeek' => $first_day, | |
| 1004 | + ]; | |
| 1005 | + } | |
| 1006 | + | |
| 1007 | + // IMPORTANT — `month_abbrev` and `weekday_abbrev` are keyed by the | |
| 1008 | + // TRANSLATED LONG NAME, not by a numeric index: | |
| 1009 | + // | |
| 1010 | + // $wp_locale->month['01'] = 'January' (or 'जनवरी', 'enero'…) | |
| 1011 | + // $wp_locale->month_abbrev['January'] = 'Jan' (or 'जन', 'ene'…) | |
| 1012 | + // | |
| 1013 | + // An earlier version of this code mistakenly indexed | |
| 1014 | + // month_abbrev by '01'..'12' / weekday_abbrev by 0..6, which | |
| 1015 | + // ALWAYS returned null → flatpickr's locale.months.shorthand | |
| 1016 | + // shipped as an array of empty strings → the `M` token in any | |
| 1017 | + // altFormat rendered as nothing. Net effect: a date set to | |
| 1018 | + // "19 May 2026" displayed as "19 2026" (no month) under any | |
| 1019 | + // non-en_US locale that exposed the bug. | |
| 1020 | + // | |
| 1021 | + // WP_Locale exposes get_month_abbrev() / get_weekday_abbrev() | |
| 1022 | + // which take the long name and do the right lookup. We use | |
| 1023 | + // those so the indexing rule lives inside core, not here. | |
| 1024 | + $months_long = []; | |
| 1025 | + $months_short = []; | |
| 1026 | + for ($m = 1; $m <= 12; ++$m) { | |
| 1027 | + $key = sprintf('%02d', $m); | |
| 1028 | + $long = $wp_locale->month[$key] ?? ''; | |
| 1029 | + $short = $long !== '' ? (string) $wp_locale->get_month_abbrev($long) : ''; | |
| 1030 | + $months_long[] = $long; | |
| 1031 | + // Final fallback to the long name if the locale has no | |
| 1032 | + // abbreviated form — better than shipping an empty string | |
| 1033 | + // that flatpickr would render as blank. | |
| 1034 | + $months_short[] = $short !== '' ? $short : $long; | |
| 1035 | + } | |
| 1036 | + | |
| 1037 | + $weekdays_long = []; | |
| 1038 | + $weekdays_short = []; | |
| 1039 | + for ($d = 0; $d <= 6; ++$d) { | |
| 1040 | + $long = $wp_locale->weekday[$d] ?? ''; | |
| 1041 | + $short = $long !== '' ? (string) $wp_locale->get_weekday_abbrev($long) : ''; | |
| 1042 | + $weekdays_long[] = $long; | |
| 1043 | + $weekdays_short[] = $short !== '' ? $short : $long; | |
| 1044 | + } | |
| 1045 | + | |
| 1046 | + $payload = [ | |
| 1047 | + 'weekdays' => [ | |
| 1048 | + 'shorthand' => $weekdays_short, | |
| 1049 | + 'longhand' => $weekdays_long, | |
| 1050 | + ], | |
| 1051 | + 'months' => [ | |
| 1052 | + 'shorthand' => $months_short, | |
| 1053 | + 'longhand' => $months_long, | |
| 1054 | + ], | |
| 1055 | + 'firstDayOfWeek' => $first_day, | |
| 1056 | + ]; | |
| 1057 | + | |
| 1058 | + return apply_filters('yatra_flatpickr_locale', $payload); | |
| 1059 | + } | |
| 1060 | + | |
| 1061 | + /** | |
| 525 | 1062 | * Get frontend translations |
| 526 | 1063 | * |
| 527 | 1064 | * @return array |
| 528 | 1065 | */ |
| @@ -543,8 +1080,34 @@ | ||
| 543 | 1080 | 'Total Amount' => __('Total Amount', 'yatra'), |
| 544 | 1081 | 'Payment Status' => __('Payment Status', 'yatra'), |
| 545 | 1082 | 'View Details' => __('View Details', 'yatra'), |
| 546 | 1083 | |
| 1084 | + // Traveler / contact / emergency field labels on the account page. | |
| 1085 | + // Keep in sync with the `fieldLabel()` map in account/BookingDetails.tsx. | |
| 1086 | + 'First Name' => __('First Name', 'yatra'), | |
| 1087 | + 'Last Name' => __('Last Name', 'yatra'), | |
| 1088 | + 'Full Name' => __('Full Name', 'yatra'), | |
| 1089 | + 'Name' => __('Name', 'yatra'), | |
| 1090 | + 'Email' => __('Email', 'yatra'), | |
| 1091 | + 'Phone' => __('Phone', 'yatra'), | |
| 1092 | + 'Mobile' => __('Mobile', 'yatra'), | |
| 1093 | + 'Date of Birth' => __('Date of Birth', 'yatra'), | |
| 1094 | + 'Gender' => __('Gender', 'yatra'), | |
| 1095 | + 'Nationality' => __('Nationality', 'yatra'), | |
| 1096 | + 'Country' => __('Country', 'yatra'), | |
| 1097 | + 'Address' => __('Address', 'yatra'), | |
| 1098 | + 'City' => __('City', 'yatra'), | |
| 1099 | + 'State' => __('State', 'yatra'), | |
| 1100 | + 'Postal Code' => __('Postal Code', 'yatra'), | |
| 1101 | + 'Zip Code' => __('Zip Code', 'yatra'), | |
| 1102 | + 'Passport' => __('Passport', 'yatra'), | |
| 1103 | + 'Passport Number' => __('Passport Number', 'yatra'), | |
| 1104 | + 'Passport Expiry' => __('Passport Expiry', 'yatra'), | |
| 1105 | + 'Dietary Requirements' => __('Dietary Requirements', 'yatra'), | |
| 1106 | + 'Special Requirements' => __('Special Requirements', 'yatra'), | |
| 1107 | + 'Relationship' => __('Relationship', 'yatra'), | |
| 1108 | + 'Company' => __('Company', 'yatra'), | |
| 1109 | + | |
| 547 | 1110 | // Common |
| 548 | 1111 | 'Loading...' => __('Loading...', 'yatra'), |
| 549 | 1112 | 'No data available' => __('No data available', 'yatra'), |
| 550 | 1113 | 'Error loading data' => __('Error loading data', 'yatra'), |
| @@ -649,8 +1212,100 @@ | ||
| 649 | 1212 | { |
| 650 | 1213 | $basePath = $type === 'css' ? 'assets/css/' : 'assets/js/'; |
| 651 | 1214 | $fullPath = YATRA_PLUGIN_PATH . $basePath . $path; |
| 652 | 1215 | return file_exists($fullPath); |
| 1216 | + } | |
| 1217 | + | |
| 1218 | + /** | |
| 1219 | + * Strip secret credentials from per-gateway config before it is localized | |
| 1220 | + * into the page (yatraBookingData). The stored payment_gateways option | |
| 1221 | + * holds private keys / access tokens that must NEVER reach the browser; the | |
| 1222 | + * checkout scripts only ever read public values (publishable keys, Square | |
| 1223 | + * application/location IDs, Authorize.Net public client key, the enabled | |
| 1224 | + * flag, etc.). This removes the known secret keys while preserving the | |
| 1225 | + * structure and every public field, so existing gateways/consumers are | |
| 1226 | + * unaffected — only secrets are dropped. | |
| 1227 | + * | |
| 1228 | + * @param mixed $gateways | |
| 1229 | + * @return array<string, mixed> | |
| 1230 | + */ | |
| 1231 | + /** | |
| 1232 | + * Per-gateway PUBLIC config for the booking page, keyed by gateway id | |
| 1233 | + * (window.yatraBookingData.gateways.<id>). Checkout scripts read their public | |
| 1234 | + * settings from here — e.g. square.js → gateways.square.application_id / | |
| 1235 | + * location_id, authorizenet.js → gateways.authorize_net.public_client_key / | |
| 1236 | + * api_login_id. | |
| 1237 | + * | |
| 1238 | + * Source of truth is each ENABLED gateway's own getFrontendData(), i.e. an | |
| 1239 | + * allowlist the gateway itself declares. This is deliberately NOT a denylist | |
| 1240 | + * over the raw stored config: a denylist would leak any secret whose key we | |
| 1241 | + * forgot (e.g. Stripe live_secret_key / test_secret_key, Bank Transfer | |
| 1242 | + * account_number / routing_code). Gateways without a getFrontendData() | |
| 1243 | + * (Bank Transfer, PayPal, Pay Later, …) contribute nothing, so their stored | |
| 1244 | + * details never reach the browser. Disabled gateways are excluded. | |
| 1245 | + * | |
| 1246 | + * @return array<string, array<string, mixed>> | |
| 1247 | + */ | |
| 1248 | + private function getGatewayFrontendConfigs(): array | |
| 1249 | + { | |
| 1250 | + if (!class_exists(\Yatra\PaymentGateways\PaymentGatewayRegistry::class)) { | |
| 1251 | + return []; | |
| 1252 | + } | |
| 1253 | + | |
| 1254 | + $out = []; | |
| 1255 | + try { | |
| 1256 | + $registry = \Yatra\PaymentGateways\PaymentGatewayRegistry::getInstance(); | |
| 1257 | + foreach ($registry->getEnabledGateways() as $id => $gateway) { | |
| 1258 | + if (!is_object($gateway) || !method_exists($gateway, 'getFrontendData')) { | |
| 1259 | + continue; | |
| 1260 | + } | |
| 1261 | + $data = $gateway->getFrontendData(); | |
| 1262 | + if (is_array($data) && $data !== []) { | |
| 1263 | + $data['enabled'] = true; | |
| 1264 | + $out[(string) $id] = $data; | |
| 1265 | + } | |
| 1266 | + } | |
| 1267 | + } catch (\Throwable $e) { | |
| 1268 | + return []; | |
| 1269 | + } | |
| 1270 | + | |
| 1271 | + return $out; | |
| 1272 | + } | |
| 1273 | + | |
| 1274 | + private function sanitizeGatewayConfigsForFrontend($gateways): array | |
| 1275 | + { | |
| 1276 | + if (!is_array($gateways)) { | |
| 1277 | + return []; | |
| 1278 | + } | |
| 1279 | + | |
| 1280 | + // Credential fields that are private to the server. | |
| 1281 | + $secretKeys = [ | |
| 1282 | + 'access_token', | |
| 1283 | + 'api_key', | |
| 1284 | + 'api_secret', | |
| 1285 | + 'secret_key', | |
| 1286 | + 'key_secret', | |
| 1287 | + 'client_secret', | |
| 1288 | + 'transaction_key', | |
| 1289 | + 'webhook_secret', | |
| 1290 | + 'webhook_signing_secret', | |
| 1291 | + 'signing_secret', | |
| 1292 | + 'private_key', | |
| 1293 | + 'password', | |
| 1294 | + 'secret', | |
| 1295 | + ]; | |
| 1296 | + | |
| 1297 | + $clean = []; | |
| 1298 | + foreach ($gateways as $id => $config) { | |
| 1299 | + if (is_array($config)) { | |
| 1300 | + foreach ($secretKeys as $secret) { | |
| 1301 | + unset($config[$secret]); | |
| 1302 | + } | |
| 1303 | + } | |
| 1304 | + $clean[$id] = $config; | |
| 1305 | + } | |
| 1306 | + | |
| 1307 | + return $clean; | |
| 653 | 1308 | } |
| 654 | 1309 | |
| 655 | 1310 | /** |
| 656 | 1311 | * Stripe Elements (assets/js/stripe.js) expects publishableKey under yatraBookingData.stripe. |