feat: Add product images support with WP Media Library integration

- Add WP Media Library integration for product and variation images
- Support images array (URLs) conversion to attachment IDs
- Add images array to API responses (Admin & Customer SPA)
- Implement drag-and-drop sortable images in Admin product form
- Add image gallery thumbnails in Customer SPA product page
- Initialize WooCommerce session for guest cart operations
- Fix product variations and attributes display in Customer SPA
- Add variation image field in Admin SPA

Changes:
- includes/Api/ProductsController.php: Handle images array, add to responses
- includes/Frontend/ShopController.php: Add images array for customer SPA
- includes/Frontend/CartController.php: Initialize WC session for guests
- admin-spa/src/lib/wp-media.ts: Add openWPMediaGallery function
- admin-spa/src/routes/Products/partials/tabs/GeneralTab.tsx: WP Media + sortable images
- admin-spa/src/routes/Products/partials/tabs/VariationsTab.tsx: Add variation image field
- customer-spa/src/pages/Product/index.tsx: Add gallery thumbnails display
This commit is contained in:
Dwindi Ramadhana
2025-11-26 16:18:43 +07:00
parent 909bddb23d
commit f397ef850f
69 changed files with 12481 additions and 156 deletions

View File

@@ -0,0 +1,365 @@
<?php
namespace WooNooW\Frontend;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
/**
* Account Controller - Customer account API
* Handles customer account operations for customer-spa
*/
class AccountController {
/**
* Register REST API routes
*/
public static function register_routes() {
$namespace = 'woonoow/v1';
// Get customer orders
register_rest_route($namespace, '/account/orders', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_orders'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
'args' => [
'page' => [
'default' => 1,
'sanitize_callback' => 'absint',
],
'per_page' => [
'default' => 10,
'sanitize_callback' => 'absint',
],
],
]);
// Get single order
register_rest_route($namespace, '/account/orders/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_order'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
'args' => [
'id' => [
'validate_callback' => function($param) {
return is_numeric($param);
},
],
],
]);
// Get customer profile
register_rest_route($namespace, '/account/profile', [
[
'methods' => 'GET',
'callback' => [__CLASS__, 'get_profile'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
],
[
'methods' => 'POST',
'callback' => [__CLASS__, 'update_profile'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
],
]);
// Update password
register_rest_route($namespace, '/account/password', [
'methods' => 'POST',
'callback' => [__CLASS__, 'update_password'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
'args' => [
'current_password' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
'new_password' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
// Get addresses
register_rest_route($namespace, '/account/addresses', [
[
'methods' => 'GET',
'callback' => [__CLASS__, 'get_addresses'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
],
[
'methods' => 'POST',
'callback' => [__CLASS__, 'update_addresses'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
],
]);
// Get downloads (for digital products)
register_rest_route($namespace, '/account/downloads', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_downloads'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
]);
}
/**
* Check if user is logged in
*/
public static function check_customer_permission() {
return is_user_logged_in();
}
/**
* Get customer orders
*/
public static function get_orders(WP_REST_Request $request) {
$customer_id = get_current_user_id();
$page = $request->get_param('page');
$per_page = $request->get_param('per_page');
$args = [
'customer_id' => $customer_id,
'limit' => $per_page,
'page' => $page,
'orderby' => 'date',
'order' => 'DESC',
];
$orders = wc_get_orders($args);
$formatted_orders = array_map(function($order) {
return self::format_order($order);
}, $orders);
// Get total count
$total_args = [
'customer_id' => $customer_id,
'return' => 'ids',
];
$total = count(wc_get_orders($total_args));
return new WP_REST_Response([
'orders' => $formatted_orders,
'total' => $total,
'total_pages' => ceil($total / $per_page),
'page' => $page,
'per_page' => $per_page,
], 200);
}
/**
* Get single order
*/
public static function get_order(WP_REST_Request $request) {
$order_id = $request->get_param('id');
$customer_id = get_current_user_id();
$order = wc_get_order($order_id);
if (!$order) {
return new WP_Error('order_not_found', 'Order not found', ['status' => 404]);
}
// Check if order belongs to customer
if ($order->get_customer_id() !== $customer_id) {
return new WP_Error('forbidden', 'You do not have permission to view this order', ['status' => 403]);
}
return new WP_REST_Response(self::format_order($order, true), 200);
}
/**
* Get customer profile
*/
public static function get_profile(WP_REST_Request $request) {
$user_id = get_current_user_id();
$user = get_userdata($user_id);
if (!$user) {
return new WP_Error('user_not_found', 'User not found', ['status' => 404]);
}
return new WP_REST_Response([
'id' => $user->ID,
'email' => $user->user_email,
'first_name' => get_user_meta($user_id, 'first_name', true),
'last_name' => get_user_meta($user_id, 'last_name', true),
'username' => $user->user_login,
], 200);
}
/**
* Update customer profile
*/
public static function update_profile(WP_REST_Request $request) {
$user_id = get_current_user_id();
$first_name = $request->get_param('first_name');
$last_name = $request->get_param('last_name');
$email = $request->get_param('email');
// Update user meta
if ($first_name !== null) {
update_user_meta($user_id, 'first_name', sanitize_text_field($first_name));
}
if ($last_name !== null) {
update_user_meta($user_id, 'last_name', sanitize_text_field($last_name));
}
// Update email if changed
if ($email !== null && is_email($email)) {
$user = get_userdata($user_id);
if ($user->user_email !== $email) {
wp_update_user([
'ID' => $user_id,
'user_email' => $email,
]);
}
}
return new WP_REST_Response([
'message' => 'Profile updated successfully',
], 200);
}
/**
* Update password
*/
public static function update_password(WP_REST_Request $request) {
$user_id = get_current_user_id();
$current_password = $request->get_param('current_password');
$new_password = $request->get_param('new_password');
$user = get_userdata($user_id);
// Verify current password
if (!wp_check_password($current_password, $user->user_pass, $user_id)) {
return new WP_Error('invalid_password', 'Current password is incorrect', ['status' => 400]);
}
// Update password
wp_set_password($new_password, $user_id);
return new WP_REST_Response([
'message' => 'Password updated successfully',
], 200);
}
/**
* Get customer addresses
*/
public static function get_addresses(WP_REST_Request $request) {
$customer_id = get_current_user_id();
$customer = new \WC_Customer($customer_id);
return new WP_REST_Response([
'billing' => [
'first_name' => $customer->get_billing_first_name(),
'last_name' => $customer->get_billing_last_name(),
'company' => $customer->get_billing_company(),
'address_1' => $customer->get_billing_address_1(),
'address_2' => $customer->get_billing_address_2(),
'city' => $customer->get_billing_city(),
'state' => $customer->get_billing_state(),
'postcode' => $customer->get_billing_postcode(),
'country' => $customer->get_billing_country(),
'email' => $customer->get_billing_email(),
'phone' => $customer->get_billing_phone(),
],
'shipping' => [
'first_name' => $customer->get_shipping_first_name(),
'last_name' => $customer->get_shipping_last_name(),
'company' => $customer->get_shipping_company(),
'address_1' => $customer->get_shipping_address_1(),
'address_2' => $customer->get_shipping_address_2(),
'city' => $customer->get_shipping_city(),
'state' => $customer->get_shipping_state(),
'postcode' => $customer->get_shipping_postcode(),
'country' => $customer->get_shipping_country(),
],
], 200);
}
/**
* Update customer addresses
*/
public static function update_addresses(WP_REST_Request $request) {
$customer_id = get_current_user_id();
$customer = new \WC_Customer($customer_id);
$billing = $request->get_param('billing');
$shipping = $request->get_param('shipping');
// Update billing address
if ($billing) {
foreach ($billing as $key => $value) {
$method = 'set_billing_' . $key;
if (method_exists($customer, $method)) {
$customer->$method(sanitize_text_field($value));
}
}
}
// Update shipping address
if ($shipping) {
foreach ($shipping as $key => $value) {
$method = 'set_shipping_' . $key;
if (method_exists($customer, $method)) {
$customer->$method(sanitize_text_field($value));
}
}
}
$customer->save();
return new WP_REST_Response([
'message' => 'Addresses updated successfully',
], 200);
}
/**
* Get customer downloads
*/
public static function get_downloads(WP_REST_Request $request) {
$customer_id = get_current_user_id();
$downloads = wc_get_customer_available_downloads($customer_id);
return new WP_REST_Response($downloads, 200);
}
/**
* Format order data for API response
*/
private static function format_order($order, $detailed = false) {
$data = [
'id' => $order->get_id(),
'order_number' => $order->get_order_number(),
'status' => $order->get_status(),
'date_created' => $order->get_date_created()->date('Y-m-d H:i:s'),
'total' => $order->get_total(),
'currency' => $order->get_currency(),
'payment_method' => $order->get_payment_method_title(),
];
if ($detailed) {
$data['items'] = array_map(function($item) {
$product = $item->get_product();
return [
'id' => $item->get_id(),
'name' => $item->get_name(),
'quantity' => $item->get_quantity(),
'total' => $item->get_total(),
'image' => $product ? wp_get_attachment_url($product->get_image_id()) : '',
];
}, $order->get_items());
$data['billing'] = $order->get_address('billing');
$data['shipping'] = $order->get_address('shipping');
$data['subtotal'] = $order->get_subtotal();
$data['shipping_total'] = $order->get_shipping_total();
$data['tax_total'] = $order->get_total_tax();
$data['discount_total'] = $order->get_discount_total();
}
return $data;
}
}

View File

@@ -0,0 +1,304 @@
<?php
namespace WooNooW\Frontend;
/**
* Frontend Assets Manager
* Handles loading of customer-spa assets
*/
class Assets {
/**
* Initialize
*/
public static function init() {
add_action('wp_enqueue_scripts', [self::class, 'enqueue_assets'], 20);
add_action('wp_head', [self::class, 'add_inline_config'], 5);
add_action('wp_enqueue_scripts', [self::class, 'dequeue_conflicting_scripts'], 100);
add_filter('script_loader_tag', [self::class, 'add_module_type'], 10, 3);
}
/**
* Add type="module" to customer-spa scripts
*/
public static function add_module_type($tag, $handle, $src) {
// Add type="module" to our Vite scripts
if (strpos($handle, 'woonoow-customer') !== false) {
$tag = str_replace('<script ', '<script type="module" ', $tag);
}
return $tag;
}
/**
* Enqueue customer-spa assets
*/
public static function enqueue_assets() {
// Only load on pages with WooNooW shortcodes or in full SPA mode
if (!self::should_load_assets()) {
return;
}
// Check if dev mode is enabled
$is_dev = defined('WOONOOW_CUSTOMER_DEV') && WOONOOW_CUSTOMER_DEV;
if ($is_dev) {
// Dev mode: Load from Vite dev server
$dev_server = 'https://woonoow.local:5174';
// Vite client for HMR
wp_enqueue_script(
'woonoow-customer-vite',
$dev_server . '/@vite/client',
[],
null,
false // Load in header
);
// Main entry point
wp_enqueue_script(
'woonoow-customer-spa',
$dev_server . '/src/main.tsx',
['woonoow-customer-vite'],
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__)));
$dist_path = plugin_dir_path(dirname(dirname(__FILE__))) . 'customer-spa/dist/';
// 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;
}
// Load manifest to get hashed filenames
$manifest_file = $dist_path . 'manifest.json';
if (file_exists($manifest_file)) {
$manifest = json_decode(file_get_contents($manifest_file), true);
// Enqueue main JS
if (isset($manifest['src/main.tsx'])) {
$main_js = $manifest['src/main.tsx']['file'];
wp_enqueue_script(
'woonoow-customer-spa',
$plugin_url . 'customer-spa/dist/' . $main_js,
[],
null,
true
);
}
// Enqueue main CSS
if (isset($manifest['src/main.tsx']['css'])) {
foreach ($manifest['src/main.tsx']['css'] as $css_file) {
wp_enqueue_style(
'woonoow-customer-spa',
$plugin_url . 'customer-spa/dist/' . $css_file,
[],
null
);
}
}
} else {
// Fallback for production build without manifest
wp_enqueue_script(
'woonoow-customer-spa',
$plugin_url . 'customer-spa/dist/app.js',
[],
null,
true
);
wp_enqueue_style(
'woonoow-customer-spa',
$plugin_url . 'customer-spa/dist/app.css',
[],
null
);
}
}
}
/**
* Add inline config and scripts to page head
*/
public static function add_inline_config() {
if (!self::should_load_assets()) {
return;
}
// Get Customer SPA settings
$spa_settings = get_option('woonoow_customer_spa_settings', []);
$default_settings = [
'mode' => 'disabled',
'layout' => 'modern',
'colors' => [
'primary' => '#3B82F6',
'secondary' => '#8B5CF6',
'accent' => '#10B981',
],
'typography' => [
'preset' => 'professional',
],
];
$theme_settings = array_replace_recursive($default_settings, $spa_settings);
// Get WooCommerce currency settings
$currency_settings = [
'code' => get_woocommerce_currency(),
'symbol' => get_woocommerce_currency_symbol(),
'position' => get_option('woocommerce_currency_pos', 'left'),
'thousandSeparator' => wc_get_price_thousand_separator(),
'decimalSeparator' => wc_get_price_decimal_separator(),
'decimals' => wc_get_price_decimals(),
];
$config = [
'apiUrl' => rest_url('woonoow/v1'),
'nonce' => wp_create_nonce('wp_rest'),
'siteUrl' => get_site_url(),
'siteTitle' => get_bloginfo('name'),
'user' => [
'isLoggedIn' => is_user_logged_in(),
'id' => get_current_user_id(),
],
'theme' => $theme_settings,
'currency' => $currency_settings,
];
?>
<script type="text/javascript">
window.woonoowCustomer = <?php echo wp_json_encode($config); ?>;
</script>
<?php
// If dev mode, output scripts directly
$is_dev = defined('WOONOOW_CUSTOMER_DEV') && WOONOOW_CUSTOMER_DEV;
if ($is_dev) {
$dev_server = 'https://woonoow.local:5174';
?>
<script type="module">
import RefreshRuntime from '<?php echo $dev_server; ?>/@react-refresh'
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
<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');
}
}
/**
* Check if we should load customer-spa assets
*/
private static function should_load_assets() {
global $post;
// Get Customer SPA settings
$spa_settings = get_option('woonoow_customer_spa_settings', []);
$mode = isset($spa_settings['mode']) ? $spa_settings['mode'] : 'disabled';
// If disabled, don't load
if ($mode === 'disabled') {
// Still check for shortcodes
if ($post && has_shortcode($post->post_content, 'woonoow_shop')) {
return true;
}
if ($post && has_shortcode($post->post_content, 'woonoow_cart')) {
return true;
}
if ($post && has_shortcode($post->post_content, 'woonoow_checkout')) {
return true;
}
return false;
}
// Full SPA mode - load on all WooCommerce pages
if ($mode === 'full') {
if (function_exists('is_shop') && is_shop()) {
return true;
}
if (function_exists('is_product') && is_product()) {
return true;
}
if (function_exists('is_cart') && is_cart()) {
return true;
}
if (function_exists('is_checkout') && is_checkout()) {
return true;
}
if (function_exists('is_account_page') && is_account_page()) {
return true;
}
return false;
}
// Checkout-Only mode - load only on specific pages
if ($mode === 'checkout_only') {
$checkout_pages = isset($spa_settings['checkoutPages']) ? $spa_settings['checkoutPages'] : [];
if (!empty($checkout_pages['checkout']) && function_exists('is_checkout') && is_checkout() && !is_order_received_page()) {
return true;
}
if (!empty($checkout_pages['thankyou']) && function_exists('is_order_received_page') && is_order_received_page()) {
return true;
}
if (!empty($checkout_pages['account']) && function_exists('is_account_page') && is_account_page()) {
return true;
}
if (!empty($checkout_pages['cart']) && function_exists('is_cart') && is_cart()) {
return true;
}
return false;
}
// Check if current page has WooNooW shortcodes
if ($post && has_shortcode($post->post_content, 'woonoow_shop')) {
return true;
}
if ($post && has_shortcode($post->post_content, 'woonoow_cart')) {
return true;
}
if ($post && has_shortcode($post->post_content, 'woonoow_checkout')) {
return true;
}
if ($post && has_shortcode($post->post_content, 'woonoow_account')) {
return true;
}
return false;
}
/**
* Dequeue conflicting scripts when SPA is active
*/
public static function dequeue_conflicting_scripts() {
if (!self::should_load_assets()) {
return;
}
// Dequeue WooCommerce scripts that conflict with SPA
wp_dequeue_script('wc-cart-fragments');
wp_dequeue_script('woocommerce');
wp_dequeue_script('wc-add-to-cart');
wp_dequeue_script('wc-add-to-cart-variation');
// Dequeue WordPress block scripts that cause errors in SPA
wp_dequeue_script('wp-block-library');
wp_dequeue_script('wp-block-navigation');
wp_dequeue_script('wp-interactivity');
wp_dequeue_script('wp-interactivity-router');
// Keep only essential WooCommerce styles, dequeue others if needed
// wp_dequeue_style('woocommerce-general');
// wp_dequeue_style('woocommerce-layout');
// wp_dequeue_style('woocommerce-smallscreen');
}
}

View File

@@ -0,0 +1,306 @@
<?php
namespace WooNooW\Frontend;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
/**
* Cart Controller - Customer-facing cart API
* Handles cart operations for customer-spa
*/
class CartController {
/**
* Register REST API routes
*/
public static function register_routes() {
$namespace = 'woonoow/v1';
// Get cart
register_rest_route($namespace, '/cart', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_cart'],
'permission_callback' => '__return_true',
]);
// Add to cart
register_rest_route($namespace, '/cart/add', [
'methods' => 'POST',
'callback' => [__CLASS__, 'add_to_cart'],
'permission_callback' => '__return_true',
'args' => [
'product_id' => [
'required' => true,
'validate_callback' => function($param) {
return is_numeric($param);
},
],
'quantity' => [
'default' => 1,
'sanitize_callback' => 'absint',
],
'variation_id' => [
'default' => 0,
'sanitize_callback' => 'absint',
],
],
]);
// Update cart item
register_rest_route($namespace, '/cart/update', [
'methods' => 'POST',
'callback' => [__CLASS__, 'update_cart'],
'permission_callback' => '__return_true',
'args' => [
'cart_item_key' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
'quantity' => [
'required' => true,
'sanitize_callback' => 'absint',
],
],
]);
// Remove from cart
register_rest_route($namespace, '/cart/remove', [
'methods' => 'POST',
'callback' => [__CLASS__, 'remove_from_cart'],
'permission_callback' => '__return_true',
'args' => [
'cart_item_key' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
// Apply coupon
register_rest_route($namespace, '/cart/apply-coupon', [
'methods' => 'POST',
'callback' => [__CLASS__, 'apply_coupon'],
'permission_callback' => '__return_true',
'args' => [
'coupon_code' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
// Remove coupon
register_rest_route($namespace, '/cart/remove-coupon', [
'methods' => 'POST',
'callback' => [__CLASS__, 'remove_coupon'],
'permission_callback' => '__return_true',
'args' => [
'coupon_code' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
}
/**
* 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]);
}
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');
$variation_id = $request->get_param('variation_id');
// Initialize WooCommerce session for guest users
if (!WC()->session->has_session()) {
WC()->session->set_customer_session_cookie(true);
}
if (!WC()->cart) {
return new WP_Error('cart_error', 'Cart not initialized', ['status' => 500]);
}
// Validate product
$product = wc_get_product($product_id);
if (!$product) {
return new WP_Error('invalid_product', 'Product not found', ['status' => 404]);
}
// Add to cart
$cart_item_key = WC()->cart->add_to_cart($product_id, $quantity, $variation_id);
if (!$cart_item_key) {
return new WP_Error('add_to_cart_failed', 'Failed to add product to cart', ['status' => 400]);
}
return new WP_REST_Response([
'message' => 'Product added to cart',
'cart_item_key' => $cart_item_key,
'cart' => self::format_cart(),
], 200);
}
/**
* Update cart item quantity
*/
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]);
}
// 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(),
], 200);
}
/**
* Remove item from cart
*/
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]);
}
// 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(),
], 200);
}
/**
* Apply coupon to cart
*/
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(),
], 200);
}
/**
* Remove coupon from cart
*/
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(),
], 200);
}
/**
* Format cart data for API response
*/
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'];
$items[] = [
'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'] ?? [],
];
}
// Get applied coupons
$coupons = [];
foreach ($cart->get_applied_coupons() as $coupon_code) {
$coupon = new \WC_Coupon($coupon_code);
$coupons[] = [
'code' => $coupon_code,
'discount' => $cart->get_coupon_discount_amount($coupon_code),
'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(),
'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(),
];
}
}

View File

@@ -0,0 +1,224 @@
<?php
namespace WooNooW\Frontend;
/**
* WooCommerce Hook Bridge
* Captures WooCommerce action hook output and makes it available to the SPA
*/
class HookBridge {
/**
* Common WooCommerce hooks to capture
*/
private static $hooks = [
// Single Product Hooks
'woocommerce_before_single_product',
'woocommerce_before_single_product_summary',
'woocommerce_single_product_summary',
'woocommerce_before_add_to_cart_form',
'woocommerce_before_add_to_cart_button',
'woocommerce_after_add_to_cart_button',
'woocommerce_after_add_to_cart_form',
'woocommerce_product_meta_start',
'woocommerce_product_meta_end',
'woocommerce_after_single_product_summary',
'woocommerce_after_single_product',
// Shop/Archive Hooks
'woocommerce_before_shop_loop',
'woocommerce_after_shop_loop',
'woocommerce_before_shop_loop_item',
'woocommerce_after_shop_loop_item',
'woocommerce_before_shop_loop_item_title',
'woocommerce_shop_loop_item_title',
'woocommerce_after_shop_loop_item_title',
// Cart Hooks
'woocommerce_before_cart',
'woocommerce_before_cart_table',
'woocommerce_before_cart_contents',
'woocommerce_cart_contents',
'woocommerce_after_cart_contents',
'woocommerce_after_cart_table',
'woocommerce_cart_collaterals',
'woocommerce_after_cart',
// Checkout Hooks
'woocommerce_before_checkout_form',
'woocommerce_checkout_before_customer_details',
'woocommerce_checkout_after_customer_details',
'woocommerce_checkout_before_order_review',
'woocommerce_checkout_after_order_review',
'woocommerce_after_checkout_form',
];
/**
* Capture hook output for a specific context
*
* @param string $context 'product', 'shop', 'cart', 'checkout'
* @param array $args Context-specific arguments (e.g., product_id)
* @return array Associative array of hook_name => html_output
*/
public static function capture_hooks($context, $args = []) {
$captured = [];
// Filter hooks based on context
$context_hooks = self::get_context_hooks($context);
foreach ($context_hooks as $hook) {
// Start output buffering
ob_start();
// Setup context (e.g., global $product for product hooks)
self::setup_context($context, $args);
// Execute the hook
do_action($hook);
// Capture output
$output = ob_get_clean();
// Only include hooks that have output
if (!empty(trim($output))) {
$captured[$hook] = $output;
}
// Cleanup context
self::cleanup_context($context);
}
return $captured;
}
/**
* Get hooks for a specific context
*/
private static function get_context_hooks($context) {
$context_map = [
'product' => [
'woocommerce_before_single_product',
'woocommerce_before_single_product_summary',
'woocommerce_single_product_summary',
'woocommerce_before_add_to_cart_form',
'woocommerce_before_add_to_cart_button',
'woocommerce_after_add_to_cart_button',
'woocommerce_after_add_to_cart_form',
'woocommerce_product_meta_start',
'woocommerce_product_meta_end',
'woocommerce_after_single_product_summary',
'woocommerce_after_single_product',
],
'shop' => [
'woocommerce_before_shop_loop',
'woocommerce_after_shop_loop',
'woocommerce_before_shop_loop_item',
'woocommerce_after_shop_loop_item',
],
'cart' => [
'woocommerce_before_cart',
'woocommerce_before_cart_table',
'woocommerce_cart_collaterals',
'woocommerce_after_cart',
],
'checkout' => [
'woocommerce_before_checkout_form',
'woocommerce_checkout_before_customer_details',
'woocommerce_checkout_after_customer_details',
'woocommerce_after_checkout_form',
],
];
return $context_map[$context] ?? [];
}
/**
* Setup context for hook execution
*/
private static function setup_context($context, $args) {
global $product, $post;
switch ($context) {
case 'product':
if (isset($args['product_id'])) {
$product = wc_get_product($args['product_id']);
$post = get_post($args['product_id']);
setup_postdata($post);
}
break;
case 'shop':
// Setup shop context if needed
break;
case 'cart':
// Ensure cart is loaded
if (!WC()->cart) {
WC()->cart = new \WC_Cart();
}
break;
case 'checkout':
// Ensure checkout is loaded
if (!WC()->checkout()) {
WC()->checkout = new \WC_Checkout();
}
break;
}
}
/**
* Cleanup context after hook execution
*/
private static function cleanup_context($context) {
global $product, $post;
switch ($context) {
case 'product':
wp_reset_postdata();
$product = null;
break;
}
}
/**
* Register REST API endpoint for hook capture
*/
public static function register_routes() {
register_rest_route('woonoow/v1', '/hooks/(?P<context>[a-z]+)', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_hooks'],
'permission_callback' => '__return_true',
'args' => [
'context' => [
'required' => true,
'type' => 'string',
'enum' => ['product', 'shop', 'cart', 'checkout'],
],
'product_id' => [
'type' => 'integer',
],
],
]);
}
/**
* REST API callback to get hooks
*/
public static function get_hooks($request) {
$context = $request->get_param('context');
$args = [];
// Get context-specific args
if ($context === 'product') {
$args['product_id'] = $request->get_param('product_id');
}
$hooks = self::capture_hooks($context, $args);
return rest_ensure_response([
'success' => true,
'context' => $context,
'hooks' => $hooks,
]);
}
}

View File

@@ -0,0 +1,347 @@
<?php
namespace WooNooW\Frontend;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
/**
* Shop Controller - Customer-facing product catalog API
* Handles product listing, search, and categories for customer-spa
*/
class ShopController {
/**
* Register REST API routes
*/
public static function register_routes() {
$namespace = 'woonoow/v1';
// Get products (public)
register_rest_route($namespace, '/shop/products', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_products'],
'permission_callback' => '__return_true',
'args' => [
'page' => [
'default' => 1,
'sanitize_callback' => 'absint',
],
'per_page' => [
'default' => 12,
'sanitize_callback' => 'absint',
],
'category' => [
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
],
'search' => [
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
],
'orderby' => [
'default' => 'date',
'sanitize_callback' => 'sanitize_text_field',
],
'order' => [
'default' => 'DESC',
'sanitize_callback' => 'sanitize_text_field',
],
'slug' => [
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
// Get single product (public)
register_rest_route($namespace, '/shop/products/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_product'],
'permission_callback' => '__return_true',
'args' => [
'id' => [
'validate_callback' => function($param) {
return is_numeric($param);
},
],
],
]);
// Get categories (public)
register_rest_route($namespace, '/shop/categories', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_categories'],
'permission_callback' => '__return_true',
]);
// Search products (public)
register_rest_route($namespace, '/shop/search', [
'methods' => 'GET',
'callback' => [__CLASS__, 'search_products'],
'permission_callback' => '__return_true',
'args' => [
's' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
}
/**
* Get products list
*/
public static function get_products(WP_REST_Request $request) {
$page = $request->get_param('page');
$per_page = $request->get_param('per_page');
$category = $request->get_param('category');
$search = $request->get_param('search');
$orderby = $request->get_param('orderby');
$order = $request->get_param('order');
$slug = $request->get_param('slug');
$args = [
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => $per_page,
'paged' => $page,
'orderby' => $orderby,
'order' => $order,
];
// Add slug filter (for single product lookup)
if (!empty($slug)) {
$args['name'] = $slug;
}
// Add category filter
if (!empty($category)) {
$args['tax_query'] = [
[
'taxonomy' => 'product_cat',
'field' => 'slug',
'terms' => $category,
],
];
}
// Add search
if (!empty($search)) {
$args['s'] = $search;
}
$query = new \WP_Query($args);
// Check if this is a single product request (by slug)
$is_single = !empty($slug);
$products = [];
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$product = wc_get_product(get_the_ID());
if ($product) {
// Return detailed data for single product requests
$products[] = self::format_product($product, $is_single);
}
}
wp_reset_postdata();
}
return new WP_REST_Response([
'products' => $products,
'total' => $query->found_posts,
'total_pages' => $query->max_num_pages,
'page' => $page,
'per_page' => $per_page,
], 200);
}
/**
* Get single product
*/
public static function get_product(WP_REST_Request $request) {
$product_id = $request->get_param('id');
$product = wc_get_product($product_id);
if (!$product) {
return new WP_Error('product_not_found', 'Product not found', ['status' => 404]);
}
return new WP_REST_Response(self::format_product($product, true), 200);
}
/**
* Get categories
*/
public static function get_categories(WP_REST_Request $request) {
$terms = get_terms([
'taxonomy' => 'product_cat',
'hide_empty' => true,
]);
if (is_wp_error($terms)) {
return new WP_Error('categories_error', 'Failed to get categories', ['status' => 500]);
}
$categories = [];
foreach ($terms as $term) {
$thumbnail_id = get_term_meta($term->term_id, 'thumbnail_id', true);
$categories[] = [
'id' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
'count' => $term->count,
'image' => $thumbnail_id ? wp_get_attachment_url($thumbnail_id) : '',
];
}
return new WP_REST_Response($categories, 200);
}
/**
* Search products
*/
public static function search_products(WP_REST_Request $request) {
$search = $request->get_param('s');
$args = [
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => 10,
's' => $search,
];
$query = new \WP_Query($args);
$products = [];
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$product = wc_get_product(get_the_ID());
if ($product) {
$products[] = self::format_product($product);
}
}
wp_reset_postdata();
}
return new WP_REST_Response($products, 200);
}
/**
* Format product data for API response
*/
private static function format_product($product, $detailed = false) {
$data = [
'id' => $product->get_id(),
'name' => $product->get_name(),
'slug' => $product->get_slug(),
'price' => $product->get_price(),
'regular_price' => $product->get_regular_price(),
'sale_price' => $product->get_sale_price(),
'price_html' => $product->get_price_html(),
'on_sale' => $product->is_on_sale(),
'in_stock' => $product->is_in_stock(),
'stock_status' => $product->get_stock_status(),
'stock_quantity' => $product->get_stock_quantity(),
'type' => $product->get_type(),
'image' => wp_get_attachment_url($product->get_image_id()),
'permalink' => get_permalink($product->get_id()),
];
// Add detailed info if requested
if ($detailed) {
$data['description'] = $product->get_description();
$data['short_description'] = $product->get_short_description();
$data['sku'] = $product->get_sku();
$data['categories'] = wp_get_post_terms($product->get_id(), 'product_cat', ['fields' => 'all']);
$data['tags'] = wp_get_post_terms($product->get_id(), 'product_tag', ['fields' => 'names']);
// Gallery images
$gallery_ids = $product->get_gallery_image_ids();
$data['gallery'] = array_map('wp_get_attachment_url', $gallery_ids);
// Images array (featured + gallery) for frontend
$images = [];
if ($data['image']) {
$images[] = $data['image'];
}
$images = array_merge($images, $data['gallery']);
$data['images'] = $images;
// Attributes and Variations for variable products
if ($product->is_type('variable')) {
$data['attributes'] = self::get_product_attributes($product);
$data['variations'] = self::get_product_variations($product);
}
// Related products
$related_ids = wc_get_related_products($product->get_id(), 4);
$data['related_products'] = array_map(function($id) {
$related = wc_get_product($id);
return $related ? self::format_product($related) : null;
}, $related_ids);
$data['related_products'] = array_filter($data['related_products']);
}
return $data;
}
/**
* Get product attributes
*/
private static function get_product_attributes($product) {
$attributes = [];
foreach ($product->get_attributes() as $attribute) {
$attribute_data = [
'name' => wc_attribute_label($attribute->get_name()),
'options' => [],
'visible' => $attribute->get_visible(),
'variation' => $attribute->get_variation(),
];
// Get attribute options
if ($attribute->is_taxonomy()) {
$terms = wc_get_product_terms($product->get_id(), $attribute->get_name(), ['fields' => 'names']);
$attribute_data['options'] = $terms;
} else {
$attribute_data['options'] = $attribute->get_options();
}
$attributes[] = $attribute_data;
}
return $attributes;
}
/**
* Get product variations
*/
private static function get_product_variations($product) {
$variations = [];
foreach ($product->get_available_variations() as $variation) {
$variation_obj = wc_get_product($variation['variation_id']);
if ($variation_obj) {
$variations[] = [
'id' => $variation['variation_id'],
'attributes' => $variation['attributes'],
'price' => $variation_obj->get_price(),
'regular_price' => $variation_obj->get_regular_price(),
'sale_price' => $variation_obj->get_sale_price(),
'in_stock' => $variation_obj->is_in_stock(),
'stock_quantity' => $variation_obj->get_stock_quantity(),
'image' => wp_get_attachment_url($variation_obj->get_image_id()),
];
}
}
return $variations;
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace WooNooW\Frontend;
/**
* Shortcodes Manager
* Handles WooNooW customer-facing shortcodes
*/
class Shortcodes {
/**
* Initialize
*/
public static function init() {
add_shortcode('woonoow_shop', [__CLASS__, 'shop_shortcode']);
add_shortcode('woonoow_cart', [__CLASS__, 'cart_shortcode']);
add_shortcode('woonoow_checkout', [__CLASS__, 'checkout_shortcode']);
add_shortcode('woonoow_account', [__CLASS__, 'account_shortcode']);
}
/**
* Shop shortcode
* Usage: [woonoow_shop]
*/
public static function shop_shortcode($atts) {
$atts = shortcode_atts([
'category' => '',
'per_page' => 12,
], $atts);
ob_start();
?>
<div id="woonoow-customer-app" data-page="shop" data-category="<?php echo esc_attr($atts['category']); ?>" data-per-page="<?php echo esc_attr($atts['per_page']); ?>">
<!-- Customer SPA will mount here -->
<div class="woonoow-loading">
<p><?php esc_html_e('Loading shop...', 'woonoow'); ?></p>
</div>
</div>
<?php
return ob_get_clean();
}
/**
* Cart shortcode
* Usage: [woonoow_cart]
*/
public static function cart_shortcode($atts) {
ob_start();
?>
<div id="woonoow-customer-app" data-page="cart">
<!-- Customer SPA will mount here -->
<div class="woonoow-loading">
<p><?php esc_html_e('Loading cart...', 'woonoow'); ?></p>
</div>
</div>
<?php
return ob_get_clean();
}
/**
* Checkout shortcode
* Usage: [woonoow_checkout]
*/
public static function checkout_shortcode($atts) {
// Require user to be logged in for checkout
if (!is_user_logged_in()) {
return '<div class="woonoow-notice">' .
'<p>' . esc_html__('Please log in to proceed to checkout.', 'woonoow') . '</p>' .
'<a href="' . esc_url(wp_login_url(get_permalink())) . '" class="button">' .
esc_html__('Log In', 'woonoow') . '</a>' .
'</div>';
}
ob_start();
?>
<div id="woonoow-customer-app" data-page="checkout">
<!-- Customer SPA will mount here -->
<div class="woonoow-loading">
<p><?php esc_html_e('Loading checkout...', 'woonoow'); ?></p>
</div>
</div>
<?php
return ob_get_clean();
}
/**
* Account shortcode
* Usage: [woonoow_account]
*/
public static function account_shortcode($atts) {
// Require user to be logged in
if (!is_user_logged_in()) {
return '<div class="woonoow-notice">' .
'<p>' . esc_html__('Please log in to view your account.', 'woonoow') . '</p>' .
'<a href="' . esc_url(wp_login_url(get_permalink())) . '" class="button">' .
esc_html__('Log In', 'woonoow') . '</a>' .
'</div>';
}
ob_start();
?>
<div id="woonoow-customer-app" data-page="account">
<!-- Customer SPA will mount here -->
<div class="woonoow-loading">
<p><?php esc_html_e('Loading account...', 'woonoow'); ?></p>
</div>
</div>
<?php
return ob_get_clean();
}
}

View File

@@ -0,0 +1,247 @@
<?php
namespace WooNooW\Frontend;
/**
* Template Override
* Overrides WooCommerce templates to use WooNooW SPA
*/
class TemplateOverride {
/**
* Initialize
*/
public static function init() {
// 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);
}
/**
* Disable canonical redirects for SPA routes
* This prevents WordPress from redirecting /product/slug URLs
*/
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) {
$settings = get_option('woonoow_customer_spa_settings', []);
$mode = isset($settings['mode']) ? $settings['mode'] : 'disabled';
// Mode 1: Disabled
if ($mode === 'disabled') {
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';
if (file_exists($spa_template)) {
// Set status to 200 to prevent 404
status_header(200);
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,
];
$should_override = false;
if (!empty($checkout_pages['checkout']) && is_checkout() && !is_order_received_page()) {
$should_override = true;
}
if (!empty($checkout_pages['thankyou']) && is_order_received_page()) {
$should_override = true;
}
if (!empty($checkout_pages['account']) && is_account_page()) {
$should_override = true;
}
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
if (is_woocommerce() || is_product() || is_cart() || is_checkout() || is_account_page()) {
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
if (file_exists($spa_template)) {
return $spa_template;
}
}
}
return $template;
}
/**
* 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;
$data_attrs = 'data-page="product" data-product-id="' . esc_attr($post->ID) . '"';
} elseif (is_cart()) {
$page_type = 'cart';
$data_attrs = 'data-page="cart"';
} elseif (is_checkout()) {
$page_type = 'checkout';
$data_attrs = 'data-page="checkout"';
} elseif (is_account_page()) {
$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() {
if (!self::should_use_spa()) {
return;
}
// Close hidden wrapper
echo '</div>';
}
/**
* Check if we 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;
}
/**
* Override WooCommerce templates
*/
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',
'single-product.php',
'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) {
// Return empty template (SPA will handle rendering)
return plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-wrapper.php';
}
}
return $template;
}
}