| @@ -78,10 +78,27 @@ | ||
| 78 | 78 | |
| 79 | 79 | public static function register() |
| 80 | 80 | { |
| 81 | 81 | add_action('wp_enqueue_scripts', [self::class, 'enqueueAssets']); |
| 82 | + add_action('template_redirect', [self::class, 'markOrderReviewPageNoIndex']); | |
| 82 | 83 | } |
| 83 | 84 | |
| 85 | + /** | |
| 86 | + * The store's chosen order-review page is reached from an email with a | |
| 87 | + * bearer credential in its URL and must not be indexed. The shortcode | |
| 88 | + * renders inside the page body, after wp_head() has already printed the | |
| 89 | + * robots tag, so the page has to be flagged before the head — here, once | |
| 90 | + * the main query knows which page it is. | |
| 91 | + * | |
| 92 | + * @return void | |
| 93 | + */ | |
| 94 | + public static function markOrderReviewPageNoIndex() | |
| 95 | + { | |
| 96 | + if (TemplateService::getCurrentFcPageType() === 'order_review') { | |
| 97 | + add_filter('wp_robots', 'wp_robots_no_robots'); | |
| 98 | + } | |
| 99 | + } | |
| 100 | + | |
| 84 | 101 | public static function enqueueAssets() |
| 85 | 102 | { |
| 86 | 103 | $pageType = TemplateService::getCurrentFcPageType(); |
| 87 | 104 | |
| @@ -101,13 +118,141 @@ | ||
| 101 | 118 | break; |
| 102 | 119 | case 'checkout': |
| 103 | 120 | self::loadCheckoutAssets(); |
| 104 | 121 | break; |
| 122 | + case 'order_review': | |
| 123 | + // The store's chosen review page: enqueued here, in the head, | |
| 124 | + // rather than mid-body by the shortcode, so the form and the | |
| 125 | + // page do not paint unstyled first. | |
| 126 | + self::loadReviewSubmissionFormAssets(); | |
| 127 | + Vite::enqueueStyle('fluent-cart-order-review', 'public/order-review/order-review.scss'); | |
| 128 | + break; | |
| 105 | 129 | default: |
| 106 | 130 | return; |
| 107 | 131 | } |
| 108 | 132 | } |
| 109 | 133 | |
| 134 | + // Shared between the full single-product bundle and the review-form-only | |
| 135 | + // one, so a form rendered either way is enqueued under one handle and | |
| 136 | + // localized with one array. | |
| 137 | + const REVIEW_FORM_SCRIPT_HANDLE = 'fluent-cart-review-form'; | |
| 138 | + const REVIEW_FORM_STYLE_HANDLE = 'fluent-cart-reviews'; | |
| 139 | + | |
| 140 | + /** | |
| 141 | + * Just enough to run a review form: the form script, the review | |
| 142 | + * stylesheet, and the strings the script reads. | |
| 143 | + * | |
| 144 | + * A review page is not a product page. loadSingleProductAssets() also | |
| 145 | + * carries gallery zoom, the single-product behaviour, the review list, | |
| 146 | + * the product-card assets and the add-to-cart and direct-checkout | |
| 147 | + * styles — none of which a standalone form uses, and all of which every | |
| 148 | + * visitor to that page would download and execute. | |
| 149 | + * | |
| 150 | + * Idempotent, and idempotent against the full bundle too: both enqueue | |
| 151 | + * the same file under REVIEW_FORM_SCRIPT_HANDLE, so whichever runs | |
| 152 | + * second is dropped by WordPress rather than loading the script twice | |
| 153 | + * and giving every form two controllers. | |
| 154 | + */ | |
| 155 | + public static function loadReviewSubmissionFormAssets() | |
| 156 | + { | |
| 157 | + self::markFrontendAssetsRequired(); | |
| 158 | + | |
| 159 | + if (wp_script_is(self::REVIEW_FORM_SCRIPT_HANDLE, 'enqueued')) { | |
| 160 | + return; | |
| 161 | + } | |
| 162 | + | |
| 163 | + // Through the same helper the full bundle uses, so the version and | |
| 164 | + // the localization are stamped on identically. | |
| 165 | + Vite::enqueueAllScripts( | |
| 166 | + [ | |
| 167 | + [ | |
| 168 | + 'source' => 'public/single-product/ReviewForm.js', | |
| 169 | + 'dependencies' => [], | |
| 170 | + 'inFooter' => true, | |
| 171 | + 'handle' => self::REVIEW_FORM_SCRIPT_HANDLE, | |
| 172 | + ], | |
| 173 | + ], | |
| 174 | + self::REVIEW_FORM_SCRIPT_HANDLE, | |
| 175 | + [ | |
| 176 | + 'fluentcart_review_vars' => static::reviewJsVars(), | |
| 177 | + ] | |
| 178 | + ); | |
| 179 | + | |
| 180 | + Vite::enqueueStyle( | |
| 181 | + self::REVIEW_FORM_STYLE_HANDLE, | |
| 182 | + 'public/single-product/reviews.scss' | |
| 183 | + ); | |
| 184 | + | |
| 185 | + /** | |
| 186 | + * The review form's assets are on this page. An add-on that extends | |
| 187 | + * the form (Pro's photo upload) enqueues its own script and styles | |
| 188 | + * here, so a surface that loads the form without being a product | |
| 189 | + * page — the customer dashboard, the order-review page — gets them | |
| 190 | + * too. Fired once per request, when the assets are first enqueued. | |
| 191 | + * | |
| 192 | + * @param array $context empty for now; the single payload every | |
| 193 | + * action carries, so listeners keep one | |
| 194 | + * signature as context is added | |
| 195 | + */ | |
| 196 | + do_action('fluent_cart/review/submission_form_assets_loaded', []); | |
| 197 | + } | |
| 198 | + | |
| 199 | + /** | |
| 200 | + * @return array | |
| 201 | + */ | |
| 202 | + public static function reviewJsVars(): array | |
| 203 | + { | |
| 204 | + $reviewVars = [ | |
| 205 | + // Threaded replies (customer replies + multiple store replies) | |
| 206 | + // are a Pro capability, off by default. The frontend uses this to | |
| 207 | + // decide whether to render the reviewer reply form in the modal. | |
| 208 | + 'trans' => [ | |
| 209 | + 'no_reviews' => __('No reviews yet. Be the first to write a review!', 'fluent-cart'), | |
| 210 | + 'verified_purchase' => __('Verified Purchase', 'fluent-cart'), | |
| 211 | + 'rating_required' => __('Please select a rating', 'fluent-cart'), | |
| 212 | + 'photo_preview' => __('Photo preview', 'fluent-cart'), | |
| 213 | + /* translators: %s: number of attachments still behind the + */ | |
| 214 | + 'view_more_attachments' => __('View %s more attachments', 'fluent-cart'), | |
| 215 | + 'content_required' => __('Please write a review', 'fluent-cart'), | |
| 216 | + 'name_required' => __('Please enter your name', 'fluent-cart'), | |
| 217 | + 'email_required' => __('Please enter your email', 'fluent-cart'), | |
| 218 | + 'submitting' => __('Submitting...', 'fluent-cart'), | |
| 219 | + 'submit_review' => __('Submit Review', 'fluent-cart'), | |
| 220 | + 'store_reply' => __('Store Reply', 'fluent-cart'), | |
| 221 | + 'reviewer' => __('Reviewer', 'fluent-cart'), | |
| 222 | + 'reply' => __('Reply', 'fluent-cart'), | |
| 223 | + 'view_reply' => __('View Reply', 'fluent-cart'), | |
| 224 | + 'review_thread' => __('Review Thread', 'fluent-cart'), | |
| 225 | + 'no_replies' => __('No replies yet.', 'fluent-cart'), | |
| 226 | + 'close' => __('Close', 'fluent-cart'), | |
| 227 | + 'all' => __('All', 'fluent-cart'), | |
| 228 | + 'prev' => __('Prev', 'fluent-cart'), | |
| 229 | + 'next' => __('Next', 'fluent-cart'), | |
| 230 | + 'prev_page' => __('Previous page', 'fluent-cart'), | |
| 231 | + 'next_page' => __('Next page', 'fluent-cart'), | |
| 232 | + /* translators: 1: the page this segment goes to, 2: how many there are. */ | |
| 233 | + 'slider_page_of' => __('Page %1$s of %2$s', 'fluent-cart'), | |
| 234 | + 'read_more' => __('Read more', 'fluent-cart'), | |
| 235 | + 'show_less' => __('Show less', 'fluent-cart'), | |
| 236 | + 'load_error' => __('Unable to load reviews. Please try again later.', 'fluent-cart'), | |
| 237 | + 'edit_review' => __('Edit your review', 'fluent-cart'), | |
| 238 | + 'rated_out_of' => __('Rated %d out of 5', 'fluent-cart'), | |
| 239 | + /* translators: 1: current step, 2: total steps */ | |
| 240 | + 'step_x_of_y' => __('Step %1$s of %2$s', 'fluent-cart'), | |
| 241 | + 'rating_poor' => __('Poor', 'fluent-cart'), | |
| 242 | + 'rating_average' => __('Average', 'fluent-cart'), | |
| 243 | + 'rating_good' => __('Good', 'fluent-cart'), | |
| 244 | + 'rating_very_good' => __('Very Good', 'fluent-cart'), | |
| 245 | + 'rating_excellent' => __('Excellent', 'fluent-cart'), | |
| 246 | + 'rating_step_hint' => __('Rate this product', 'fluent-cart'), | |
| 247 | + 'details_step_hint' => __('Tell us more', 'fluent-cart'), | |
| 248 | + 'photos_step_hint' => __('Photos are optional', 'fluent-cart'), | |
| 249 | + ], | |
| 250 | + ]; | |
| 251 | + | |
| 252 | + return apply_filters('fluent_cart/review/js_vars', $reviewVars); | |
| 253 | + } | |
| 254 | + | |
| 110 | 255 | public static function loadSingleProductAssets() |
| 111 | 256 | { |
| 112 | 257 | self::markFrontendAssetsRequired(); |
| 113 | 258 | static $isLoaded = false; |
| @@ -125,11 +270,26 @@ | ||
| 125 | 270 | [ |
| 126 | 271 | 'source' => 'public/single-product/SingleProduct.js', |
| 127 | 272 | 'dependencies' => [], |
| 128 | 273 | 'inFooter' => true |
| 274 | + ], | |
| 275 | + [ | |
| 276 | + 'source' => 'public/single-product/Reviews.js', | |
| 277 | + 'dependencies' => [], | |
| 278 | + 'inFooter' => true | |
| 279 | + ], | |
| 280 | + [ | |
| 281 | + 'source' => 'public/single-product/ReviewForm.js', | |
| 282 | + 'dependencies' => [], | |
| 283 | + 'inFooter' => true, | |
| 284 | + // Named, not numbered, so loadReviewSubmissionFormAssets() enqueueing | |
| 285 | + // the same file lands on the same handle and WordPress drops | |
| 286 | + // the second one. | |
| 287 | + 'handle' => self::REVIEW_FORM_SCRIPT_HANDLE, | |
| 129 | 288 | ] |
| 130 | 289 | ]; |
| 131 | 290 | $localizeData = [ |
| 291 | + 'fluentcart_review_vars' => static::reviewJsVars(), | |
| 132 | 292 | 'fluentcart_single_product_vars' => [ |
| 133 | 293 | 'trans' => TransStrings::singleProductPageString(), |
| 134 | 294 | 'cart_button_text' => apply_filters('fluent_cart/product/add_to_cart_text', __('Add To Cart', 'fluent-cart'), []), |
| 135 | 295 | // App::storeSettings()->get('cart_button_text', __('Add to Cart', 'fluent-cart')), |
| @@ -146,8 +306,9 @@ | ||
| 146 | 306 | |
| 147 | 307 | $singlePageStyles = [ |
| 148 | 308 | 'public/single-product/single-product.scss', |
| 149 | 309 | 'public/single-product/similar-product.scss', |
| 310 | + 'public/single-product/reviews.scss', | |
| 150 | 311 | 'public/product-card/style/product-card.scss', |
| 151 | 312 | 'public/single-product/xzoom/xzoom.css', |
| 152 | 313 | 'public/buttons/add-to-cart/style/style.scss', |
| 153 | 314 | 'public/buttons/direct-checkout/style/style.scss' |
| @@ -342,15 +503,30 @@ | ||
| 342 | 503 | Vite::enqueueStyle('fluentcart-customer-css', |
| 343 | 504 | 'public/customer-profile/style/customer-profile.scss' |
| 344 | 505 | ); |
| 345 | 506 | |
| 507 | + // My Reviews opens the storefront review form in a modal, fetched | |
| 508 | + // per product; the form's own script and stylesheet have to be on | |
| 509 | + // the page before that markup arrives. | |
| 510 | + if (\FluentCart\Api\ModuleSettings::isActive('reviews')) { | |
| 511 | + self::loadReviewSubmissionFormAssets(); | |
| 512 | + } | |
| 513 | + | |
| 514 | + // wp-i18n: this bundle pulls in the admin translator transitively | |
| 515 | + // (Start.js -> @/Bits/common.js -> @/utils/translator/Translator.js), | |
| 516 | + // which resolves strings through window.wp.i18n. | |
| 346 | 517 | Vite::enqueueScript( |
| 347 | 518 | 'fluentcart-customer-js', |
| 348 | 519 | 'public/customer-profile/Start.js', |
| 349 | - [] | |
| 520 | + ['wp-i18n'] | |
| 350 | 521 | )->with(CustomerProfileHandler::getLocalizationData()); |
| 351 | 522 | |
| 352 | - //will add script here/ skipping for now | |
| 523 | + /** | |
| 524 | + * Enqueue additional customer-dashboard assets — the hook an add-on | |
| 525 | + * uses to ship the JS backing a portal section it registers through | |
| 526 | + * `fluent_cart/customer_portal/profile_sections`. | |
| 527 | + */ | |
| 528 | + do_action('fluent_cart/customer_dashboard/enqueue_assets'); | |
| 353 | 529 | } |
| 354 | 530 | |
| 355 | 531 | public static function loadCartAssets() |
| 356 | 532 | { |
| @@ -403,9 +579,9 @@ | ||
| 403 | 579 | 'rest' => Helper::getRestInfo(), |
| 404 | 580 | 'ajaxurl' => admin_url('admin-ajax.php'), |
| 405 | 581 | ], |
| 406 | 582 | 'fluentcart_drawer_vars' => [ |
| 407 | - 'placeholder_image' => Vite::getAssetUrl('images/placeholder.svg'), | |
| 583 | + 'placeholder_image' => Helper::getProductPlaceholderUrl(), | |
| 408 | 584 | 'cart_item_layout' => $cartItemLayout, |
| 409 | 585 | 'empty_cart_layout' => $emptyCartLayout, |
| 410 | 586 | 'cart_driver' => Helper::getCartDriver(), |
| 411 | 587 | 'cart_image' => Vite::getAssetUrl('images/cart.svg'), |
| @@ -416,9 +592,10 @@ | ||
| 416 | 592 | 'cart_item_count' => $isInstantCheckout ? 0 : ($cart ? count($cart->cart_data ?? []) : 0), |
| 417 | 593 | 'cart_total_quantity' => $isInstantCheckout ? 0 : ($cart ? array_sum(array_map(function ($item) { return (int) ($item['quantity'] ?? 1); }, $cart->cart_data ?? [])) : 0), |
| 418 | 594 | ], |
| 419 | 595 | 'fluentcart_utm_vars' => [ |
| 420 | - 'allowed_keys' => UtmHelper::allowedUtmParameterKey() | |
| 596 | + 'allowed_keys' => UtmHelper::allowedUtmParameterKey(), | |
| 597 | + 'internal_domains' => UtmHelper::getInternalDomains() | |
| 421 | 598 | ] |
| 422 | 599 | ]); |
| 423 | 600 | |
| 424 | 601 | Vite::enqueueStyle( |
| @@ -513,8 +690,16 @@ | ||
| 513 | 690 | if ($hasInstantCheckout || $hasModalCheckout) { |
| 514 | 691 | $isInstantCheckout = 'yes'; |
| 515 | 692 | } |
| 516 | 693 | |
| 694 | + $checkoutErrorNotices = Arr::get($cart->checkout_data, '__checkout_error_notices', []); | |
| 695 | + if ($checkoutErrorNotices) { | |
| 696 | + $checkoutData = is_array($cart->checkout_data) ? $cart->checkout_data : []; | |
| 697 | + unset($checkoutData['__checkout_error_notices']); | |
| 698 | + $cart->checkout_data = $checkoutData; | |
| 699 | + $cart->save(); | |
| 700 | + } | |
| 701 | + | |
| 517 | 702 | $data = [ |
| 518 | 703 | 'fluentcart_checkout_vars' => [ |
| 519 | 704 | 'rest' => Helper::getRestInfo(), |
| 520 | 705 | 'ajaxurl' => admin_url('admin-ajax.php'), |
| @@ -520,8 +705,9 @@ | ||
| 520 | 705 | 'ajaxurl' => admin_url('admin-ajax.php'), |
| 521 | 706 | 'is_all_digital' => !$cart->requireShipping(), |
| 522 | 707 | 'is_cart_locked' => $cart->checkout_data['is_locked'] ?? 'no', |
| 523 | 708 | 'disable_coupons' => $cart->checkout_data['disable_coupons'] ?? 'no', |
| 709 | + 'notices' => array_values($checkoutErrorNotices), | |
| 524 | 710 | 'payment_methods_with_custom_checkout_buttons' => apply_filters('fluent_cart/payment_methods_with_custom_checkout_buttons', []), |
| 525 | 711 | 'tax_settings' => (new TaxModule())->getSettings(), |
| 526 | 712 | 'submit_button' => [ |
| 527 | 713 | 'text' => __('Place Order', 'fluent-cart'), |