fix: WP-Admin CSS conflicts and add-to-cart redirect

- Fix CSS conflicts between WP-Admin and SPA (radio buttons, chart text)
- Add Tailwind important selector scoped to #woonoow-admin-app
- Remove overly aggressive inline SVG styles from Assets.php
- Add targeted WordPress admin CSS overrides in index.css
- Fix add-to-cart redirect to use woocommerce_add_to_cart_redirect filter
- Let WooCommerce handle cart operations natively for proper session management
- Remove duplicate tailwind.config.cjs
This commit is contained in:
Dwindi Ramadhana
2025-12-31 14:06:04 +07:00
parent 93523a74ac
commit 82399d4ddf
20 changed files with 1272 additions and 571 deletions

View File

@@ -35,15 +35,11 @@ class Assets {
public static function enqueue_assets() {
// Only load on pages with WooNooW shortcodes or in full SPA mode
if (!self::should_load_assets()) {
error_log('[WooNooW Customer] should_load_assets returned false - not loading');
return;
}
error_log('[WooNooW Customer] should_load_assets returned true - loading assets');
// Check if dev mode is enabled
$is_dev = defined('WOONOOW_CUSTOMER_DEV') && WOONOOW_CUSTOMER_DEV;
error_log('[WooNooW Customer] Dev mode: ' . ($is_dev ? 'true' : 'false'));
if ($is_dev) {
// Dev mode: Load from Vite dev server
@@ -66,9 +62,6 @@ class Assets {
null,
false // Load in header
);
error_log('WooNooW Customer: Loading from Vite dev server at ' . $dev_server);
error_log('WooNooW Customer: Scripts enqueued - vite client and main.tsx');
} else {
// Production mode: Load from build
$plugin_url = plugin_dir_url(dirname(dirname(__FILE__)));
@@ -76,7 +69,6 @@ class Assets {
// Check if build exists
if (!file_exists($dist_path)) {
error_log('WooNooW: customer-spa build not found. Run: cd customer-spa && npm run build');
return;
}
@@ -84,9 +76,6 @@ class Assets {
$js_url = $plugin_url . 'customer-spa/dist/app.js';
$css_url = $plugin_url . 'customer-spa/dist/app.css';
error_log('[WooNooW Customer] Enqueuing JS: ' . $js_url);
error_log('[WooNooW Customer] Enqueuing CSS: ' . $css_url);
wp_enqueue_script(
'woonoow-customer-spa',
$js_url,
@@ -109,8 +98,6 @@ class Assets {
[],
null
);
error_log('[WooNooW Customer] Assets enqueued successfully');
}
}
@@ -242,7 +229,6 @@ class Assets {
<script type="module" crossorigin src="<?php echo $dev_server; ?>/@vite/client"></script>
<script type="module" crossorigin src="<?php echo $dev_server; ?>/src/main.tsx"></script>
<?php
error_log('WooNooW Customer: Scripts output directly in head with React Refresh preamble');
}
}
@@ -252,11 +238,8 @@ class Assets {
private static function should_load_assets() {
global $post;
error_log('[WooNooW Customer] should_load_assets check - Post ID: ' . ($post ? $post->ID : 'none'));
// First check: Is this a designated SPA page?
if (self::is_spa_page()) {
error_log('[WooNooW Customer] Designated SPA page detected - loading assets');
return true;
}
@@ -264,8 +247,6 @@ class Assets {
$spa_settings = get_option('woonoow_customer_spa_settings', []);
$mode = isset($spa_settings['mode']) ? $spa_settings['mode'] : 'disabled';
error_log('[WooNooW Customer] SPA mode: ' . $mode);
// If disabled, don't load
if ($mode === 'disabled') {
// Special handling for WooCommerce Shop page (it's an archive, not a regular post)
@@ -274,7 +255,6 @@ class Assets {
if ($shop_page_id) {
$shop_page = get_post($shop_page_id);
if ($shop_page && has_shortcode($shop_page->post_content, 'woonoow_shop')) {
error_log('[WooNooW Customer] Found woonoow_shop shortcode on Shop page (ID: ' . $shop_page_id . ')');
return true;
}
}
@@ -282,27 +262,19 @@ class Assets {
// Check for shortcodes on regular pages
if ($post) {
error_log('[WooNooW Customer] Checking post content for shortcodes');
error_log('[WooNooW Customer] Post content: ' . substr($post->post_content, 0, 200));
if (has_shortcode($post->post_content, 'woonoow_shop')) {
error_log('[WooNooW Customer] Found woonoow_shop shortcode');
return true;
}
if (has_shortcode($post->post_content, 'woonoow_cart')) {
error_log('[WooNooW Customer] Found woonoow_cart shortcode');
return true;
}
if (has_shortcode($post->post_content, 'woonoow_checkout')) {
error_log('[WooNooW Customer] Found woonoow_checkout shortcode');
return true;
}
if (has_shortcode($post->post_content, 'woonoow_account')) {
error_log('[WooNooW Customer] Found woonoow_account shortcode');
return true;
}
}
error_log('[WooNooW Customer] No shortcodes found, not loading');
return false;
}

View File

@@ -9,413 +9,465 @@ use WP_Error;
* Cart Controller - Customer-facing cart API
* Handles cart operations for customer-spa
*/
class CartController {
class CartController
{
/**
* Initialize controller
*/
public static function init() {
public static function init()
{
// Bypass cookie authentication for cart endpoints to allow guest users
add_filter('rest_authentication_errors', function($result) {
add_filter('rest_authentication_errors', function ($result) {
// If already authenticated or error, return as is
if (!empty($result)) {
return $result;
}
// Check if this is a cart endpoint
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
if (strpos($request_uri, '/woonoow/v1/cart') !== false) {
return true; // Allow access
}
return $result;
}, 100);
}
/**
* Register REST API routes
*/
public static function register_routes() {
public static function register_routes()
{
$namespace = 'woonoow/v1';
// Get cart
$result = register_rest_route($namespace, '/cart', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_cart'],
'methods' => 'GET',
'callback' => [__CLASS__, 'get_cart'],
'permission_callback' => '__return_true',
]);
// Add to cart
$result = register_rest_route($namespace, '/cart/add', [
'methods' => 'POST',
'callback' => [__CLASS__, 'add_to_cart'],
'permission_callback' => function() {
'methods' => 'POST',
'callback' => [__CLASS__, 'add_to_cart'],
'permission_callback' => function () {
// Allow both logged-in and guest users
return true;
},
'args' => [
'product_id' => [
'required' => true,
'validate_callback' => function($param) {
'args' => [
'product_id' => [
'required' => true,
'validate_callback' => function ($param) {
return is_numeric($param);
},
],
'quantity' => [
'default' => 1,
'quantity' => [
'default' => 1,
'sanitize_callback' => 'absint',
],
'variation_id' => [
'default' => 0,
'default' => 0,
'sanitize_callback' => 'absint',
],
],
]);
// Update cart item
register_rest_route($namespace, '/cart/update', [
'methods' => 'POST',
'callback' => [__CLASS__, 'update_cart'],
'permission_callback' => function() { return true; },
'args' => [
'methods' => 'POST',
'callback' => [__CLASS__, 'update_cart'],
'permission_callback' => function () {
return true; },
'args' => [
'cart_item_key' => [
'required' => true,
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
'quantity' => [
'required' => true,
'quantity' => [
'required' => true,
'sanitize_callback' => 'absint',
],
],
]);
// Remove from cart
register_rest_route($namespace, '/cart/remove', [
'methods' => 'POST',
'callback' => [__CLASS__, 'remove_from_cart'],
'permission_callback' => function() { return true; },
'args' => [
'methods' => 'POST',
'callback' => [__CLASS__, 'remove_from_cart'],
'permission_callback' => function () {
return true; },
'args' => [
'cart_item_key' => [
'required' => true,
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
// Apply coupon
register_rest_route($namespace, '/cart/apply-coupon', [
'methods' => 'POST',
'callback' => [__CLASS__, 'apply_coupon'],
'permission_callback' => function() { return true; },
'args' => [
'methods' => 'POST',
'callback' => [__CLASS__, 'apply_coupon'],
'permission_callback' => function () {
return true; },
'args' => [
'coupon_code' => [
'required' => true,
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
// Clear cart
register_rest_route($namespace, '/cart/clear', [
'methods' => 'POST',
'callback' => [__CLASS__, 'clear_cart'],
'permission_callback' => function () {
return true; },
]);
// Remove coupon
register_rest_route($namespace, '/cart/remove-coupon', [
'methods' => 'POST',
'callback' => [__CLASS__, 'remove_coupon'],
'permission_callback' => function() { return true; },
'args' => [
'methods' => 'POST',
'callback' => [__CLASS__, 'remove_coupon'],
'permission_callback' => function () {
return true; },
'args' => [
'coupon_code' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
'required' => true,
'type' => 'string',
],
],
]);
}
/**
* Get cart contents
*/
public static function get_cart(WP_REST_Request $request) {
if (!WC()->cart) {
return new WP_Error('cart_error', 'Cart not initialized', ['status' => 500]);
public static function get_cart(WP_REST_Request $request)
{
// Initialize WooCommerce session and cart for REST API requests
if (!WC()->session) {
WC()->initialize_session();
}
if (!WC()->cart) {
WC()->initialize_cart();
}
// Set session cookie for guest users to persist cart
if (!WC()->session->has_session()) {
WC()->session->set_customer_session_cookie(true);
}
return new WP_REST_Response(self::format_cart(), 200);
}
/**
* Add item to cart
*/
public static function add_to_cart(WP_REST_Request $request) {
$product_id = $request->get_param('product_id');
$quantity = $request->get_param('quantity');
public static function add_to_cart(WP_REST_Request $request)
{
$product_id = $request->get_param('product_id');
$quantity = $request->get_param('quantity') ?: 1; // Default to 1
$variation_id = $request->get_param('variation_id');
error_log("WooNooW Cart: Adding product {$product_id} (variation: {$variation_id}) qty: {$quantity}");
// Check if WooCommerce is available
if (!function_exists('WC')) {
error_log('WooNooW Cart Error: WooCommerce not loaded');
return new WP_Error('wc_not_loaded', 'WooCommerce is not loaded', ['status' => 500]);
}
// Initialize WooCommerce session and cart for REST API requests
// WooCommerce doesn't auto-initialize these for REST API calls
if (!WC()->session) {
error_log('WooNooW Cart: Initializing WC session for REST API');
WC()->initialize_session();
}
if (!WC()->cart) {
error_log('WooNooW Cart: Initializing WC cart for REST API');
WC()->initialize_cart();
}
// Set session cookie for guest users
// CRITICAL: Set session cookie for guest users to persist cart
if (!WC()->session->has_session()) {
WC()->session->set_customer_session_cookie(true);
error_log('WooNooW Cart: Session cookie set for guest user');
}
error_log('WooNooW Cart: WC Session and Cart initialized successfully');
// Validate product
$product = wc_get_product($product_id);
if (!$product) {
error_log("WooNooW Cart Error: Product {$product_id} not found");
return new WP_Error('invalid_product', 'Product not found', ['status' => 404]);
}
error_log("WooNooW Cart: Product validated - {$product->get_name()} (Type: {$product->get_type()})");
// For variable products, validate the variation and get attributes
// For variable products, get attributes from request or variation
$variation_attributes = [];
if ($variation_id > 0) {
$variation = wc_get_product($variation_id);
if (!$variation) {
error_log("WooNooW Cart Error: Variation {$variation_id} not found");
return new WP_Error('invalid_variation', "Variation {$variation_id} not found", ['status' => 404]);
return new WP_Error('invalid_variation', "Variation not found", ['status' => 404]);
}
if ($variation->get_parent_id() != $product_id) {
error_log("WooNooW Cart Error: Variation {$variation_id} does not belong to product {$product_id}");
return new WP_Error('invalid_variation', "Variation does not belong to this product", ['status' => 400]);
}
if (!$variation->is_purchasable() || !$variation->is_in_stock()) {
error_log("WooNooW Cart Error: Variation {$variation_id} is not purchasable or out of stock");
return new WP_Error('variation_not_available', "This variation is not available for purchase", ['status' => 400]);
if (!$variation->is_in_stock()) {
return new WP_Error('variation_not_available', "This variation is out of stock", ['status' => 400]);
}
// Get variation attributes from post meta
// WooCommerce stores variation attributes as post meta with 'attribute_' prefix
$variation_attributes = [];
// Get parent product to know which attributes to look for
$parent_product = wc_get_product($product_id);
$parent_attributes = $parent_product->get_attributes();
error_log("WooNooW Cart: Parent product attributes: " . print_r(array_keys($parent_attributes), true));
// For each parent attribute, get the value from variation post meta
foreach ($parent_attributes as $attribute) {
if ($attribute->get_variation()) {
$attribute_name = $attribute->get_name();
$meta_key = 'attribute_' . $attribute_name;
// Get the value from post meta
$attribute_value = get_post_meta($variation_id, $meta_key, true);
error_log("WooNooW Cart: Checking attribute {$attribute_name} (meta key: {$meta_key}): {$attribute_value}");
if (!empty($attribute_value)) {
// WooCommerce expects lowercase attribute names
$wc_attribute_key = 'attribute_' . strtolower($attribute_name);
$variation_attributes[$wc_attribute_key] = $attribute_value;
// Build attributes from request parameters (like WooCommerce does)
// Check for attribute_* parameters in the request
$params = $request->get_params();
foreach ($params as $key => $value) {
if (strpos($key, 'attribute_') === 0) {
$variation_attributes[sanitize_title($key)] = wc_clean($value);
}
}
// If no attributes in request, get from variation meta directly
if (empty($variation_attributes)) {
$parent = wc_get_product($product_id);
foreach ($parent->get_attributes() as $attr_name => $attribute) {
if (!$attribute->get_variation())
continue;
$meta_key = 'attribute_' . $attr_name;
$value = get_post_meta($variation_id, $meta_key, true);
if (!empty($value)) {
$variation_attributes[$meta_key] = $value;
}
}
}
error_log("WooNooW Cart: Variation validated - {$variation->get_name()}");
error_log("WooNooW Cart: Variation attributes extracted: " . print_r($variation_attributes, true));
}
// Clear any existing notices before adding to cart
wc_clear_notices();
// Add to cart with variation attributes
error_log("WooNooW Cart: Calling WC()->cart->add_to_cart({$product_id}, {$quantity}, {$variation_id}, attributes)");
$cart_item_key = WC()->cart->add_to_cart($product_id, $quantity, $variation_id, $variation_attributes);
if (!$cart_item_key) {
// Get WooCommerce notices to provide better error message
$notices = wc_get_notices('error');
$error_messages = [];
foreach ($notices as $notice) {
$error_messages[] = is_array($notice) ? $notice['notice'] : $notice;
}
$error_message = !empty($error_messages) ? implode(', ', $error_messages) : 'Failed to add product to cart';
wc_clear_notices(); // Clear notices after reading
error_log("WooNooW Cart Error: add_to_cart returned false - {$error_message}");
error_log("WooNooW Cart Error: All WC notices: " . print_r($notices, true));
wc_clear_notices();
return new WP_Error('add_to_cart_failed', $error_message, ['status' => 400]);
}
error_log("WooNooW Cart: Product added successfully - Key: {$cart_item_key}");
return new WP_REST_Response([
'message' => 'Product added to cart',
'message' => 'Product added to cart',
'cart_item_key' => $cart_item_key,
'cart' => self::format_cart(),
'cart' => self::format_cart(),
], 200);
}
/**
* Update cart item quantity
*/
public static function update_cart(WP_REST_Request $request) {
public static function update_cart(WP_REST_Request $request)
{
$cart_item_key = $request->get_param('cart_item_key');
$quantity = $request->get_param('quantity');
if (!WC()->cart) {
return new WP_Error('cart_error', 'Cart not initialized', ['status' => 500]);
$quantity = $request->get_param('quantity');
// Initialize WooCommerce session and cart for REST API requests
if (!WC()->session) {
WC()->initialize_session();
}
if (!WC()->cart) {
WC()->initialize_cart();
}
if (!WC()->session->has_session()) {
WC()->session->set_customer_session_cookie(true);
}
// Update quantity
$updated = WC()->cart->set_quantity($cart_item_key, $quantity);
if (!$updated) {
return new WP_Error('update_failed', 'Failed to update cart item', ['status' => 400]);
}
return new WP_REST_Response([
'message' => 'Cart updated',
'cart' => self::format_cart(),
'cart' => self::format_cart(),
], 200);
}
/**
* Remove item from cart
*/
public static function remove_from_cart(WP_REST_Request $request) {
public static function remove_from_cart(WP_REST_Request $request)
{
$cart_item_key = $request->get_param('cart_item_key');
if (!WC()->cart) {
return new WP_Error('cart_error', 'Cart not initialized', ['status' => 500]);
// Initialize WooCommerce session and cart for REST API requests
if (!WC()->session) {
WC()->initialize_session();
}
if (!WC()->cart) {
WC()->initialize_cart();
}
if (!WC()->session->has_session()) {
WC()->session->set_customer_session_cookie(true);
}
// Check if item exists in cart
$cart_contents = WC()->cart->get_cart();
if (!isset($cart_contents[$cart_item_key])) {
return new WP_Error('item_not_found', "Cart item not found", ['status' => 404]);
}
// Remove item
$removed = WC()->cart->remove_cart_item($cart_item_key);
if (!$removed) {
return new WP_Error('remove_failed', 'Failed to remove cart item', ['status' => 400]);
}
return new WP_REST_Response([
'message' => 'Item removed from cart',
'cart' => self::format_cart(),
'cart' => self::format_cart(),
], 200);
}
/**
* Clear entire cart
*/
public static function clear_cart(WP_REST_Request $request)
{
// Initialize WooCommerce session and cart for REST API requests
if (!WC()->session) {
WC()->initialize_session();
}
if (!WC()->cart) {
WC()->initialize_cart();
}
if (!WC()->session->has_session()) {
WC()->session->set_customer_session_cookie(true);
}
// Empty the cart
WC()->cart->empty_cart();
return new WP_REST_Response([
'message' => 'Cart cleared',
'cart' => self::format_cart(),
], 200);
}
/**
* Apply coupon to cart
*/
public static function apply_coupon(WP_REST_Request $request) {
public static function apply_coupon(WP_REST_Request $request)
{
$coupon_code = $request->get_param('coupon_code');
if (!WC()->cart) {
return new WP_Error('cart_error', 'Cart not initialized', ['status' => 500]);
}
// Apply coupon
$applied = WC()->cart->apply_coupon($coupon_code);
if (!$applied) {
return new WP_Error('coupon_failed', 'Failed to apply coupon', ['status' => 400]);
}
return new WP_REST_Response([
'message' => 'Coupon applied',
'cart' => self::format_cart(),
'cart' => self::format_cart(),
], 200);
}
/**
* Remove coupon from cart
*/
public static function remove_coupon(WP_REST_Request $request) {
public static function remove_coupon(WP_REST_Request $request)
{
$coupon_code = $request->get_param('coupon_code');
if (!WC()->cart) {
return new WP_Error('cart_error', 'Cart not initialized', ['status' => 500]);
}
// Remove coupon
$removed = WC()->cart->remove_coupon($coupon_code);
if (!$removed) {
return new WP_Error('remove_coupon_failed', 'Failed to remove coupon', ['status' => 400]);
}
return new WP_REST_Response([
'message' => 'Coupon removed',
'cart' => self::format_cart(),
'cart' => self::format_cart(),
], 200);
}
/**
* Format cart data for API response
*/
private static function format_cart() {
private static function format_cart()
{
$cart = WC()->cart;
if (!$cart) {
return null;
}
$items = [];
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
$product = $cart_item['data'];
// Format variation attributes with clean names (Size instead of attribute_size)
$formatted_attributes = [];
if (!empty($cart_item['variation'])) {
foreach ($cart_item['variation'] as $attr_key => $attr_value) {
// Remove 'attribute_' prefix and capitalize
$clean_key = str_replace('attribute_', '', $attr_key);
$clean_key = ucfirst($clean_key);
// Capitalize value
$formatted_attributes[$clean_key] = ucfirst($attr_value);
}
}
$items[] = [
'key' => $cart_item_key,
'product_id' => $cart_item['product_id'],
'key' => $cart_item_key,
'product_id' => $cart_item['product_id'],
'variation_id' => $cart_item['variation_id'] ?? 0,
'quantity' => $cart_item['quantity'],
'name' => $product->get_name(),
'price' => $product->get_price(),
'subtotal' => $cart_item['line_subtotal'],
'total' => $cart_item['line_total'],
'image' => wp_get_attachment_url($product->get_image_id()),
'permalink' => get_permalink($cart_item['product_id']),
'attributes' => $cart_item['variation'] ?? [],
'quantity' => $cart_item['quantity'],
'name' => $product->get_name(),
'price' => $product->get_price(),
'subtotal' => $cart_item['line_subtotal'],
'total' => $cart_item['line_total'],
'image' => wp_get_attachment_url($product->get_image_id()),
'permalink' => get_permalink($cart_item['product_id']),
'attributes' => $formatted_attributes,
];
}
// Get applied coupons
$coupons = [];
foreach ($cart->get_applied_coupons() as $coupon_code) {
$coupon = new \WC_Coupon($coupon_code);
$coupons[] = [
'code' => $coupon_code,
'code' => $coupon_code,
'discount' => $cart->get_coupon_discount_amount($coupon_code),
'type' => $coupon->get_discount_type(),
'type' => $coupon->get_discount_type(),
];
}
return [
'items' => $items,
'subtotal' => $cart->get_subtotal(),
'subtotal_tax' => $cart->get_subtotal_tax(),
'discount_total' => $cart->get_discount_total(),
'discount_tax' => $cart->get_discount_tax(),
'shipping_total' => $cart->get_shipping_total(),
'shipping_tax' => $cart->get_shipping_tax(),
'items' => $items,
'subtotal' => $cart->get_subtotal(),
'subtotal_tax' => $cart->get_subtotal_tax(),
'discount_total' => $cart->get_discount_total(),
'discount_tax' => $cart->get_discount_tax(),
'shipping_total' => $cart->get_shipping_total(),
'shipping_tax' => $cart->get_shipping_tax(),
'cart_contents_tax' => $cart->get_cart_contents_tax(),
'fee_total' => $cart->get_fee_total(),
'fee_tax' => $cart->get_fee_tax(),
'total' => $cart->get_total('edit'),
'total_tax' => $cart->get_total_tax(),
'coupons' => $coupons,
'needs_shipping' => $cart->needs_shipping(),
'needs_payment' => $cart->needs_payment(),
'fee_total' => $cart->get_fee_total(),
'fee_tax' => $cart->get_fee_tax(),
'total' => $cart->get_total('edit'),
'total_tax' => $cart->get_total_tax(),
'coupons' => $coupons,
'needs_shipping' => $cart->needs_shipping(),
'needs_payment' => $cart->needs_payment(),
];
}
}

View File

@@ -5,69 +5,126 @@ namespace WooNooW\Frontend;
* Template Override
* Overrides WooCommerce templates to use WooNooW SPA
*/
class TemplateOverride {
class TemplateOverride
{
/**
* Initialize
*/
public static function init() {
public static function init()
{
// Hook to wp_loaded with priority 10 (BEFORE WooCommerce's priority 20)
// This ensures we process add-to-cart before WooCommerce does
add_action('wp_loaded', [__CLASS__, 'intercept_add_to_cart'], 10);
// Use blank template for full-page SPA
add_filter('template_include', [__CLASS__, 'use_spa_template'], 999);
// Disable canonical redirects for SPA routes
add_filter('redirect_canonical', [__CLASS__, 'disable_canonical_redirect'], 10, 2);
// Override WooCommerce shop page
add_filter('woocommerce_show_page_title', '__return_false');
// Replace WooCommerce content with our SPA
add_action('woocommerce_before_main_content', [__CLASS__, 'start_spa_wrapper'], 5);
add_action('woocommerce_after_main_content', [__CLASS__, 'end_spa_wrapper'], 999);
// Remove WooCommerce default content
remove_action('woocommerce_before_shop_loop', 'woocommerce_result_count', 20);
remove_action('woocommerce_before_shop_loop', 'woocommerce_catalog_ordering', 30);
remove_action('woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10);
remove_action('woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10);
// Override single product template
add_filter('woocommerce_locate_template', [__CLASS__, 'override_template'], 10, 3);
// Remove theme header and footer when SPA is active
add_action('get_header', [__CLASS__, 'remove_theme_header']);
add_action('get_footer', [__CLASS__, 'remove_theme_footer']);
}
/**
* Intercept add-to-cart redirect (NOT the add-to-cart itself)
* Let WooCommerce handle the cart operation properly, we just redirect afterward
*
* This is the proper approach - WooCommerce manages sessions correctly,
* we just customize where the redirect goes.
*/
public static function intercept_add_to_cart()
{
// Only act if add-to-cart is present
if (!isset($_GET['add-to-cart'])) {
return;
}
// Get SPA page from appearance settings
$appearance_settings = get_option('woonoow_appearance_settings', []);
$spa_page_id = isset($appearance_settings['general']['spa_page']) ? $appearance_settings['general']['spa_page'] : 0;
if (!$spa_page_id) {
return; // No SPA page configured, let WooCommerce handle everything
}
// Hook into WooCommerce's redirect filter AFTER it adds to cart
// This is the proper way to customize the redirect destination
add_filter('woocommerce_add_to_cart_redirect', function ($url) use ($spa_page_id) {
// Get redirect parameter from original request
$redirect_to = isset($_GET['redirect']) ? sanitize_text_field($_GET['redirect']) : 'cart';
// Build redirect URL with hash route for SPA
$redirect_url = get_permalink($spa_page_id);
// Determine hash route based on redirect parameter
$hash_route = '/cart'; // Default
if ($redirect_to === 'checkout') {
$hash_route = '/checkout';
} elseif ($redirect_to === 'shop') {
$hash_route = '/shop';
}
// Return the SPA URL with hash route
return trailingslashit($redirect_url) . '#' . $hash_route;
}, 999);
// Prevent caching
add_action('template_redirect', function () {
nocache_headers();
}, 1);
}
/**
* Disable canonical redirects for SPA routes
* This prevents WordPress from redirecting /product/slug URLs
*/
public static function disable_canonical_redirect($redirect_url, $requested_url) {
public static function disable_canonical_redirect($redirect_url, $requested_url)
{
$settings = get_option('woonoow_customer_spa_settings', []);
$mode = isset($settings['mode']) ? $settings['mode'] : 'disabled';
// Only disable redirects in full SPA mode
if ($mode !== 'full') {
return $redirect_url;
}
// Check if this is a SPA route
$spa_routes = ['/product/', '/cart', '/checkout', '/my-account'];
foreach ($spa_routes as $route) {
if (strpos($requested_url, $route) !== false) {
// This is a SPA route, disable WordPress redirect
return false;
}
}
return $redirect_url;
}
/**
* Use SPA template (blank page)
*/
public static function use_spa_template($template) {
public static function use_spa_template($template)
{
// Check if current page is a designated SPA page
if (self::is_spa_page()) {
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
@@ -75,21 +132,23 @@ class TemplateOverride {
return $spa_template;
}
}
// Legacy: Check SPA mode settings
$settings = get_option('woonoow_customer_spa_settings', []);
$mode = isset($settings['mode']) ? $settings['mode'] : 'disabled';
// Mode 1: Disabled - but still check for shortcodes (legacy)
if ($mode === 'disabled') {
// Check if page has woonoow shortcodes
global $post;
if ($post && (
has_shortcode($post->post_content, 'woonoow_shop') ||
has_shortcode($post->post_content, 'woonoow_cart') ||
has_shortcode($post->post_content, 'woonoow_checkout') ||
has_shortcode($post->post_content, 'woonoow_account')
)) {
if (
$post && (
has_shortcode($post->post_content, 'woonoow_shop') ||
has_shortcode($post->post_content, 'woonoow_cart') ||
has_shortcode($post->post_content, 'woonoow_checkout') ||
has_shortcode($post->post_content, 'woonoow_account')
)
) {
// Use blank template for shortcode pages too
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
if (file_exists($spa_template)) {
@@ -98,19 +157,19 @@ class TemplateOverride {
}
return $template;
}
// Check if current URL is a SPA route (for direct access)
$request_uri = $_SERVER['REQUEST_URI'];
$spa_routes = ['/shop', '/product/', '/cart', '/checkout', '/my-account'];
$is_spa_route = false;
foreach ($spa_routes as $route) {
if (strpos($request_uri, $route) !== false) {
$is_spa_route = true;
break;
}
}
// If it's a SPA route in full mode, use SPA template
if ($mode === 'full' && $is_spa_route) {
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
@@ -120,18 +179,18 @@ class TemplateOverride {
return $spa_template;
}
}
// Mode 3: Checkout-Only (partial SPA)
if ($mode === 'checkout_only') {
$checkout_pages = isset($settings['checkoutPages']) ? $settings['checkoutPages'] : [
'checkout' => true,
'thankyou' => true,
'account' => true,
'cart' => false,
'account' => true,
'cart' => false,
];
$should_override = false;
if (!empty($checkout_pages['checkout']) && is_checkout() && !is_order_received_page()) {
$should_override = true;
}
@@ -144,17 +203,17 @@ class TemplateOverride {
if (!empty($checkout_pages['cart']) && is_cart()) {
$should_override = true;
}
if ($should_override) {
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
if (file_exists($spa_template)) {
return $spa_template;
}
}
return $template;
}
// Mode 2: Full SPA
if ($mode === 'full') {
// Override all WooCommerce pages
@@ -165,23 +224,24 @@ class TemplateOverride {
}
}
}
return $template;
}
/**
* Start SPA wrapper
*/
public static function start_spa_wrapper() {
public static function start_spa_wrapper()
{
// Check if we should use SPA
if (!self::should_use_spa()) {
return;
}
// Determine page type
$page_type = 'shop';
$data_attrs = 'data-page="shop"';
if (is_product()) {
$page_type = 'product';
global $post;
@@ -196,58 +256,61 @@ class TemplateOverride {
$page_type = 'account';
$data_attrs = 'data-page="account"';
}
// Output SPA mount point
echo '<div id="woonoow-customer-app" ' . $data_attrs . '>';
echo '<div class="woonoow-loading">';
echo '<p>' . esc_html__('Loading...', 'woonoow') . '</p>';
echo '</div>';
echo '</div>';
// Hide WooCommerce content
echo '<div style="display: none;">';
}
/**
* End SPA wrapper
*/
public static function end_spa_wrapper() {
public static function end_spa_wrapper()
{
if (!self::should_use_spa()) {
return;
}
// Close hidden wrapper
echo '</div>';
}
/**
* Check if we should use SPA
*/
private static function should_use_spa() {
private static function should_use_spa()
{
// Check if frontend mode is enabled
$mode = get_option('woonoow_frontend_mode', 'shortcodes');
if ($mode === 'disabled') {
return false;
}
// For full SPA mode, always use SPA
if ($mode === 'full_spa') {
return true;
}
// For shortcode mode, check if we're on WooCommerce pages
if (is_shop() || is_product() || is_cart() || is_checkout() || is_account_page()) {
return true;
}
return false;
}
/**
* Remove theme header when SPA is active
*/
public static function remove_theme_header() {
public static function remove_theme_header()
{
if (self::should_remove_theme_elements()) {
remove_all_actions('wp_head');
// Re-add essential WordPress head actions
@@ -258,69 +321,74 @@ class TemplateOverride {
add_action('wp_head', 'wp_site_icon', 99);
}
}
/**
* Remove theme footer when SPA is active
*/
public static function remove_theme_footer() {
public static function remove_theme_footer()
{
if (self::should_remove_theme_elements()) {
remove_all_actions('wp_footer');
// Re-add essential WordPress footer actions
add_action('wp_footer', 'wp_print_footer_scripts', 20);
}
}
/**
* Check if current page is the designated SPA page
*/
private static function is_spa_page() {
private static function is_spa_page()
{
global $post;
if (!$post) {
return false;
}
// Get SPA page ID from appearance settings
$appearance_settings = get_option('woonoow_appearance_settings', []);
$spa_page_id = isset($appearance_settings['general']['spa_page']) ? $appearance_settings['general']['spa_page'] : 0;
// Check if current page matches the SPA page
if ($spa_page_id && $post->ID == $spa_page_id) {
return true;
}
return false;
}
/**
* Check if we should remove theme header/footer
*/
private static function should_remove_theme_elements() {
private static function should_remove_theme_elements()
{
// Remove for designated SPA pages
if (self::is_spa_page()) {
return true;
}
$settings = get_option('woonoow_customer_spa_settings', []);
$mode = isset($settings['mode']) ? $settings['mode'] : 'disabled';
// Check if we're on a WooCommerce page in full mode
if ($mode === 'full') {
if (is_shop() || is_product() || is_cart() || is_checkout() || is_account_page() || is_woocommerce()) {
return true;
}
}
// Also remove for pages with shortcodes (even in disabled mode)
global $post;
if ($post && (
has_shortcode($post->post_content, 'woonoow_shop') ||
has_shortcode($post->post_content, 'woonoow_cart') ||
has_shortcode($post->post_content, 'woonoow_checkout') ||
has_shortcode($post->post_content, 'woonoow_account')
)) {
if (
$post && (
has_shortcode($post->post_content, 'woonoow_shop') ||
has_shortcode($post->post_content, 'woonoow_cart') ||
has_shortcode($post->post_content, 'woonoow_checkout') ||
has_shortcode($post->post_content, 'woonoow_account')
)
) {
return true;
}
// Special check for Shop page (archive)
if (function_exists('is_shop') && is_shop()) {
$shop_page_id = get_option('woocommerce_shop_page_id');
@@ -331,19 +399,20 @@ class TemplateOverride {
}
}
}
return false;
}
/**
* Override WooCommerce templates
*/
public static function override_template($template, $template_name, $template_path) {
public static function override_template($template, $template_name, $template_path)
{
// Only override if SPA is enabled
if (!self::should_use_spa()) {
return $template;
}
// Templates to override
$override_templates = [
'archive-product.php',
@@ -351,7 +420,7 @@ class TemplateOverride {
'cart/cart.php',
'checkout/form-checkout.php',
];
// Check if this template should be overridden
foreach ($override_templates as $override) {
if (strpos($template_name, $override) !== false) {
@@ -359,7 +428,7 @@ class TemplateOverride {
return plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-wrapper.php';
}
}
return $template;
}
}