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:
373
includes/Api/Controllers/CartController.php
Normal file
373
includes/Api/Controllers/CartController.php
Normal file
@@ -0,0 +1,373 @@
|
||||
<?php
|
||||
namespace WooNooW\Api\Controllers;
|
||||
|
||||
use WP_REST_Controller;
|
||||
use WP_REST_Response;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Cart Controller
|
||||
* Handles cart operations via REST API
|
||||
*/
|
||||
class CartController extends WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public function register_routes() {
|
||||
$namespace = 'woonoow/v1';
|
||||
|
||||
// Get cart
|
||||
register_rest_route($namespace, '/cart', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_cart'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// Add to cart
|
||||
register_rest_route($namespace, '/cart/add', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'add_to_cart'],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'product_id' => [
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
'sanitize_callback' => 'absint',
|
||||
],
|
||||
'quantity' => [
|
||||
'required' => false,
|
||||
'type' => 'integer',
|
||||
'default' => 1,
|
||||
'sanitize_callback' => 'absint',
|
||||
],
|
||||
'variation_id' => [
|
||||
'required' => false,
|
||||
'type' => 'integer',
|
||||
'sanitize_callback' => 'absint',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Update cart item
|
||||
register_rest_route($namespace, '/cart/update', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_cart_item'],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'cart_item_key' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
],
|
||||
'quantity' => [
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
'sanitize_callback' => 'absint',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Remove from cart
|
||||
register_rest_route($namespace, '/cart/remove', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'remove_from_cart'],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'cart_item_key' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Clear cart
|
||||
register_rest_route($namespace, '/cart/clear', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'clear_cart'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// Apply coupon
|
||||
register_rest_route($namespace, '/cart/apply-coupon', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'apply_coupon'],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'coupon_code' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Remove coupon
|
||||
register_rest_route($namespace, '/cart/remove-coupon', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'remove_coupon'],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'coupon_code' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cart contents
|
||||
*/
|
||||
public function get_cart($request) {
|
||||
if (!function_exists('WC')) {
|
||||
return new WP_Error('woocommerce_not_active', 'WooCommerce is not active', ['status' => 500]);
|
||||
}
|
||||
|
||||
// Ensure cart is initialized
|
||||
if (is_null(WC()->cart)) {
|
||||
wc_load_cart();
|
||||
}
|
||||
|
||||
$cart = WC()->cart;
|
||||
|
||||
return new WP_REST_Response([
|
||||
'items' => $this->format_cart_items($cart->get_cart()),
|
||||
'totals' => [
|
||||
'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(),
|
||||
'fee_total' => $cart->get_fee_total(),
|
||||
'fee_tax' => $cart->get_fee_tax(),
|
||||
'total' => $cart->get_total(''),
|
||||
'total_tax' => $cart->get_total_tax(),
|
||||
],
|
||||
'coupons' => $cart->get_applied_coupons(),
|
||||
'needs_shipping' => $cart->needs_shipping(),
|
||||
'needs_payment' => $cart->needs_payment(),
|
||||
'item_count' => $cart->get_cart_contents_count(),
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add product to cart
|
||||
*/
|
||||
public function add_to_cart($request) {
|
||||
if (!function_exists('WC')) {
|
||||
return new WP_Error('woocommerce_not_active', 'WooCommerce is not active', ['status' => 500]);
|
||||
}
|
||||
|
||||
// Ensure cart is initialized
|
||||
if (is_null(WC()->cart)) {
|
||||
wc_load_cart();
|
||||
}
|
||||
|
||||
$product_id = $request->get_param('product_id');
|
||||
$quantity = $request->get_param('quantity') ?: 1;
|
||||
$variation_id = $request->get_param('variation_id') ?: 0;
|
||||
|
||||
// Validate product
|
||||
$product = wc_get_product($product_id);
|
||||
if (!$product) {
|
||||
return new WP_Error('invalid_product', 'Product not found', ['status' => 404]);
|
||||
}
|
||||
|
||||
// Check stock
|
||||
if (!$product->is_in_stock()) {
|
||||
return new WP_Error('out_of_stock', 'Product is out of stock', ['status' => 400]);
|
||||
}
|
||||
|
||||
// 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([
|
||||
'success' => true,
|
||||
'cart_item_key' => $cart_item_key,
|
||||
'message' => sprintf('%s has been added to your cart.', $product->get_name()),
|
||||
'cart' => $this->get_cart($request)->data,
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cart item quantity
|
||||
*/
|
||||
public function update_cart_item($request) {
|
||||
if (!function_exists('WC')) {
|
||||
return new WP_Error('woocommerce_not_active', 'WooCommerce is not active', ['status' => 500]);
|
||||
}
|
||||
|
||||
// Ensure cart is initialized
|
||||
if (is_null(WC()->cart)) {
|
||||
wc_load_cart();
|
||||
}
|
||||
|
||||
$cart_item_key = $request->get_param('cart_item_key');
|
||||
$quantity = $request->get_param('quantity');
|
||||
|
||||
// Validate cart item
|
||||
$cart = WC()->cart->get_cart();
|
||||
if (!isset($cart[$cart_item_key])) {
|
||||
return new WP_Error('invalid_cart_item', 'Cart item not found', ['status' => 404]);
|
||||
}
|
||||
|
||||
// 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([
|
||||
'success' => true,
|
||||
'message' => 'Cart updated successfully',
|
||||
'cart' => $this->get_cart($request)->data,
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item from cart
|
||||
*/
|
||||
public function remove_from_cart($request) {
|
||||
if (!function_exists('WC')) {
|
||||
return new WP_Error('woocommerce_not_active', 'WooCommerce is not active', ['status' => 500]);
|
||||
}
|
||||
|
||||
// Ensure cart is initialized
|
||||
if (is_null(WC()->cart)) {
|
||||
wc_load_cart();
|
||||
}
|
||||
|
||||
$cart_item_key = $request->get_param('cart_item_key');
|
||||
|
||||
// Validate cart item
|
||||
$cart = WC()->cart->get_cart();
|
||||
if (!isset($cart[$cart_item_key])) {
|
||||
return new WP_Error('invalid_cart_item', '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([
|
||||
'success' => true,
|
||||
'message' => 'Item removed from cart',
|
||||
'cart' => $this->get_cart($request)->data,
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cart
|
||||
*/
|
||||
public function clear_cart($request) {
|
||||
if (!function_exists('WC')) {
|
||||
return new WP_Error('woocommerce_not_active', 'WooCommerce is not active', ['status' => 500]);
|
||||
}
|
||||
|
||||
// Ensure cart is initialized
|
||||
if (is_null(WC()->cart)) {
|
||||
wc_load_cart();
|
||||
}
|
||||
|
||||
WC()->cart->empty_cart();
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => true,
|
||||
'message' => 'Cart cleared successfully',
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply coupon
|
||||
*/
|
||||
public function apply_coupon($request) {
|
||||
if (!function_exists('WC')) {
|
||||
return new WP_Error('woocommerce_not_active', 'WooCommerce is not active', ['status' => 500]);
|
||||
}
|
||||
|
||||
// Ensure cart is initialized
|
||||
if (is_null(WC()->cart)) {
|
||||
wc_load_cart();
|
||||
}
|
||||
|
||||
$coupon_code = $request->get_param('coupon_code');
|
||||
|
||||
// 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([
|
||||
'success' => true,
|
||||
'message' => 'Coupon applied successfully',
|
||||
'cart' => $this->get_cart($request)->data,
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove coupon
|
||||
*/
|
||||
public function remove_coupon($request) {
|
||||
if (!function_exists('WC')) {
|
||||
return new WP_Error('woocommerce_not_active', 'WooCommerce is not active', ['status' => 500]);
|
||||
}
|
||||
|
||||
// Ensure cart is initialized
|
||||
if (is_null(WC()->cart)) {
|
||||
wc_load_cart();
|
||||
}
|
||||
|
||||
$coupon_code = $request->get_param('coupon_code');
|
||||
|
||||
// Remove coupon
|
||||
$removed = WC()->cart->remove_coupon($coupon_code);
|
||||
|
||||
if (!$removed) {
|
||||
return new WP_Error('coupon_remove_failed', 'Failed to remove coupon', ['status' => 400]);
|
||||
}
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => true,
|
||||
'message' => 'Coupon removed successfully',
|
||||
'cart' => $this->get_cart($request)->data,
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format cart items for response
|
||||
*/
|
||||
private function format_cart_items($cart_items) {
|
||||
$formatted = [];
|
||||
|
||||
foreach ($cart_items as $cart_item_key => $cart_item) {
|
||||
$product = $cart_item['data'];
|
||||
|
||||
$formatted[] = [
|
||||
'key' => $cart_item_key,
|
||||
'product_id' => $cart_item['product_id'],
|
||||
'variation_id' => $cart_item['variation_id'],
|
||||
'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_image_url($product->get_image_id(), 'thumbnail'),
|
||||
'permalink' => $product->get_permalink(),
|
||||
];
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
}
|
||||
317
includes/Api/Controllers/SettingsController.php
Normal file
317
includes/Api/Controllers/SettingsController.php
Normal file
@@ -0,0 +1,317 @@
|
||||
<?php
|
||||
namespace WooNooW\Api\Controllers;
|
||||
|
||||
use WP_REST_Controller;
|
||||
use WP_REST_Server;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Settings Controller
|
||||
* Handles Customer SPA settings via REST API
|
||||
*/
|
||||
class SettingsController extends WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* Namespace
|
||||
*/
|
||||
protected $namespace = 'woonoow/v1';
|
||||
|
||||
/**
|
||||
* Rest base
|
||||
*/
|
||||
protected $rest_base = 'settings';
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public function register_routes() {
|
||||
// Get/Update Customer SPA settings
|
||||
register_rest_route($this->namespace, '/' . $this->rest_base . '/customer-spa', [
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'get_customer_spa_settings'],
|
||||
'permission_callback' => [$this, 'get_settings_permissions_check'],
|
||||
],
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'update_customer_spa_settings'],
|
||||
'permission_callback' => [$this, 'update_settings_permissions_check'],
|
||||
'args' => $this->get_customer_spa_schema(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Customer SPA settings
|
||||
*/
|
||||
public function get_customer_spa_settings(WP_REST_Request $request) {
|
||||
$settings = $this->get_default_settings();
|
||||
$saved_settings = get_option('woonoow_customer_spa_settings', []);
|
||||
|
||||
// Merge with saved settings
|
||||
if (!empty($saved_settings)) {
|
||||
$settings = array_replace_recursive($settings, $saved_settings);
|
||||
}
|
||||
|
||||
// Add enabled flag
|
||||
$settings['enabled'] = get_option('woonoow_customer_spa_enabled', false);
|
||||
|
||||
return new WP_REST_Response($settings, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Customer SPA settings
|
||||
*/
|
||||
public function update_customer_spa_settings(WP_REST_Request $request) {
|
||||
$params = $request->get_json_params();
|
||||
|
||||
// Extract enabled flag
|
||||
if (isset($params['enabled'])) {
|
||||
update_option('woonoow_customer_spa_enabled', (bool) $params['enabled']);
|
||||
unset($params['enabled']);
|
||||
}
|
||||
|
||||
// Get current settings
|
||||
$current_settings = get_option('woonoow_customer_spa_settings', $this->get_default_settings());
|
||||
|
||||
// Merge with new settings
|
||||
$new_settings = array_replace_recursive($current_settings, $params);
|
||||
|
||||
// Validate settings
|
||||
$validated = $this->validate_settings($new_settings);
|
||||
if (is_wp_error($validated)) {
|
||||
return $validated;
|
||||
}
|
||||
|
||||
// Save settings
|
||||
update_option('woonoow_customer_spa_settings', $new_settings);
|
||||
|
||||
// Return updated settings
|
||||
$new_settings['enabled'] = get_option('woonoow_customer_spa_enabled', false);
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => true,
|
||||
'data' => $new_settings,
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default settings
|
||||
*/
|
||||
private function get_default_settings() {
|
||||
return [
|
||||
'mode' => 'disabled',
|
||||
'checkoutPages' => [
|
||||
'checkout' => true,
|
||||
'thankyou' => true,
|
||||
'account' => true,
|
||||
'cart' => false,
|
||||
],
|
||||
'layout' => 'modern',
|
||||
'branding' => [
|
||||
'logo' => '',
|
||||
'favicon' => '',
|
||||
'siteName' => get_bloginfo('name'),
|
||||
],
|
||||
'colors' => [
|
||||
'primary' => '#3B82F6',
|
||||
'secondary' => '#8B5CF6',
|
||||
'accent' => '#10B981',
|
||||
'background' => '#FFFFFF',
|
||||
'text' => '#1F2937',
|
||||
],
|
||||
'typography' => [
|
||||
'preset' => 'professional',
|
||||
'customFonts' => null,
|
||||
],
|
||||
'menus' => [
|
||||
'primary' => 0,
|
||||
'footer' => 0,
|
||||
],
|
||||
'homepage' => [
|
||||
'sections' => [
|
||||
[
|
||||
'id' => 'hero-1',
|
||||
'type' => 'hero',
|
||||
'enabled' => true,
|
||||
'order' => 0,
|
||||
'config' => [],
|
||||
],
|
||||
[
|
||||
'id' => 'featured-1',
|
||||
'type' => 'featured',
|
||||
'enabled' => true,
|
||||
'order' => 1,
|
||||
'config' => [],
|
||||
],
|
||||
[
|
||||
'id' => 'categories-1',
|
||||
'type' => 'categories',
|
||||
'enabled' => true,
|
||||
'order' => 2,
|
||||
'config' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
'product' => [
|
||||
'layout' => 'standard',
|
||||
'showRelatedProducts' => true,
|
||||
'showReviews' => true,
|
||||
],
|
||||
'checkout' => [
|
||||
'style' => 'onepage',
|
||||
'enableGuestCheckout' => true,
|
||||
'showTrustBadges' => true,
|
||||
'showOrderSummary' => 'sidebar',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate settings
|
||||
*/
|
||||
private function validate_settings($settings) {
|
||||
// Validate mode
|
||||
if (isset($settings['mode']) && !in_array($settings['mode'], ['disabled', 'full', 'checkout_only'])) {
|
||||
return new WP_Error(
|
||||
'invalid_mode',
|
||||
__('Invalid mode. Must be disabled, full, or checkout_only.', 'woonoow'),
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
// Validate layout
|
||||
if (isset($settings['layout']) && !in_array($settings['layout'], ['classic', 'modern', 'boutique', 'launch'])) {
|
||||
return new WP_Error(
|
||||
'invalid_layout',
|
||||
__('Invalid layout. Must be classic, modern, boutique, or launch.', 'woonoow'),
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
// Validate colors (hex format)
|
||||
if (isset($settings['colors'])) {
|
||||
foreach ($settings['colors'] as $key => $color) {
|
||||
if (!preg_match('/^#[a-fA-F0-9]{6}$/', $color)) {
|
||||
return new WP_Error(
|
||||
'invalid_color',
|
||||
sprintf(__('Invalid color format for %s. Must be hex format (#RRGGBB).', 'woonoow'), $key),
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate typography preset
|
||||
if (isset($settings['typography']['preset'])) {
|
||||
$valid_presets = ['professional', 'modern', 'elegant', 'tech', 'custom'];
|
||||
if (!in_array($settings['typography']['preset'], $valid_presets)) {
|
||||
return new WP_Error(
|
||||
'invalid_typography',
|
||||
__('Invalid typography preset.', 'woonoow'),
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Customer SPA settings schema
|
||||
*/
|
||||
private function get_customer_spa_schema() {
|
||||
return [
|
||||
'mode' => [
|
||||
'type' => 'string',
|
||||
'enum' => ['disabled', 'full', 'checkout_only'],
|
||||
'description' => __('Customer SPA mode', 'woonoow'),
|
||||
],
|
||||
'checkoutPages' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'checkout' => ['type' => 'boolean'],
|
||||
'thankyou' => ['type' => 'boolean'],
|
||||
'account' => ['type' => 'boolean'],
|
||||
'cart' => ['type' => 'boolean'],
|
||||
],
|
||||
],
|
||||
'layout' => [
|
||||
'type' => 'string',
|
||||
'enum' => ['classic', 'modern', 'boutique', 'launch'],
|
||||
'description' => __('Master layout', 'woonoow'),
|
||||
],
|
||||
'branding' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'logo' => ['type' => 'string'],
|
||||
'favicon' => ['type' => 'string'],
|
||||
'siteName' => ['type' => 'string'],
|
||||
],
|
||||
],
|
||||
'colors' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'primary' => ['type' => 'string', 'pattern' => '^#[a-fA-F0-9]{6}$'],
|
||||
'secondary' => ['type' => 'string', 'pattern' => '^#[a-fA-F0-9]{6}$'],
|
||||
'accent' => ['type' => 'string', 'pattern' => '^#[a-fA-F0-9]{6}$'],
|
||||
'background' => ['type' => 'string', 'pattern' => '^#[a-fA-F0-9]{6}$'],
|
||||
'text' => ['type' => 'string', 'pattern' => '^#[a-fA-F0-9]{6}$'],
|
||||
],
|
||||
],
|
||||
'typography' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'preset' => ['type' => 'string', 'enum' => ['professional', 'modern', 'elegant', 'tech', 'custom']],
|
||||
'customFonts' => ['type' => ['object', 'null']],
|
||||
],
|
||||
],
|
||||
'menus' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'primary' => ['type' => 'integer'],
|
||||
'footer' => ['type' => 'integer'],
|
||||
],
|
||||
],
|
||||
'homepage' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'sections' => ['type' => 'array'],
|
||||
],
|
||||
],
|
||||
'product' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'layout' => ['type' => 'string'],
|
||||
'showRelatedProducts' => ['type' => 'boolean'],
|
||||
'showReviews' => ['type' => 'boolean'],
|
||||
],
|
||||
],
|
||||
'checkout' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'style' => ['type' => 'string'],
|
||||
'enableGuestCheckout' => ['type' => 'boolean'],
|
||||
'showTrustBadges' => ['type' => 'boolean'],
|
||||
'showOrderSummary' => ['type' => 'string'],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check permissions for getting settings
|
||||
*/
|
||||
public function get_settings_permissions_check() {
|
||||
return current_user_can('manage_woocommerce');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check permissions for updating settings
|
||||
*/
|
||||
public function update_settings_permissions_check() {
|
||||
return current_user_can('manage_woocommerce');
|
||||
}
|
||||
}
|
||||
@@ -337,12 +337,33 @@ class ProductsController {
|
||||
$product->set_tag_ids($data['tags']);
|
||||
}
|
||||
|
||||
// Images
|
||||
if (!empty($data['image_id'])) {
|
||||
$product->set_image_id($data['image_id']);
|
||||
}
|
||||
if (!empty($data['gallery_image_ids']) && is_array($data['gallery_image_ids'])) {
|
||||
$product->set_gallery_image_ids($data['gallery_image_ids']);
|
||||
// Images - support both image_id/gallery_image_ids and images array
|
||||
if (!empty($data['images']) && is_array($data['images'])) {
|
||||
// Convert URLs to attachment IDs
|
||||
$image_ids = [];
|
||||
foreach ($data['images'] as $image_url) {
|
||||
$attachment_id = attachment_url_to_postid($image_url);
|
||||
if ($attachment_id) {
|
||||
$image_ids[] = $attachment_id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($image_ids)) {
|
||||
// First image is featured
|
||||
$product->set_image_id($image_ids[0]);
|
||||
// Rest are gallery
|
||||
if (count($image_ids) > 1) {
|
||||
$product->set_gallery_image_ids(array_slice($image_ids, 1));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Legacy support for direct IDs
|
||||
if (!empty($data['image_id'])) {
|
||||
$product->set_image_id($data['image_id']);
|
||||
}
|
||||
if (!empty($data['gallery_image_ids']) && is_array($data['gallery_image_ids'])) {
|
||||
$product->set_gallery_image_ids($data['gallery_image_ids']);
|
||||
}
|
||||
}
|
||||
|
||||
$product->save();
|
||||
@@ -407,12 +428,35 @@ class ProductsController {
|
||||
$product->set_tag_ids($data['tags']);
|
||||
}
|
||||
|
||||
// Images
|
||||
if (isset($data['image_id'])) {
|
||||
$product->set_image_id($data['image_id']);
|
||||
}
|
||||
if (isset($data['gallery_image_ids'])) {
|
||||
$product->set_gallery_image_ids($data['gallery_image_ids']);
|
||||
// Images - support both image_id/gallery_image_ids and images array
|
||||
if (isset($data['images']) && is_array($data['images']) && !empty($data['images'])) {
|
||||
// Convert URLs to attachment IDs
|
||||
$image_ids = [];
|
||||
foreach ($data['images'] as $image_url) {
|
||||
$attachment_id = attachment_url_to_postid($image_url);
|
||||
if ($attachment_id) {
|
||||
$image_ids[] = $attachment_id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($image_ids)) {
|
||||
// First image is featured
|
||||
$product->set_image_id($image_ids[0]);
|
||||
// Rest are gallery
|
||||
if (count($image_ids) > 1) {
|
||||
$product->set_gallery_image_ids(array_slice($image_ids, 1));
|
||||
} else {
|
||||
$product->set_gallery_image_ids([]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Legacy support for direct IDs
|
||||
if (isset($data['image_id'])) {
|
||||
$product->set_image_id($data['image_id']);
|
||||
}
|
||||
if (isset($data['gallery_image_ids'])) {
|
||||
$product->set_gallery_image_ids($data['gallery_image_ids']);
|
||||
}
|
||||
}
|
||||
|
||||
// Update custom meta fields (Level 1 compatibility)
|
||||
@@ -596,7 +640,24 @@ class ProductsController {
|
||||
$data['downloadable'] = $product->is_downloadable();
|
||||
$data['featured'] = $product->is_featured();
|
||||
|
||||
// Gallery images
|
||||
// Images array (URLs) for frontend - featured + gallery
|
||||
$images = [];
|
||||
$featured_image_id = $product->get_image_id();
|
||||
if ($featured_image_id) {
|
||||
$featured_url = wp_get_attachment_url($featured_image_id);
|
||||
if ($featured_url) {
|
||||
$images[] = $featured_url;
|
||||
}
|
||||
}
|
||||
foreach ($product->get_gallery_image_ids() as $image_id) {
|
||||
$url = wp_get_attachment_url($image_id);
|
||||
if ($url) {
|
||||
$images[] = $url;
|
||||
}
|
||||
}
|
||||
$data['images'] = $images;
|
||||
|
||||
// Gallery images (detailed info)
|
||||
$gallery = [];
|
||||
foreach ($product->get_gallery_image_ids() as $image_id) {
|
||||
$image = wp_get_attachment_image_src($image_id, 'full');
|
||||
@@ -691,6 +752,11 @@ class ProductsController {
|
||||
$formatted_attributes[$clean_name] = $value;
|
||||
}
|
||||
|
||||
$image_url = $image ? $image[0] : '';
|
||||
if (!$image_url && $variation->get_image_id()) {
|
||||
$image_url = wp_get_attachment_url($variation->get_image_id());
|
||||
}
|
||||
|
||||
$variations[] = [
|
||||
'id' => $variation->get_id(),
|
||||
'sku' => $variation->get_sku(),
|
||||
@@ -702,7 +768,8 @@ class ProductsController {
|
||||
'manage_stock' => $variation->get_manage_stock(),
|
||||
'attributes' => $formatted_attributes,
|
||||
'image_id' => $variation->get_image_id(),
|
||||
'image_url' => $image ? $image[0] : '',
|
||||
'image_url' => $image_url,
|
||||
'image' => $image_url, // For form compatibility
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -749,7 +816,16 @@ class ProductsController {
|
||||
if (isset($var_data['manage_stock'])) $variation->set_manage_stock($var_data['manage_stock']);
|
||||
if (isset($var_data['stock_quantity'])) $variation->set_stock_quantity($var_data['stock_quantity']);
|
||||
if (isset($var_data['attributes'])) $variation->set_attributes($var_data['attributes']);
|
||||
if (isset($var_data['image_id'])) $variation->set_image_id($var_data['image_id']);
|
||||
|
||||
// Handle image - support both image_id and image URL
|
||||
if (isset($var_data['image']) && !empty($var_data['image'])) {
|
||||
$image_id = attachment_url_to_postid($var_data['image']);
|
||||
if ($image_id) {
|
||||
$variation->set_image_id($image_id);
|
||||
}
|
||||
} elseif (isset($var_data['image_id'])) {
|
||||
$variation->set_image_id($var_data['image_id']);
|
||||
}
|
||||
|
||||
$variation->save();
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@ use WooNooW\Api\ActivityLogController;
|
||||
use WooNooW\Api\ProductsController;
|
||||
use WooNooW\Api\CouponsController;
|
||||
use WooNooW\Api\CustomersController;
|
||||
use WooNooW\Frontend\ShopController;
|
||||
use WooNooW\Frontend\CartController as FrontendCartController;
|
||||
use WooNooW\Frontend\AccountController;
|
||||
use WooNooW\Frontend\HookBridge;
|
||||
use WooNooW\Api\Controllers\SettingsController;
|
||||
use WooNooW\Api\Controllers\CartController as ApiCartController;
|
||||
|
||||
class Routes {
|
||||
public static function init() {
|
||||
@@ -66,6 +72,14 @@ class Routes {
|
||||
OrdersController::register();
|
||||
AnalyticsController::register_routes();
|
||||
|
||||
// Settings controller
|
||||
$settings_controller = new SettingsController();
|
||||
$settings_controller->register_routes();
|
||||
|
||||
// Cart controller (API)
|
||||
$api_cart_controller = new ApiCartController();
|
||||
$api_cart_controller->register_routes();
|
||||
|
||||
// Payments controller
|
||||
$payments_controller = new PaymentsController();
|
||||
$payments_controller->register_routes();
|
||||
@@ -116,6 +130,14 @@ class Routes {
|
||||
|
||||
// Customers controller
|
||||
CustomersController::register_routes();
|
||||
|
||||
// Frontend controllers (customer-facing)
|
||||
error_log('WooNooW Routes: Registering Frontend controllers');
|
||||
ShopController::register_routes();
|
||||
FrontendCartController::register_routes();
|
||||
AccountController::register_routes();
|
||||
HookBridge::register_routes();
|
||||
error_log('WooNooW Routes: Frontend controllers registered');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ if ( ! defined('ABSPATH') ) exit;
|
||||
*/
|
||||
class NavigationRegistry {
|
||||
const NAV_OPTION = 'wnw_nav_tree';
|
||||
const NAV_VERSION = '1.0.0';
|
||||
const NAV_VERSION = '1.0.1'; // Bumped to add Customer SPA settings
|
||||
|
||||
/**
|
||||
* Initialize hooks
|
||||
@@ -29,6 +29,15 @@ class NavigationRegistry {
|
||||
* Build the complete navigation tree
|
||||
*/
|
||||
public static function build_nav_tree() {
|
||||
// Check if we need to rebuild (version mismatch)
|
||||
$cached = get_option(self::NAV_OPTION, []);
|
||||
$cached_version = $cached['version'] ?? '';
|
||||
|
||||
if ($cached_version === self::NAV_VERSION && !empty($cached['tree'])) {
|
||||
// Cache is valid, no need to rebuild
|
||||
return;
|
||||
}
|
||||
|
||||
// Base navigation tree (core WooNooW sections)
|
||||
$tree = self::get_base_tree();
|
||||
|
||||
@@ -182,6 +191,7 @@ class NavigationRegistry {
|
||||
['label' => __('Tax', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/tax'],
|
||||
['label' => __('Customers', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/customers'],
|
||||
['label' => __('Notifications', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/notifications'],
|
||||
['label' => __('Customer SPA', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/customer-spa'],
|
||||
['label' => __('Developer', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/developer'],
|
||||
];
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ use WooNooW\Core\Notifications\PushNotificationHandler;
|
||||
use WooNooW\Core\Notifications\EmailManager;
|
||||
use WooNooW\Core\ActivityLog\ActivityLogTable;
|
||||
use WooNooW\Branding;
|
||||
use WooNooW\Frontend\Assets as FrontendAssets;
|
||||
use WooNooW\Frontend\Shortcodes;
|
||||
use WooNooW\Frontend\TemplateOverride;
|
||||
|
||||
class Bootstrap {
|
||||
public static function init() {
|
||||
@@ -37,6 +40,11 @@ class Bootstrap {
|
||||
PushNotificationHandler::init();
|
||||
EmailManager::instance(); // Initialize custom email system
|
||||
|
||||
// Frontend (customer-spa)
|
||||
FrontendAssets::init();
|
||||
Shortcodes::init();
|
||||
TemplateOverride::init();
|
||||
|
||||
// Activity Log
|
||||
ActivityLogTable::create_table();
|
||||
|
||||
|
||||
207
includes/Core/Installer.php
Normal file
207
includes/Core/Installer.php
Normal file
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
namespace WooNooW\Core;
|
||||
|
||||
/**
|
||||
* Plugin Installer
|
||||
* Handles plugin activation tasks
|
||||
*/
|
||||
class Installer {
|
||||
|
||||
/**
|
||||
* Run on plugin activation
|
||||
*/
|
||||
public static function activate() {
|
||||
// Create WooNooW pages
|
||||
self::create_pages();
|
||||
|
||||
// Set WooCommerce to use HPOS
|
||||
update_option('woocommerce_custom_orders_table_enabled', 'yes');
|
||||
update_option('woocommerce_custom_orders_table_migration_enabled', 'yes');
|
||||
|
||||
// Flush rewrite rules
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update WooNooW pages
|
||||
* Smart detection: reuses existing WooCommerce pages if they exist
|
||||
*/
|
||||
private static function create_pages() {
|
||||
$pages = [
|
||||
'shop' => [
|
||||
'title' => 'Shop',
|
||||
'content' => '[woonoow_shop]',
|
||||
'wc_option' => 'woocommerce_shop_page_id',
|
||||
],
|
||||
'cart' => [
|
||||
'title' => 'Cart',
|
||||
'content' => '[woonoow_cart]',
|
||||
'wc_option' => 'woocommerce_cart_page_id',
|
||||
],
|
||||
'checkout' => [
|
||||
'title' => 'Checkout',
|
||||
'content' => '[woonoow_checkout]',
|
||||
'wc_option' => 'woocommerce_checkout_page_id',
|
||||
],
|
||||
'account' => [
|
||||
'title' => 'My Account',
|
||||
'content' => '[woonoow_account]',
|
||||
'wc_option' => 'woocommerce_myaccount_page_id',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($pages as $key => $page_data) {
|
||||
$page_id = null;
|
||||
|
||||
// Strategy 1: Check if WooCommerce already has a page set
|
||||
if (isset($page_data['wc_option'])) {
|
||||
$wc_page_id = get_option($page_data['wc_option']);
|
||||
if ($wc_page_id && get_post($wc_page_id)) {
|
||||
$page_id = $wc_page_id;
|
||||
error_log("WooNooW: Found existing WooCommerce {$page_data['title']} page (ID: {$page_id})");
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Check if WooNooW already created a page
|
||||
if (!$page_id) {
|
||||
$woonoow_page_id = get_option('woonoow_' . $key . '_page_id');
|
||||
if ($woonoow_page_id && get_post($woonoow_page_id)) {
|
||||
$page_id = $woonoow_page_id;
|
||||
error_log("WooNooW: Found existing WooNooW {$page_data['title']} page (ID: {$page_id})");
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: Search for page by title
|
||||
if (!$page_id) {
|
||||
$existing_page = get_page_by_title($page_data['title'], OBJECT, 'page');
|
||||
if ($existing_page) {
|
||||
$page_id = $existing_page->ID;
|
||||
error_log("WooNooW: Found existing {$page_data['title']} page by title (ID: {$page_id})");
|
||||
}
|
||||
}
|
||||
|
||||
// If page exists, update its content with our shortcode
|
||||
if ($page_id) {
|
||||
$current_post = get_post($page_id);
|
||||
|
||||
// Only update if it doesn't already have our shortcode
|
||||
if (!has_shortcode($current_post->post_content, 'woonoow_' . $key)) {
|
||||
// Backup original content
|
||||
update_post_meta($page_id, '_woonoow_original_content', $current_post->post_content);
|
||||
|
||||
// Update with our shortcode
|
||||
wp_update_post([
|
||||
'ID' => $page_id,
|
||||
'post_content' => $page_data['content'],
|
||||
]);
|
||||
|
||||
error_log("WooNooW: Updated {$page_data['title']} page with WooNooW shortcode");
|
||||
} else {
|
||||
error_log("WooNooW: {$page_data['title']} page already has WooNooW shortcode");
|
||||
}
|
||||
} else {
|
||||
// No existing page found, create new one
|
||||
$page_id = wp_insert_post([
|
||||
'post_title' => $page_data['title'],
|
||||
'post_content' => $page_data['content'],
|
||||
'post_status' => 'publish',
|
||||
'post_type' => 'page',
|
||||
'post_author' => get_current_user_id(),
|
||||
'comment_status' => 'closed',
|
||||
]);
|
||||
|
||||
if ($page_id && !is_wp_error($page_id)) {
|
||||
error_log("WooNooW: Created new {$page_data['title']} page (ID: {$page_id})");
|
||||
}
|
||||
}
|
||||
|
||||
// Store page ID and update WooCommerce settings
|
||||
if ($page_id && !is_wp_error($page_id)) {
|
||||
update_option('woonoow_' . $key . '_page_id', $page_id);
|
||||
|
||||
if (isset($page_data['wc_option'])) {
|
||||
update_option($page_data['wc_option'], $page_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run on plugin deactivation
|
||||
*/
|
||||
public static function deactivate() {
|
||||
// Restore original page content
|
||||
self::restore_original_content();
|
||||
|
||||
// Flush rewrite rules
|
||||
flush_rewrite_rules();
|
||||
|
||||
// Note: We don't delete pages on deactivation
|
||||
// Users might have content on those pages
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore original content to pages that were modified
|
||||
*/
|
||||
private static function restore_original_content() {
|
||||
$page_keys = ['shop', 'cart', 'checkout', 'account'];
|
||||
|
||||
foreach ($page_keys as $key) {
|
||||
$page_id = get_option('woonoow_' . $key . '_page_id');
|
||||
|
||||
if ($page_id) {
|
||||
$original_content = get_post_meta($page_id, '_woonoow_original_content', true);
|
||||
|
||||
if ($original_content) {
|
||||
// Restore original content
|
||||
wp_update_post([
|
||||
'ID' => $page_id,
|
||||
'post_content' => $original_content,
|
||||
]);
|
||||
|
||||
// Remove backup
|
||||
delete_post_meta($page_id, '_woonoow_original_content');
|
||||
|
||||
error_log("WooNooW: Restored original content for page ID: {$page_id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run on plugin uninstall
|
||||
*/
|
||||
public static function uninstall() {
|
||||
// Only delete if user explicitly wants to remove all data
|
||||
if (get_option('woonoow_remove_data_on_uninstall', false)) {
|
||||
self::delete_pages();
|
||||
self::delete_options();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete WooNooW pages
|
||||
*/
|
||||
private static function delete_pages() {
|
||||
$page_keys = ['shop', 'cart', 'checkout', 'account'];
|
||||
|
||||
foreach ($page_keys as $key) {
|
||||
$page_id = get_option('woonoow_' . $key . '_page_id');
|
||||
|
||||
if ($page_id) {
|
||||
wp_delete_post($page_id, true); // Force delete
|
||||
delete_option('woonoow_' . $key . '_page_id');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete WooNooW options
|
||||
*/
|
||||
private static function delete_options() {
|
||||
global $wpdb;
|
||||
|
||||
// Delete all options starting with 'woonoow_'
|
||||
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'woonoow_%'");
|
||||
}
|
||||
}
|
||||
365
includes/Frontend/AccountController.php
Normal file
365
includes/Frontend/AccountController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
304
includes/Frontend/Assets.php
Normal file
304
includes/Frontend/Assets.php
Normal 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');
|
||||
}
|
||||
}
|
||||
306
includes/Frontend/CartController.php
Normal file
306
includes/Frontend/CartController.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
224
includes/Frontend/HookBridge.php
Normal file
224
includes/Frontend/HookBridge.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
347
includes/Frontend/ShopController.php
Normal file
347
includes/Frontend/ShopController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
110
includes/Frontend/Shortcodes.php
Normal file
110
includes/Frontend/Shortcodes.php
Normal 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();
|
||||
}
|
||||
}
|
||||
247
includes/Frontend/TemplateOverride.php
Normal file
247
includes/Frontend/TemplateOverride.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user