- 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
474 lines
15 KiB
PHP
474 lines
15 KiB
PHP
<?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
|
|
{
|
|
|
|
/**
|
|
* Initialize controller
|
|
*/
|
|
public static function init()
|
|
{
|
|
// Bypass cookie authentication for cart endpoints to allow guest users
|
|
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()
|
|
{
|
|
$namespace = 'woonoow/v1';
|
|
|
|
// Get cart
|
|
$result = register_rest_route($namespace, '/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 () {
|
|
// Allow both logged-in and guest users
|
|
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' => function () {
|
|
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' => function () {
|
|
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' => function () {
|
|
return true; },
|
|
'args' => [
|
|
'coupon_code' => [
|
|
'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' => [
|
|
'coupon_code' => [
|
|
'required' => true,
|
|
'type' => 'string',
|
|
],
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get cart contents
|
|
*/
|
|
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') ?: 1; // Default to 1
|
|
$variation_id = $request->get_param('variation_id');
|
|
|
|
// Initialize WooCommerce session and cart for REST API requests
|
|
if (!WC()->session) {
|
|
WC()->initialize_session();
|
|
}
|
|
if (!WC()->cart) {
|
|
WC()->initialize_cart();
|
|
}
|
|
// CRITICAL: Set session cookie for guest users to persist cart
|
|
if (!WC()->session->has_session()) {
|
|
WC()->session->set_customer_session_cookie(true);
|
|
}
|
|
|
|
// Validate product
|
|
$product = wc_get_product($product_id);
|
|
if (!$product) {
|
|
return new WP_Error('invalid_product', 'Product not found', ['status' => 404]);
|
|
}
|
|
|
|
// For variable products, get attributes from request or variation
|
|
$variation_attributes = [];
|
|
if ($variation_id > 0) {
|
|
$variation = wc_get_product($variation_id);
|
|
if (!$variation) {
|
|
return new WP_Error('invalid_variation', "Variation not found", ['status' => 404]);
|
|
}
|
|
|
|
if ($variation->get_parent_id() != $product_id) {
|
|
return new WP_Error('invalid_variation', "Variation does not belong to this product", ['status' => 400]);
|
|
}
|
|
|
|
if (!$variation->is_in_stock()) {
|
|
return new WP_Error('variation_not_available', "This variation is out of stock", ['status' => 400]);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clear any existing notices before adding to cart
|
|
wc_clear_notices();
|
|
|
|
// Add to cart with variation attributes
|
|
$cart_item_key = WC()->cart->add_to_cart($product_id, $quantity, $variation_id, $variation_attributes);
|
|
|
|
if (!$cart_item_key) {
|
|
$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();
|
|
|
|
return new WP_Error('add_to_cart_failed', $error_message, ['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');
|
|
|
|
// 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(),
|
|
], 200);
|
|
}
|
|
|
|
/**
|
|
* Remove item from cart
|
|
*/
|
|
public static function remove_from_cart(WP_REST_Request $request)
|
|
{
|
|
$cart_item_key = $request->get_param('cart_item_key');
|
|
|
|
// 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(),
|
|
], 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)
|
|
{
|
|
$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'];
|
|
|
|
// 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'],
|
|
'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' => $formatted_attributes,
|
|
];
|
|
}
|
|
|
|
// 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(),
|
|
];
|
|
}
|
|
}
|