fix: WP-Admin CSS conflicts and add-to-cart redirect
- Fix CSS conflicts between WP-Admin and SPA (radio buttons, chart text) - Add Tailwind important selector scoped to #woonoow-admin-app - Remove overly aggressive inline SVG styles from Assets.php - Add targeted WordPress admin CSS overrides in index.css - Fix add-to-cart redirect to use woocommerce_add_to_cart_redirect filter - Let WooCommerce handle cart operations natively for proper session management - Remove duplicate tailwind.config.cjs
This commit is contained in:
@@ -76,6 +76,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
WordPress Admin Override Fixes
|
||||||
|
These rules use high specificity + !important
|
||||||
|
to override WordPress admin CSS conflicts
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Fix SVG icon styling - WordPress sets fill:currentColor on all SVGs */
|
||||||
|
#woonoow-admin-app svg {
|
||||||
|
fill: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* But allow explicit fill-current class to work for filled icons */
|
||||||
|
#woonoow-admin-app svg.fill-current,
|
||||||
|
#woonoow-admin-app .fill-current svg,
|
||||||
|
#woonoow-admin-app [class*="fill-"] svg {
|
||||||
|
fill: currentColor !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix radio button indicator - WordPress overrides circle fill */
|
||||||
|
#woonoow-admin-app [data-radix-radio-group-item] svg,
|
||||||
|
#woonoow-admin-app [role="radio"] svg {
|
||||||
|
fill: currentColor !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix font-weight inheritance - prevent WordPress bold overrides */
|
||||||
|
#woonoow-admin-app text,
|
||||||
|
#woonoow-admin-app tspan {
|
||||||
|
font-weight: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset form element styling that WordPress overrides */
|
||||||
|
#woonoow-admin-app input[type="radio"],
|
||||||
|
#woonoow-admin-app input[type="checkbox"] {
|
||||||
|
appearance: none !important;
|
||||||
|
-webkit-appearance: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Command palette input: remove native borders/shadows to match shadcn */
|
/* Command palette input: remove native borders/shadows to match shadcn */
|
||||||
.command-palette-search {
|
.command-palette-search {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ export default function ProductEdit() {
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
formRef={formRef}
|
formRef={formRef}
|
||||||
hideSubmitButton={true}
|
hideSubmitButton={true}
|
||||||
|
productId={product.id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Level 1 compatibility: Custom meta fields from plugins */}
|
{/* Level 1 compatibility: Custom meta fields from plugins */}
|
||||||
|
|||||||
215
admin-spa/src/routes/Products/partials/DirectCartLinks.tsx
Normal file
215
admin-spa/src/routes/Products/partials/DirectCartLinks.tsx
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Copy, Check, ExternalLink } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface DirectCartLinksProps {
|
||||||
|
productId: number;
|
||||||
|
productType: 'simple' | 'variable';
|
||||||
|
variations?: Array<{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
attributes: Record<string, string>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DirectCartLinks({ productId, productType, variations = [] }: DirectCartLinksProps) {
|
||||||
|
const [quantity, setQuantity] = useState(1);
|
||||||
|
const [copiedLink, setCopiedLink] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const siteUrl = window.location.origin;
|
||||||
|
const spaPagePath = '/store'; // This should ideally come from settings
|
||||||
|
|
||||||
|
const generateLink = (variationId?: number, redirect: 'cart' | 'checkout' = 'cart') => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set('add-to-cart', productId.toString());
|
||||||
|
if (variationId) {
|
||||||
|
params.set('variation_id', variationId.toString());
|
||||||
|
}
|
||||||
|
if (quantity > 1) {
|
||||||
|
params.set('quantity', quantity.toString());
|
||||||
|
}
|
||||||
|
params.set('redirect', redirect);
|
||||||
|
|
||||||
|
return `${siteUrl}${spaPagePath}?${params.toString()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = async (link: string, label: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(link);
|
||||||
|
setCopiedLink(link);
|
||||||
|
toast.success(`${label} link copied!`);
|
||||||
|
setTimeout(() => setCopiedLink(null), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('Failed to copy link');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const LinkRow = ({
|
||||||
|
label,
|
||||||
|
link,
|
||||||
|
description
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
link: string;
|
||||||
|
description?: string;
|
||||||
|
}) => {
|
||||||
|
const isCopied = copiedLink === link;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-sm font-medium">{label}</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => copyToClipboard(link, label)}
|
||||||
|
>
|
||||||
|
{isCopied ? (
|
||||||
|
<>
|
||||||
|
<Check className="h-4 w-4 mr-1" />
|
||||||
|
Copied
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Copy className="h-4 w-4 mr-1" />
|
||||||
|
Copy
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => window.open(link, '_blank')}
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={link}
|
||||||
|
readOnly
|
||||||
|
className="font-mono text-xs"
|
||||||
|
onClick={(e) => e.currentTarget.select()}
|
||||||
|
/>
|
||||||
|
{description && (
|
||||||
|
<p className="text-xs text-muted-foreground">{description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Direct-to-Cart Links</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Generate copyable links that add this product to cart and redirect to cart or checkout page.
|
||||||
|
Perfect for landing pages, email campaigns, and social media.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{/* Quantity Selector */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="link-quantity">Default Quantity</Label>
|
||||||
|
<Input
|
||||||
|
id="link-quantity"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={quantity}
|
||||||
|
onChange={(e) => setQuantity(Math.max(1, parseInt(e.target.value) || 1))}
|
||||||
|
className="w-32"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Set quantity to 1 to exclude from URL (cleaner links)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Simple Product Links */}
|
||||||
|
{productType === 'simple' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="border-b pb-2">
|
||||||
|
<h4 className="font-medium">Simple Product Links</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LinkRow
|
||||||
|
label="Add to Cart"
|
||||||
|
link={generateLink(undefined, 'cart')}
|
||||||
|
description="Adds product to cart and shows cart page"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LinkRow
|
||||||
|
label="Direct to Checkout"
|
||||||
|
link={generateLink(undefined, 'checkout')}
|
||||||
|
description="Adds product to cart and goes directly to checkout"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Variable Product Links */}
|
||||||
|
{productType === 'variable' && variations.length > 0 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="border-b pb-2">
|
||||||
|
<h4 className="font-medium">Variable Product Links</h4>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{variations.length} variation(s) - Select a variation to generate links
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{variations.map((variation, index) => (
|
||||||
|
<details key={variation.id} className="group border rounded-lg">
|
||||||
|
<summary className="cursor-pointer p-3 hover:bg-muted/50 flex items-center justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<span className="font-medium text-sm">{variation.name}</span>
|
||||||
|
<span className="text-xs text-muted-foreground ml-2">
|
||||||
|
(ID: {variation.id})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 transition-transform group-open:rotate-180"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<div className="p-4 pt-0 space-y-3 border-t">
|
||||||
|
<LinkRow
|
||||||
|
label="Add to Cart"
|
||||||
|
link={generateLink(variation.id, 'cart')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LinkRow
|
||||||
|
label="Direct to Checkout"
|
||||||
|
link={generateLink(variation.id, 'checkout')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* URL Parameters Reference */}
|
||||||
|
<div className="mt-6 p-4 bg-muted rounded-lg">
|
||||||
|
<h4 className="font-medium text-sm mb-2">URL Parameters Reference</h4>
|
||||||
|
<div className="space-y-1 text-xs text-muted-foreground">
|
||||||
|
<div><code className="bg-background px-1 py-0.5 rounded">add-to-cart</code> - Product ID (required)</div>
|
||||||
|
<div><code className="bg-background px-1 py-0.5 rounded">variation_id</code> - Variation ID (for variable products)</div>
|
||||||
|
<div><code className="bg-background px-1 py-0.5 rounded">quantity</code> - Quantity (default: 1)</div>
|
||||||
|
<div><code className="bg-background px-1 py-0.5 rounded">redirect</code> - Destination: <code>cart</code> or <code>checkout</code></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ type Props = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
formRef?: React.RefObject<HTMLFormElement>;
|
formRef?: React.RefObject<HTMLFormElement>;
|
||||||
hideSubmitButton?: boolean;
|
hideSubmitButton?: boolean;
|
||||||
|
productId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ProductFormTabbed({
|
export function ProductFormTabbed({
|
||||||
@@ -50,6 +51,7 @@ export function ProductFormTabbed({
|
|||||||
className,
|
className,
|
||||||
formRef,
|
formRef,
|
||||||
hideSubmitButton = false,
|
hideSubmitButton = false,
|
||||||
|
productId,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
// Form state
|
// Form state
|
||||||
const [name, setName] = useState(initial?.name || '');
|
const [name, setName] = useState(initial?.name || '');
|
||||||
@@ -225,6 +227,7 @@ export function ProductFormTabbed({
|
|||||||
variations={variations}
|
variations={variations}
|
||||||
setVariations={setVariations}
|
setVariations={setVariations}
|
||||||
regularPrice={regularPrice}
|
regularPrice={regularPrice}
|
||||||
|
productId={productId}
|
||||||
/>
|
/>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Plus, X, Layers, Image as ImageIcon } from 'lucide-react';
|
import { Plus, X, Layers, Image as ImageIcon, Copy, Check, ExternalLink } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { getStoreCurrency } from '@/lib/currency';
|
import { getStoreCurrency } from '@/lib/currency';
|
||||||
import { openWPMediaImage } from '@/lib/wp-media';
|
import { openWPMediaImage } from '@/lib/wp-media';
|
||||||
@@ -30,6 +30,7 @@ type VariationsTabProps = {
|
|||||||
variations: ProductVariant[];
|
variations: ProductVariant[];
|
||||||
setVariations: (value: ProductVariant[]) => void;
|
setVariations: (value: ProductVariant[]) => void;
|
||||||
regularPrice: string;
|
regularPrice: string;
|
||||||
|
productId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function VariationsTab({
|
export function VariationsTab({
|
||||||
@@ -38,8 +39,33 @@ export function VariationsTab({
|
|||||||
variations,
|
variations,
|
||||||
setVariations,
|
setVariations,
|
||||||
regularPrice,
|
regularPrice,
|
||||||
|
productId,
|
||||||
}: VariationsTabProps) {
|
}: VariationsTabProps) {
|
||||||
const store = getStoreCurrency();
|
const store = getStoreCurrency();
|
||||||
|
const [copiedLink, setCopiedLink] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const siteUrl = window.location.origin;
|
||||||
|
const spaPagePath = '/store';
|
||||||
|
|
||||||
|
const generateLink = (variationId: number, redirect: 'cart' | 'checkout' = 'cart') => {
|
||||||
|
if (!productId) return '';
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set('add-to-cart', productId.toString());
|
||||||
|
params.set('variation_id', variationId.toString());
|
||||||
|
params.set('redirect', redirect);
|
||||||
|
return `${siteUrl}${spaPagePath}?${params.toString()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = async (link: string, label: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(link);
|
||||||
|
setCopiedLink(link);
|
||||||
|
toast.success(`${label} link copied!`);
|
||||||
|
setTimeout(() => setCopiedLink(null), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('Failed to copy link');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const addAttribute = () => {
|
const addAttribute = () => {
|
||||||
setAttributes([...attributes, { name: '', options: [], variation: false }]);
|
setAttributes([...attributes, { name: '', options: [], variation: false }]);
|
||||||
@@ -305,6 +331,45 @@ export function VariationsTab({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Direct Cart Links */}
|
||||||
|
{productId && variation.id && (
|
||||||
|
<div className="mt-4 pt-4 border-t space-y-2">
|
||||||
|
<Label className="text-xs font-medium text-muted-foreground">
|
||||||
|
{__('Direct-to-Cart Links')}
|
||||||
|
</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => copyToClipboard(generateLink(variation.id!, 'cart'), 'Cart')}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{copiedLink === generateLink(variation.id!, 'cart') ? (
|
||||||
|
<Check className="h-3 w-3 mr-1" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3 w-3 mr-1" />
|
||||||
|
)}
|
||||||
|
{__('Copy Cart Link')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => copyToClipboard(generateLink(variation.id!, 'checkout'), 'Checkout')}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{copiedLink === generateLink(variation.id!, 'checkout') ? (
|
||||||
|
<Check className="h-3 w-3 mr-1" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3 w-3 mr-1" />
|
||||||
|
)}
|
||||||
|
{__('Copy Checkout Link')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
darkMode: ["class"],
|
|
||||||
content: ["./src/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"],
|
|
||||||
theme: {
|
|
||||||
container: {
|
|
||||||
center: true,
|
|
||||||
padding: '1rem'
|
|
||||||
},
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
border: 'hsl(var(--border))',
|
|
||||||
input: 'hsl(var(--input))',
|
|
||||||
ring: 'hsl(var(--ring))',
|
|
||||||
background: 'hsl(var(--background))',
|
|
||||||
foreground: 'hsl(var(--foreground))',
|
|
||||||
primary: {
|
|
||||||
DEFAULT: 'hsl(var(--primary))',
|
|
||||||
foreground: 'hsl(var(--primary-foreground))'
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
DEFAULT: 'hsl(var(--secondary))',
|
|
||||||
foreground: 'hsl(var(--secondary-foreground))'
|
|
||||||
},
|
|
||||||
muted: {
|
|
||||||
DEFAULT: 'hsl(var(--muted))',
|
|
||||||
foreground: 'hsl(var(--muted-foreground))'
|
|
||||||
},
|
|
||||||
accent: {
|
|
||||||
DEFAULT: 'hsl(var(--accent))',
|
|
||||||
foreground: 'hsl(var(--accent-foreground))'
|
|
||||||
},
|
|
||||||
popover: {
|
|
||||||
DEFAULT: 'hsl(var(--popover))',
|
|
||||||
foreground: 'hsl(var(--popover-foreground))'
|
|
||||||
},
|
|
||||||
card: {
|
|
||||||
DEFAULT: 'hsl(var(--card))',
|
|
||||||
foreground: 'hsl(var(--card-foreground))'
|
|
||||||
},
|
|
||||||
destructive: {
|
|
||||||
DEFAULT: 'hsl(var(--destructive))',
|
|
||||||
foreground: 'hsl(var(--destructive-foreground))'
|
|
||||||
},
|
|
||||||
chart: {
|
|
||||||
'1': 'hsl(var(--chart-1))',
|
|
||||||
'2': 'hsl(var(--chart-2))',
|
|
||||||
'3': 'hsl(var(--chart-3))',
|
|
||||||
'4': 'hsl(var(--chart-4))',
|
|
||||||
'5': 'hsl(var(--chart-5))'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
borderRadius: {
|
|
||||||
lg: 'var(--radius)',
|
|
||||||
md: 'calc(var(--radius) - 2px)',
|
|
||||||
sm: 'calc(var(--radius) - 4px)'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: [require("tailwindcss-animate")]
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
|
important: '#woonoow-admin-app',
|
||||||
content: ["./src/**/*.{ts,tsx,css}", "./components/**/*.{ts,tsx,css}"],
|
content: ["./src/**/*.{ts,tsx,css}", "./components/**/*.{ts,tsx,css}"],
|
||||||
theme: {
|
theme: {
|
||||||
container: { center: true, padding: "1rem" },
|
container: { center: true, padding: "1rem" },
|
||||||
|
|||||||
20
composer.lock
generated
Normal file
20
composer.lock
generated
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"_readme": [
|
||||||
|
"This file locks the dependencies of your project to a known state",
|
||||||
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
|
"This file is @generated automatically"
|
||||||
|
],
|
||||||
|
"content-hash": "c8dfaf9b12dfc28774a5f4e2e71e84af",
|
||||||
|
"packages": [],
|
||||||
|
"packages-dev": [],
|
||||||
|
"aliases": [],
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"stability-flags": {},
|
||||||
|
"prefer-stable": false,
|
||||||
|
"prefer-lowest": false,
|
||||||
|
"platform": {
|
||||||
|
"php": "^8.1"
|
||||||
|
},
|
||||||
|
"platform-dev": {},
|
||||||
|
"plugin-api-version": "2.9.0"
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import { Toaster } from 'sonner';
|
|||||||
// Theme
|
// Theme
|
||||||
import { ThemeProvider } from './contexts/ThemeContext';
|
import { ThemeProvider } from './contexts/ThemeContext';
|
||||||
import { BaseLayout } from './layouts/BaseLayout';
|
import { BaseLayout } from './layouts/BaseLayout';
|
||||||
import { useAddToCartFromUrl } from './hooks/useAddToCartFromUrl';
|
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
import Shop from './pages/Shop';
|
import Shop from './pages/Shop';
|
||||||
@@ -52,14 +51,6 @@ const getAppearanceSettings = () => {
|
|||||||
return (window as any).woonoowCustomer?.appearanceSettings || {};
|
return (window as any).woonoowCustomer?.appearanceSettings || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
function App() {
|
|
||||||
const themeConfig = getThemeConfig();
|
|
||||||
const appearanceSettings = getAppearanceSettings();
|
|
||||||
const toastPosition = (appearanceSettings?.general?.toast_position || 'top-right') as any;
|
|
||||||
|
|
||||||
// Handle add-to-cart from URL parameters
|
|
||||||
useAddToCartFromUrl();
|
|
||||||
|
|
||||||
// Get initial route from data attribute (set by PHP based on SPA mode)
|
// Get initial route from data attribute (set by PHP based on SPA mode)
|
||||||
const getInitialRoute = () => {
|
const getInitialRoute = () => {
|
||||||
const appEl = document.getElementById('woonoow-customer-app');
|
const appEl = document.getElementById('woonoow-customer-app');
|
||||||
@@ -70,13 +61,12 @@ function App() {
|
|||||||
return initialRoute || '/shop'; // Default to shop if not specified
|
return initialRoute || '/shop'; // Default to shop if not specified
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Router wrapper component that uses hooks requiring Router context
|
||||||
|
function AppRoutes() {
|
||||||
const initialRoute = getInitialRoute();
|
const initialRoute = getInitialRoute();
|
||||||
console.log('[WooNooW Customer] Using initial route:', initialRoute);
|
console.log('[WooNooW Customer] Using initial route:', initialRoute);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<ThemeProvider config={themeConfig}>
|
|
||||||
<HashRouter>
|
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Root route redirects to initial route based on SPA mode */}
|
{/* Root route redirects to initial route based on SPA mode */}
|
||||||
@@ -101,6 +91,19 @@ function App() {
|
|||||||
<Route path="*" element={<Navigate to={initialRoute} replace />} />
|
<Route path="*" element={<Navigate to={initialRoute} replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const themeConfig = getThemeConfig();
|
||||||
|
const appearanceSettings = getAppearanceSettings();
|
||||||
|
const toastPosition = (appearanceSettings?.general?.toast_position || 'top-right') as any;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<ThemeProvider config={themeConfig}>
|
||||||
|
<HashRouter>
|
||||||
|
<AppRoutes />
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
|
|
||||||
{/* Toast notifications - position from settings */}
|
{/* Toast notifications - position from settings */}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { useCartStore } from '@/lib/cart/store';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to handle add-to-cart from URL parameters
|
* Hook to handle add-to-cart from URL parameters
|
||||||
@@ -10,51 +11,81 @@ import { toast } from 'sonner';
|
|||||||
* - Simple product: ?add-to-cart=123
|
* - Simple product: ?add-to-cart=123
|
||||||
* - Variable product: ?add-to-cart=123&variation_id=456
|
* - Variable product: ?add-to-cart=123&variation_id=456
|
||||||
* - With quantity: ?add-to-cart=123&quantity=2
|
* - With quantity: ?add-to-cart=123&quantity=2
|
||||||
|
* - Direct to checkout: ?add-to-cart=123&redirect=checkout
|
||||||
|
* - Stay on cart (default): ?add-to-cart=123&redirect=cart
|
||||||
*/
|
*/
|
||||||
export function useAddToCartFromUrl() {
|
export function useAddToCartFromUrl() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const { setCart } = useCartStore();
|
||||||
|
const processedRef = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(window.location.search);
|
// Check hash route for add-to-cart parameters
|
||||||
const productId = params.get('add-to-cart');
|
const hash = window.location.hash;
|
||||||
|
const hashParams = new URLSearchParams(hash.split('?')[1] || '');
|
||||||
|
const productId = hashParams.get('add-to-cart');
|
||||||
|
|
||||||
if (!productId) return;
|
if (!productId) return;
|
||||||
|
|
||||||
const variationId = params.get('variation_id');
|
const variationId = hashParams.get('variation_id');
|
||||||
const quantity = parseInt(params.get('quantity') || '1', 10);
|
const quantity = parseInt(hashParams.get('quantity') || '1', 10);
|
||||||
|
const redirect = hashParams.get('redirect') || 'cart';
|
||||||
|
|
||||||
|
// Create unique key for this add-to-cart request
|
||||||
|
const requestKey = `${productId}-${variationId || 'none'}-${quantity}`;
|
||||||
|
|
||||||
|
// Skip if already processed
|
||||||
|
if (processedRef.current.has(requestKey)) {
|
||||||
|
console.log('[WooNooW] Skipping duplicate add-to-cart:', requestKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[WooNooW] Add to cart from URL:', {
|
console.log('[WooNooW] Add to cart from URL:', {
|
||||||
productId,
|
productId,
|
||||||
variationId,
|
variationId,
|
||||||
quantity,
|
quantity,
|
||||||
|
redirect,
|
||||||
fullUrl: window.location.href,
|
fullUrl: window.location.href,
|
||||||
|
requestKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add product to cart
|
// Mark as processed
|
||||||
addToCart(productId, variationId, quantity)
|
processedRef.current.add(requestKey);
|
||||||
.then(() => {
|
|
||||||
// Remove URL parameters after adding to cart
|
|
||||||
const cleanUrl = window.location.pathname + window.location.hash;
|
|
||||||
window.history.replaceState({}, '', cleanUrl);
|
|
||||||
|
|
||||||
// Navigate to cart if not already there
|
addToCart(productId, variationId, quantity)
|
||||||
if (!location.pathname.includes('/cart')) {
|
.then((cartData) => {
|
||||||
navigate('/cart');
|
// Update cart store with fresh data from API
|
||||||
|
if (cartData) {
|
||||||
|
setCart(cartData);
|
||||||
|
console.log('[WooNooW] Cart updated with fresh data:', cartData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove URL parameters after adding to cart
|
||||||
|
const currentPath = window.location.hash.split('?')[0];
|
||||||
|
window.location.hash = currentPath;
|
||||||
|
|
||||||
|
// Navigate based on redirect parameter
|
||||||
|
const targetPage = redirect === 'checkout' ? '/checkout' : '/cart';
|
||||||
|
if (!location.pathname.includes(targetPage)) {
|
||||||
|
console.log(`[WooNooW] Navigating to ${targetPage}`);
|
||||||
|
navigate(targetPage);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('[WooNooW] Failed to add product to cart:', error);
|
console.error('[WooNooW] Failed to add product to cart:', error);
|
||||||
toast.error('Failed to add product to cart');
|
toast.error('Failed to add product to cart');
|
||||||
|
// Remove from processed set on error so it can be retried
|
||||||
|
processedRef.current.delete(requestKey);
|
||||||
});
|
});
|
||||||
}, []); // Run once on mount
|
}, [location.hash, navigate, setCart]); // Include all dependencies
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addToCart(
|
async function addToCart(
|
||||||
productId: string,
|
productId: string,
|
||||||
variationId: string | null,
|
variationId: string | null,
|
||||||
quantity: number
|
quantity: number
|
||||||
): Promise<void> {
|
): Promise<any> {
|
||||||
const apiRoot = (window as any).woonoowCustomer?.apiRoot || '/wp-json/woonoow/v1';
|
const apiRoot = (window as any).woonoowCustomer?.apiRoot || '/wp-json/woonoow/v1';
|
||||||
const nonce = (window as any).woonoowCustomer?.nonce || '';
|
const nonce = (window as any).woonoowCustomer?.nonce || '';
|
||||||
|
|
||||||
@@ -85,11 +116,13 @@ async function addToCart(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
console.log('[WooNooW] Product added to cart:', data);
|
||||||
|
|
||||||
if (!data.success) {
|
// API returns {message, cart_item_key, cart} on success
|
||||||
|
if (data.cart_item_key && data.cart) {
|
||||||
|
toast.success(data.message || 'Product added to cart');
|
||||||
|
return data.cart; // Return cart data to update store
|
||||||
|
} else {
|
||||||
throw new Error(data.message || 'Failed to add to cart');
|
throw new Error(data.message || 'Failed to add to cart');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[WooNooW] Product added to cart:', data);
|
|
||||||
toast.success('Product added to cart');
|
|
||||||
}
|
}
|
||||||
|
|||||||
111
customer-spa/src/lib/cart/api.ts
Normal file
111
customer-spa/src/lib/cart/api.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { Cart } from './store';
|
||||||
|
|
||||||
|
const getApiConfig = () => {
|
||||||
|
const apiRoot = (window as any).woonoowCustomer?.apiRoot || '/wp-json/woonoow/v1';
|
||||||
|
const nonce = (window as any).woonoowCustomer?.nonce || '';
|
||||||
|
return { apiRoot, nonce };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update cart item quantity via API
|
||||||
|
*/
|
||||||
|
export async function updateCartItemQuantity(
|
||||||
|
cartItemKey: string,
|
||||||
|
quantity: number
|
||||||
|
): Promise<Cart> {
|
||||||
|
const { apiRoot, nonce } = getApiConfig();
|
||||||
|
|
||||||
|
const response = await fetch(`${apiRoot}/cart/update`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-WP-Nonce': nonce,
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({
|
||||||
|
cart_item_key: cartItemKey,
|
||||||
|
quantity,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.message || 'Failed to update cart');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.cart;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove item from cart via API
|
||||||
|
*/
|
||||||
|
export async function removeCartItem(cartItemKey: string): Promise<Cart> {
|
||||||
|
const { apiRoot, nonce } = getApiConfig();
|
||||||
|
|
||||||
|
const response = await fetch(`${apiRoot}/cart/remove`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-WP-Nonce': nonce,
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({
|
||||||
|
cart_item_key: cartItemKey,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.message || 'Failed to remove item');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.cart;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear entire cart via API
|
||||||
|
*/
|
||||||
|
export async function clearCartAPI(): Promise<Cart> {
|
||||||
|
const { apiRoot, nonce } = getApiConfig();
|
||||||
|
|
||||||
|
const response = await fetch(`${apiRoot}/cart/clear`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-WP-Nonce': nonce,
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.message || 'Failed to clear cart');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.cart;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch current cart from API
|
||||||
|
*/
|
||||||
|
export async function fetchCart(): Promise<Cart> {
|
||||||
|
const { apiRoot, nonce } = getApiConfig();
|
||||||
|
|
||||||
|
const response = await fetch(`${apiRoot}/cart`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'X-WP-Nonce': nonce,
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch cart');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useCartStore, type CartItem } from '@/lib/cart/store';
|
import { useCartStore, type CartItem } from '@/lib/cart/store';
|
||||||
import { useCartSettings } from '@/hooks/useAppearanceSettings';
|
import { useCartSettings } from '@/hooks/useAppearanceSettings';
|
||||||
|
import { updateCartItemQuantity, removeCartItem, clearCartAPI, fetchCart } from '@/lib/cart/api';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -13,37 +14,96 @@ import {
|
|||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import Container from '@/components/Layout/Container';
|
import Container from '@/components/Layout/Container';
|
||||||
import { formatPrice } from '@/lib/currency';
|
import { formatPrice } from '@/lib/currency';
|
||||||
import { Trash2, Plus, Minus, ShoppingBag, ArrowLeft } from 'lucide-react';
|
import { Trash2, Plus, Minus, ShoppingBag, ArrowLeft, Loader2 } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
export default function Cart() {
|
export default function Cart() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { cart, removeItem, updateQuantity, clearCart } = useCartStore();
|
const { cart, setCart } = useCartStore();
|
||||||
const { layout, elements } = useCartSettings();
|
const { layout, elements } = useCartSettings();
|
||||||
const [showClearDialog, setShowClearDialog] = useState(false);
|
const [showClearDialog, setShowClearDialog] = useState(false);
|
||||||
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
// Fetch cart from server on mount to sync with WooCommerce
|
||||||
|
useEffect(() => {
|
||||||
|
const loadCart = async () => {
|
||||||
|
try {
|
||||||
|
const serverCart = await fetchCart();
|
||||||
|
setCart(serverCart);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch cart:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadCart();
|
||||||
|
}, [setCart]);
|
||||||
|
|
||||||
// Calculate total from items
|
// Calculate total from items
|
||||||
const total = cart.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
const total = cart.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||||
|
|
||||||
const handleUpdateQuantity = (key: string, newQuantity: number) => {
|
const handleUpdateQuantity = async (key: string, newQuantity: number) => {
|
||||||
if (newQuantity < 1) {
|
if (newQuantity < 1) {
|
||||||
handleRemoveItem(key);
|
handleRemoveItem(key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateQuantity(key, newQuantity);
|
|
||||||
|
setIsUpdating(true);
|
||||||
|
try {
|
||||||
|
const updatedCart = await updateCartItemQuantity(key, newQuantity);
|
||||||
|
setCart(updatedCart);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update quantity:', error);
|
||||||
|
toast.error('Failed to update quantity');
|
||||||
|
} finally {
|
||||||
|
setIsUpdating(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveItem = (key: string) => {
|
const handleRemoveItem = async (key: string) => {
|
||||||
removeItem(key);
|
setIsUpdating(true);
|
||||||
|
try {
|
||||||
|
const updatedCart = await removeCartItem(key);
|
||||||
|
setCart(updatedCart);
|
||||||
toast.success('Item removed from cart');
|
toast.success('Item removed from cart');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to remove item:', error);
|
||||||
|
toast.error('Failed to remove item');
|
||||||
|
} finally {
|
||||||
|
setIsUpdating(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClearCart = () => {
|
const handleClearCart = async () => {
|
||||||
clearCart();
|
setIsUpdating(true);
|
||||||
|
try {
|
||||||
|
const updatedCart = await clearCartAPI();
|
||||||
|
setCart(updatedCart);
|
||||||
setShowClearDialog(false);
|
setShowClearDialog(false);
|
||||||
toast.success('Cart cleared');
|
toast.success('Cart cleared');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clear cart:', error);
|
||||||
|
toast.error('Failed to clear cart');
|
||||||
|
setShowClearDialog(false);
|
||||||
|
} finally {
|
||||||
|
setIsUpdating(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Show loading state while fetching cart
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<div className="text-center py-16">
|
||||||
|
<Loader2 className="mx-auto h-16 w-16 text-gray-400 mb-4 animate-spin" />
|
||||||
|
<p className="text-gray-600">Loading cart...</p>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (cart.items.length === 0) {
|
if (cart.items.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ use WooNooW\Compat\AddonRegistry;
|
|||||||
use WooNooW\Compat\RouteRegistry;
|
use WooNooW\Compat\RouteRegistry;
|
||||||
use WooNooW\Compat\NavigationRegistry;
|
use WooNooW\Compat\NavigationRegistry;
|
||||||
|
|
||||||
class Assets {
|
class Assets
|
||||||
public static function init() {
|
{
|
||||||
|
public static function init()
|
||||||
|
{
|
||||||
add_action('admin_enqueue_scripts', [__CLASS__, 'enqueue']);
|
add_action('admin_enqueue_scripts', [__CLASS__, 'enqueue']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function enqueue($hook) {
|
public static function enqueue($hook)
|
||||||
|
{
|
||||||
// Debug logging
|
// Debug logging
|
||||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||||
error_log('[WooNooW Assets] Hook: ' . $hook);
|
error_log('[WooNooW Assets] Hook: ' . $hook);
|
||||||
@@ -42,7 +45,8 @@ class Assets {
|
|||||||
/** ----------------------------------------
|
/** ----------------------------------------
|
||||||
* DEV MODE (Vite dev server)
|
* DEV MODE (Vite dev server)
|
||||||
* -------------------------------------- */
|
* -------------------------------------- */
|
||||||
private static function enqueue_dev(): void {
|
private static function enqueue_dev(): void
|
||||||
|
{
|
||||||
$dev_url = self::dev_server_url(); // e.g. http://localhost:5173
|
$dev_url = self::dev_server_url(); // e.g. http://localhost:5173
|
||||||
|
|
||||||
// 1) Create a small handle to attach config (window.WNW_API)
|
// 1) Create a small handle to attach config (window.WNW_API)
|
||||||
@@ -136,7 +140,8 @@ class Assets {
|
|||||||
/** ----------------------------------------
|
/** ----------------------------------------
|
||||||
* PROD MODE (built assets in admin-spa/dist)
|
* PROD MODE (built assets in admin-spa/dist)
|
||||||
* -------------------------------------- */
|
* -------------------------------------- */
|
||||||
private static function enqueue_prod(): void {
|
private static function enqueue_prod(): void
|
||||||
|
{
|
||||||
// Get plugin root directory (2 levels up from includes/Admin/)
|
// Get plugin root directory (2 levels up from includes/Admin/)
|
||||||
$plugin_dir = dirname(dirname(__DIR__));
|
$plugin_dir = dirname(dirname(__DIR__));
|
||||||
$dist_dir = $plugin_dir . '/admin-spa/dist/';
|
$dist_dir = $plugin_dir . '/admin-spa/dist/';
|
||||||
@@ -159,19 +164,7 @@ class Assets {
|
|||||||
|
|
||||||
if (file_exists($dist_dir . $css)) {
|
if (file_exists($dist_dir . $css)) {
|
||||||
wp_enqueue_style('wnw-admin', $base_url . $css, [], $ver_css);
|
wp_enqueue_style('wnw-admin', $base_url . $css, [], $ver_css);
|
||||||
|
// Note: Icon fixes are now in index.css with proper specificity
|
||||||
// Fix icon rendering in WP-Admin (prevent WordPress admin styles from overriding)
|
|
||||||
$icon_fix_css = '
|
|
||||||
/* Fix Lucide icons in WP-Admin - force outlined style */
|
|
||||||
#woonoow-admin-app svg {
|
|
||||||
fill: none !important;
|
|
||||||
stroke: currentColor !important;
|
|
||||||
stroke-width: 2 !important;
|
|
||||||
stroke-linecap: round !important;
|
|
||||||
stroke-linejoin: round !important;
|
|
||||||
}
|
|
||||||
';
|
|
||||||
wp_add_inline_style('wnw-admin', $icon_fix_css);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file_exists($dist_dir . $js)) {
|
if (file_exists($dist_dir . $js)) {
|
||||||
@@ -190,7 +183,8 @@ class Assets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Attach runtime config to a handle */
|
/** Attach runtime config to a handle */
|
||||||
private static function localize_runtime(string $handle): void {
|
private static function localize_runtime(string $handle): void
|
||||||
|
{
|
||||||
wp_localize_script($handle, 'WNW_API', [
|
wp_localize_script($handle, 'WNW_API', [
|
||||||
'root' => untrailingslashit(esc_url_raw(rest_url('woonoow/v1'))),
|
'root' => untrailingslashit(esc_url_raw(rest_url('woonoow/v1'))),
|
||||||
'nonce' => wp_create_nonce('wp_rest'),
|
'nonce' => wp_create_nonce('wp_rest'),
|
||||||
@@ -249,7 +243,8 @@ class Assets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Runtime store meta for frontend (currency, decimals, separators, position). */
|
/** Runtime store meta for frontend (currency, decimals, separators, position). */
|
||||||
private static function store_runtime(): array {
|
private static function store_runtime(): array
|
||||||
|
{
|
||||||
// WooCommerce helpers may not exist in some contexts; guard with defaults
|
// WooCommerce helpers may not exist in some contexts; guard with defaults
|
||||||
$currency = function_exists('get_woocommerce_currency') ? get_woocommerce_currency() : 'USD';
|
$currency = function_exists('get_woocommerce_currency') ? get_woocommerce_currency() : 'USD';
|
||||||
$currency_sym = function_exists('get_woocommerce_currency_symbol') ? get_woocommerce_currency_symbol($currency) : '$';
|
$currency_sym = function_exists('get_woocommerce_currency_symbol') ? get_woocommerce_currency_symbol($currency) : '$';
|
||||||
@@ -275,7 +270,8 @@ class Assets {
|
|||||||
* Note: We don't check WP_ENV to avoid accidentally enabling dev mode
|
* Note: We don't check WP_ENV to avoid accidentally enabling dev mode
|
||||||
* in Local by Flywheel or other local dev environments.
|
* in Local by Flywheel or other local dev environments.
|
||||||
*/
|
*/
|
||||||
private static function is_dev_mode(): bool {
|
private static function is_dev_mode(): bool
|
||||||
|
{
|
||||||
// Only enable dev mode if explicitly set via constant
|
// Only enable dev mode if explicitly set via constant
|
||||||
$const_dev = defined('WOONOOW_ADMIN_DEV') && WOONOOW_ADMIN_DEV === true;
|
$const_dev = defined('WOONOOW_ADMIN_DEV') && WOONOOW_ADMIN_DEV === true;
|
||||||
|
|
||||||
@@ -297,7 +293,8 @@ class Assets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Dev server URL (filterable) */
|
/** Dev server URL (filterable) */
|
||||||
private static function dev_server_url(): string {
|
private static function dev_server_url(): string
|
||||||
|
{
|
||||||
// Auto-detect based on current host (for Local by Flywheel compatibility)
|
// Auto-detect based on current host (for Local by Flywheel compatibility)
|
||||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||||
$protocol = is_ssl() ? 'https' : 'http';
|
$protocol = is_ssl() ? 'https' : 'http';
|
||||||
@@ -314,7 +311,8 @@ class Assets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Basic asset versioning */
|
/** Basic asset versioning */
|
||||||
private static function asset_version(): string {
|
private static function asset_version(): string
|
||||||
|
{
|
||||||
// Bump when releasing; in dev we don't cache-bust
|
// Bump when releasing; in dev we don't cache-bust
|
||||||
return defined('WOONOOW_VERSION') ? WOONOOW_VERSION : '0.1.0';
|
return defined('WOONOOW_VERSION') ? WOONOOW_VERSION : '0.1.0';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,11 +38,6 @@ class Permissions {
|
|||||||
$has_wc = current_user_can('manage_woocommerce');
|
$has_wc = current_user_can('manage_woocommerce');
|
||||||
$has_opts = current_user_can('manage_options');
|
$has_opts = current_user_can('manage_options');
|
||||||
$result = $has_wc || $has_opts;
|
$result = $has_wc || $has_opts;
|
||||||
error_log(sprintf('WooNooW Permissions: check_admin_permission() - WC:%s Options:%s Result:%s',
|
|
||||||
$has_wc ? 'YES' : 'NO',
|
|
||||||
$has_opts ? 'YES' : 'NO',
|
|
||||||
$result ? 'ALLOWED' : 'DENIED'
|
|
||||||
));
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -447,6 +447,7 @@ class ProductsController {
|
|||||||
if (isset($data['description'])) $product->set_description(self::sanitize_textarea($data['description']));
|
if (isset($data['description'])) $product->set_description(self::sanitize_textarea($data['description']));
|
||||||
if (isset($data['short_description'])) $product->set_short_description(self::sanitize_textarea($data['short_description']));
|
if (isset($data['short_description'])) $product->set_short_description(self::sanitize_textarea($data['short_description']));
|
||||||
if (isset($data['sku'])) $product->set_sku(self::sanitize_text($data['sku']));
|
if (isset($data['sku'])) $product->set_sku(self::sanitize_text($data['sku']));
|
||||||
|
|
||||||
if (isset($data['regular_price'])) $product->set_regular_price(self::sanitize_number($data['regular_price']));
|
if (isset($data['regular_price'])) $product->set_regular_price(self::sanitize_number($data['regular_price']));
|
||||||
if (isset($data['sale_price'])) $product->set_sale_price(self::sanitize_number($data['sale_price']));
|
if (isset($data['sale_price'])) $product->set_sale_price(self::sanitize_number($data['sale_price']));
|
||||||
|
|
||||||
@@ -800,16 +801,19 @@ class ProductsController {
|
|||||||
$value = $term ? $term->name : $value;
|
$value = $term ? $term->name : $value;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Custom attribute - WooCommerce stores as 'attribute_' + exact attribute name
|
// Custom attribute - stored as lowercase in meta
|
||||||
$meta_key = 'attribute_' . $attr_name;
|
$meta_key = 'attribute_' . strtolower($attr_name);
|
||||||
$value = get_post_meta($variation_id, $meta_key, true);
|
$value = get_post_meta($variation_id, $meta_key, true);
|
||||||
|
|
||||||
// Capitalize the attribute name for display
|
// Capitalize the attribute name for display to match admin SPA
|
||||||
$clean_name = ucfirst($attr_name);
|
$clean_name = ucfirst($attr_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only add if value exists
|
||||||
|
if (!empty($value)) {
|
||||||
$formatted_attributes[$clean_name] = $value;
|
$formatted_attributes[$clean_name] = $value;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$image_url = $image ? $image[0] : '';
|
$image_url = $image ? $image[0] : '';
|
||||||
if (!$image_url && $variation->get_image_id()) {
|
if (!$image_url && $variation->get_image_id()) {
|
||||||
@@ -857,36 +861,106 @@ class ProductsController {
|
|||||||
* Save product variations
|
* Save product variations
|
||||||
*/
|
*/
|
||||||
private static function save_product_variations($product, $variations_data) {
|
private static function save_product_variations($product, $variations_data) {
|
||||||
|
// Get existing variation IDs
|
||||||
|
$existing_variation_ids = $product->get_children();
|
||||||
|
$variations_to_keep = [];
|
||||||
|
|
||||||
foreach ($variations_data as $var_data) {
|
foreach ($variations_data as $var_data) {
|
||||||
if (isset($var_data['id']) && $var_data['id']) {
|
if (isset($var_data['id']) && $var_data['id']) {
|
||||||
// Update existing variation
|
|
||||||
$variation = wc_get_product($var_data['id']);
|
$variation = wc_get_product($var_data['id']);
|
||||||
|
if (!$variation) continue;
|
||||||
|
$variations_to_keep[] = $var_data['id'];
|
||||||
} else {
|
} else {
|
||||||
// Create new variation
|
|
||||||
$variation = new WC_Product_Variation();
|
$variation = new WC_Product_Variation();
|
||||||
$variation->set_parent_id($product->get_id());
|
$variation->set_parent_id($product->get_id());
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($variation) {
|
// Build attributes array
|
||||||
|
$wc_attributes = [];
|
||||||
|
if (isset($var_data['attributes']) && is_array($var_data['attributes'])) {
|
||||||
|
$parent_attributes = $product->get_attributes();
|
||||||
|
|
||||||
|
foreach ($var_data['attributes'] as $display_name => $value) {
|
||||||
|
if (empty($value)) continue;
|
||||||
|
|
||||||
|
foreach ($parent_attributes as $attr_name => $parent_attr) {
|
||||||
|
if (!$parent_attr->get_variation()) continue;
|
||||||
|
if (strcasecmp($display_name, $attr_name) === 0 || strcasecmp($display_name, ucfirst($attr_name)) === 0) {
|
||||||
|
$wc_attributes[strtolower($attr_name)] = strtolower($value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($wc_attributes)) {
|
||||||
|
$variation->set_attributes($wc_attributes);
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($var_data['sku'])) $variation->set_sku($var_data['sku']);
|
if (isset($var_data['sku'])) $variation->set_sku($var_data['sku']);
|
||||||
if (isset($var_data['regular_price'])) $variation->set_regular_price($var_data['regular_price']);
|
|
||||||
if (isset($var_data['sale_price'])) $variation->set_sale_price($var_data['sale_price']);
|
// Set prices - if not provided, use parent's price as fallback
|
||||||
|
if (isset($var_data['regular_price']) && $var_data['regular_price'] !== '') {
|
||||||
|
$variation->set_regular_price($var_data['regular_price']);
|
||||||
|
} elseif (!$variation->get_regular_price()) {
|
||||||
|
// Fallback to parent price if variation has no price
|
||||||
|
$parent_price = $product->get_regular_price();
|
||||||
|
if ($parent_price) {
|
||||||
|
$variation->set_regular_price($parent_price);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($var_data['sale_price']) && $var_data['sale_price'] !== '') {
|
||||||
|
$variation->set_sale_price($var_data['sale_price']);
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($var_data['stock_status'])) $variation->set_stock_status($var_data['stock_status']);
|
if (isset($var_data['stock_status'])) $variation->set_stock_status($var_data['stock_status']);
|
||||||
if (isset($var_data['manage_stock'])) $variation->set_manage_stock($var_data['manage_stock']);
|
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['stock_quantity'])) $variation->set_stock_quantity($var_data['stock_quantity']);
|
||||||
if (isset($var_data['attributes'])) $variation->set_attributes($var_data['attributes']);
|
|
||||||
|
|
||||||
// Handle image - support both image_id and image URL
|
|
||||||
if (isset($var_data['image']) && !empty($var_data['image'])) {
|
if (isset($var_data['image']) && !empty($var_data['image'])) {
|
||||||
$image_id = attachment_url_to_postid($var_data['image']);
|
$image_id = attachment_url_to_postid($var_data['image']);
|
||||||
if ($image_id) {
|
if ($image_id) $variation->set_image_id($image_id);
|
||||||
$variation->set_image_id($image_id);
|
|
||||||
}
|
|
||||||
} elseif (isset($var_data['image_id'])) {
|
} elseif (isset($var_data['image_id'])) {
|
||||||
$variation->set_image_id($var_data['image_id']);
|
$variation->set_image_id($var_data['image_id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$variation->save();
|
// Save variation first
|
||||||
|
$saved_id = $variation->save();
|
||||||
|
$variations_to_keep[] = $saved_id;
|
||||||
|
|
||||||
|
// Manually save attributes using direct database insert
|
||||||
|
if (!empty($wc_attributes)) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
foreach ($wc_attributes as $attr_name => $attr_value) {
|
||||||
|
$meta_key = 'attribute_' . $attr_name;
|
||||||
|
|
||||||
|
$wpdb->delete(
|
||||||
|
$wpdb->postmeta,
|
||||||
|
['post_id' => $saved_id, 'meta_key' => $meta_key],
|
||||||
|
['%d', '%s']
|
||||||
|
);
|
||||||
|
|
||||||
|
$wpdb->insert(
|
||||||
|
$wpdb->postmeta,
|
||||||
|
[
|
||||||
|
'post_id' => $saved_id,
|
||||||
|
'meta_key' => $meta_key,
|
||||||
|
'meta_value' => $attr_value
|
||||||
|
],
|
||||||
|
['%d', '%s', '%s']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete variations that are no longer in the list
|
||||||
|
$variations_to_delete = array_diff($existing_variation_ids, $variations_to_keep);
|
||||||
|
foreach ($variations_to_delete as $variation_id) {
|
||||||
|
$variation_to_delete = wc_get_product($variation_id);
|
||||||
|
if ($variation_to_delete) {
|
||||||
|
$variation_to_delete->delete(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,5 +66,64 @@ class Bootstrap {
|
|||||||
MailQueue::init();
|
MailQueue::init();
|
||||||
WooEmailOverride::init();
|
WooEmailOverride::init();
|
||||||
OrderStore::init();
|
OrderStore::init();
|
||||||
|
|
||||||
|
// Initialize cart for REST API requests
|
||||||
|
add_action('woocommerce_init', [self::class, 'init_cart_for_rest_api']);
|
||||||
|
|
||||||
|
// Load custom variation attributes for WooCommerce admin
|
||||||
|
add_action('woocommerce_product_variation_object_read', [self::class, 'load_variation_attributes']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properly initialize WooCommerce cart for REST API requests
|
||||||
|
* This is the recommended approach per WooCommerce core team
|
||||||
|
*/
|
||||||
|
public static function init_cart_for_rest_api() {
|
||||||
|
// Only load cart for REST API requests
|
||||||
|
if (!WC()->is_rest_api_request()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load frontend includes (required for cart)
|
||||||
|
WC()->frontend_includes();
|
||||||
|
|
||||||
|
// Load cart using WooCommerce's official method
|
||||||
|
if (null === WC()->cart && function_exists('wc_load_cart')) {
|
||||||
|
wc_load_cart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load custom variation attributes from post meta for WooCommerce admin
|
||||||
|
* This ensures WooCommerce's native admin displays custom attributes correctly
|
||||||
|
*/
|
||||||
|
public static function load_variation_attributes($variation) {
|
||||||
|
if (!$variation instanceof \WC_Product_Variation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parent = wc_get_product($variation->get_parent_id());
|
||||||
|
if (!$parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attributes = [];
|
||||||
|
foreach ($parent->get_attributes() as $attr_name => $attribute) {
|
||||||
|
if (!$attribute->get_variation()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from post meta (stored as lowercase)
|
||||||
|
$meta_key = 'attribute_' . strtolower($attr_name);
|
||||||
|
$value = get_post_meta($variation->get_id(), $meta_key, true);
|
||||||
|
|
||||||
|
if (!empty($value)) {
|
||||||
|
$attributes[strtolower($attr_name)] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($attributes)) {
|
||||||
|
$variation->set_attributes($attributes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,15 +35,11 @@ class Assets {
|
|||||||
public static function enqueue_assets() {
|
public static function enqueue_assets() {
|
||||||
// Only load on pages with WooNooW shortcodes or in full SPA mode
|
// Only load on pages with WooNooW shortcodes or in full SPA mode
|
||||||
if (!self::should_load_assets()) {
|
if (!self::should_load_assets()) {
|
||||||
error_log('[WooNooW Customer] should_load_assets returned false - not loading');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
error_log('[WooNooW Customer] should_load_assets returned true - loading assets');
|
|
||||||
|
|
||||||
// Check if dev mode is enabled
|
// Check if dev mode is enabled
|
||||||
$is_dev = defined('WOONOOW_CUSTOMER_DEV') && WOONOOW_CUSTOMER_DEV;
|
$is_dev = defined('WOONOOW_CUSTOMER_DEV') && WOONOOW_CUSTOMER_DEV;
|
||||||
error_log('[WooNooW Customer] Dev mode: ' . ($is_dev ? 'true' : 'false'));
|
|
||||||
|
|
||||||
if ($is_dev) {
|
if ($is_dev) {
|
||||||
// Dev mode: Load from Vite dev server
|
// Dev mode: Load from Vite dev server
|
||||||
@@ -66,9 +62,6 @@ class Assets {
|
|||||||
null,
|
null,
|
||||||
false // Load in header
|
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 {
|
} else {
|
||||||
// Production mode: Load from build
|
// Production mode: Load from build
|
||||||
$plugin_url = plugin_dir_url(dirname(dirname(__FILE__)));
|
$plugin_url = plugin_dir_url(dirname(dirname(__FILE__)));
|
||||||
@@ -76,7 +69,6 @@ class Assets {
|
|||||||
|
|
||||||
// Check if build exists
|
// Check if build exists
|
||||||
if (!file_exists($dist_path)) {
|
if (!file_exists($dist_path)) {
|
||||||
error_log('WooNooW: customer-spa build not found. Run: cd customer-spa && npm run build');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,9 +76,6 @@ class Assets {
|
|||||||
$js_url = $plugin_url . 'customer-spa/dist/app.js';
|
$js_url = $plugin_url . 'customer-spa/dist/app.js';
|
||||||
$css_url = $plugin_url . 'customer-spa/dist/app.css';
|
$css_url = $plugin_url . 'customer-spa/dist/app.css';
|
||||||
|
|
||||||
error_log('[WooNooW Customer] Enqueuing JS: ' . $js_url);
|
|
||||||
error_log('[WooNooW Customer] Enqueuing CSS: ' . $css_url);
|
|
||||||
|
|
||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
'woonoow-customer-spa',
|
'woonoow-customer-spa',
|
||||||
$js_url,
|
$js_url,
|
||||||
@@ -109,8 +98,6 @@ class Assets {
|
|||||||
[],
|
[],
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
error_log('[WooNooW Customer] Assets enqueued successfully');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +229,6 @@ class Assets {
|
|||||||
<script type="module" crossorigin src="<?php echo $dev_server; ?>/@vite/client"></script>
|
<script type="module" crossorigin src="<?php echo $dev_server; ?>/@vite/client"></script>
|
||||||
<script type="module" crossorigin src="<?php echo $dev_server; ?>/src/main.tsx"></script>
|
<script type="module" crossorigin src="<?php echo $dev_server; ?>/src/main.tsx"></script>
|
||||||
<?php
|
<?php
|
||||||
error_log('WooNooW Customer: Scripts output directly in head with React Refresh preamble');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,11 +238,8 @@ class Assets {
|
|||||||
private static function should_load_assets() {
|
private static function should_load_assets() {
|
||||||
global $post;
|
global $post;
|
||||||
|
|
||||||
error_log('[WooNooW Customer] should_load_assets check - Post ID: ' . ($post ? $post->ID : 'none'));
|
|
||||||
|
|
||||||
// First check: Is this a designated SPA page?
|
// First check: Is this a designated SPA page?
|
||||||
if (self::is_spa_page()) {
|
if (self::is_spa_page()) {
|
||||||
error_log('[WooNooW Customer] Designated SPA page detected - loading assets');
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,8 +247,6 @@ class Assets {
|
|||||||
$spa_settings = get_option('woonoow_customer_spa_settings', []);
|
$spa_settings = get_option('woonoow_customer_spa_settings', []);
|
||||||
$mode = isset($spa_settings['mode']) ? $spa_settings['mode'] : 'disabled';
|
$mode = isset($spa_settings['mode']) ? $spa_settings['mode'] : 'disabled';
|
||||||
|
|
||||||
error_log('[WooNooW Customer] SPA mode: ' . $mode);
|
|
||||||
|
|
||||||
// If disabled, don't load
|
// If disabled, don't load
|
||||||
if ($mode === 'disabled') {
|
if ($mode === 'disabled') {
|
||||||
// Special handling for WooCommerce Shop page (it's an archive, not a regular post)
|
// Special handling for WooCommerce Shop page (it's an archive, not a regular post)
|
||||||
@@ -274,7 +255,6 @@ class Assets {
|
|||||||
if ($shop_page_id) {
|
if ($shop_page_id) {
|
||||||
$shop_page = get_post($shop_page_id);
|
$shop_page = get_post($shop_page_id);
|
||||||
if ($shop_page && has_shortcode($shop_page->post_content, 'woonoow_shop')) {
|
if ($shop_page && has_shortcode($shop_page->post_content, 'woonoow_shop')) {
|
||||||
error_log('[WooNooW Customer] Found woonoow_shop shortcode on Shop page (ID: ' . $shop_page_id . ')');
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,27 +262,19 @@ class Assets {
|
|||||||
|
|
||||||
// Check for shortcodes on regular pages
|
// Check for shortcodes on regular pages
|
||||||
if ($post) {
|
if ($post) {
|
||||||
error_log('[WooNooW Customer] Checking post content for shortcodes');
|
|
||||||
error_log('[WooNooW Customer] Post content: ' . substr($post->post_content, 0, 200));
|
|
||||||
|
|
||||||
if (has_shortcode($post->post_content, 'woonoow_shop')) {
|
if (has_shortcode($post->post_content, 'woonoow_shop')) {
|
||||||
error_log('[WooNooW Customer] Found woonoow_shop shortcode');
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (has_shortcode($post->post_content, 'woonoow_cart')) {
|
if (has_shortcode($post->post_content, 'woonoow_cart')) {
|
||||||
error_log('[WooNooW Customer] Found woonoow_cart shortcode');
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (has_shortcode($post->post_content, 'woonoow_checkout')) {
|
if (has_shortcode($post->post_content, 'woonoow_checkout')) {
|
||||||
error_log('[WooNooW Customer] Found woonoow_checkout shortcode');
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (has_shortcode($post->post_content, 'woonoow_account')) {
|
if (has_shortcode($post->post_content, 'woonoow_account')) {
|
||||||
error_log('[WooNooW Customer] Found woonoow_account shortcode');
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
error_log('[WooNooW Customer] No shortcodes found, not loading');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ use WP_Error;
|
|||||||
* Cart Controller - Customer-facing cart API
|
* Cart Controller - Customer-facing cart API
|
||||||
* Handles cart operations for customer-spa
|
* Handles cart operations for customer-spa
|
||||||
*/
|
*/
|
||||||
class CartController {
|
class CartController
|
||||||
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize controller
|
* Initialize controller
|
||||||
*/
|
*/
|
||||||
public static function init() {
|
public static function init()
|
||||||
|
{
|
||||||
// Bypass cookie authentication for cart endpoints to allow guest users
|
// Bypass cookie authentication for cart endpoints to allow guest users
|
||||||
add_filter('rest_authentication_errors', function ($result) {
|
add_filter('rest_authentication_errors', function ($result) {
|
||||||
// If already authenticated or error, return as is
|
// If already authenticated or error, return as is
|
||||||
@@ -35,7 +37,8 @@ class CartController {
|
|||||||
/**
|
/**
|
||||||
* Register REST API routes
|
* Register REST API routes
|
||||||
*/
|
*/
|
||||||
public static function register_routes() {
|
public static function register_routes()
|
||||||
|
{
|
||||||
$namespace = 'woonoow/v1';
|
$namespace = 'woonoow/v1';
|
||||||
|
|
||||||
// Get cart
|
// Get cart
|
||||||
@@ -75,7 +78,8 @@ class CartController {
|
|||||||
register_rest_route($namespace, '/cart/update', [
|
register_rest_route($namespace, '/cart/update', [
|
||||||
'methods' => 'POST',
|
'methods' => 'POST',
|
||||||
'callback' => [__CLASS__, 'update_cart'],
|
'callback' => [__CLASS__, 'update_cart'],
|
||||||
'permission_callback' => function() { return true; },
|
'permission_callback' => function () {
|
||||||
|
return true; },
|
||||||
'args' => [
|
'args' => [
|
||||||
'cart_item_key' => [
|
'cart_item_key' => [
|
||||||
'required' => true,
|
'required' => true,
|
||||||
@@ -92,7 +96,8 @@ class CartController {
|
|||||||
register_rest_route($namespace, '/cart/remove', [
|
register_rest_route($namespace, '/cart/remove', [
|
||||||
'methods' => 'POST',
|
'methods' => 'POST',
|
||||||
'callback' => [__CLASS__, 'remove_from_cart'],
|
'callback' => [__CLASS__, 'remove_from_cart'],
|
||||||
'permission_callback' => function() { return true; },
|
'permission_callback' => function () {
|
||||||
|
return true; },
|
||||||
'args' => [
|
'args' => [
|
||||||
'cart_item_key' => [
|
'cart_item_key' => [
|
||||||
'required' => true,
|
'required' => true,
|
||||||
@@ -105,7 +110,8 @@ class CartController {
|
|||||||
register_rest_route($namespace, '/cart/apply-coupon', [
|
register_rest_route($namespace, '/cart/apply-coupon', [
|
||||||
'methods' => 'POST',
|
'methods' => 'POST',
|
||||||
'callback' => [__CLASS__, 'apply_coupon'],
|
'callback' => [__CLASS__, 'apply_coupon'],
|
||||||
'permission_callback' => function() { return true; },
|
'permission_callback' => function () {
|
||||||
|
return true; },
|
||||||
'args' => [
|
'args' => [
|
||||||
'coupon_code' => [
|
'coupon_code' => [
|
||||||
'required' => true,
|
'required' => true,
|
||||||
@@ -114,15 +120,24 @@ class CartController {
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Clear cart
|
||||||
|
register_rest_route($namespace, '/cart/clear', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => [__CLASS__, 'clear_cart'],
|
||||||
|
'permission_callback' => function () {
|
||||||
|
return true; },
|
||||||
|
]);
|
||||||
|
|
||||||
// Remove coupon
|
// Remove coupon
|
||||||
register_rest_route($namespace, '/cart/remove-coupon', [
|
register_rest_route($namespace, '/cart/remove-coupon', [
|
||||||
'methods' => 'POST',
|
'methods' => 'POST',
|
||||||
'callback' => [__CLASS__, 'remove_coupon'],
|
'callback' => [__CLASS__, 'remove_coupon'],
|
||||||
'permission_callback' => function() { return true; },
|
'permission_callback' => function () {
|
||||||
|
return true; },
|
||||||
'args' => [
|
'args' => [
|
||||||
'coupon_code' => [
|
'coupon_code' => [
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'sanitize_callback' => 'sanitize_text_field',
|
'type' => 'string',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
@@ -131,9 +146,18 @@ class CartController {
|
|||||||
/**
|
/**
|
||||||
* Get cart contents
|
* Get cart contents
|
||||||
*/
|
*/
|
||||||
public static function get_cart(WP_REST_Request $request) {
|
public static function get_cart(WP_REST_Request $request)
|
||||||
|
{
|
||||||
|
// Initialize WooCommerce session and cart for REST API requests
|
||||||
|
if (!WC()->session) {
|
||||||
|
WC()->initialize_session();
|
||||||
|
}
|
||||||
if (!WC()->cart) {
|
if (!WC()->cart) {
|
||||||
return new WP_Error('cart_error', 'Cart not initialized', ['status' => 500]);
|
WC()->initialize_cart();
|
||||||
|
}
|
||||||
|
// Set session cookie for guest users to persist cart
|
||||||
|
if (!WC()->session->has_session()) {
|
||||||
|
WC()->session->set_customer_session_cookie(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WP_REST_Response(self::format_cart(), 200);
|
return new WP_REST_Response(self::format_cart(), 200);
|
||||||
@@ -142,124 +166,90 @@ class CartController {
|
|||||||
/**
|
/**
|
||||||
* Add item to cart
|
* Add item to cart
|
||||||
*/
|
*/
|
||||||
public static function add_to_cart(WP_REST_Request $request) {
|
public static function add_to_cart(WP_REST_Request $request)
|
||||||
|
{
|
||||||
$product_id = $request->get_param('product_id');
|
$product_id = $request->get_param('product_id');
|
||||||
$quantity = $request->get_param('quantity');
|
$quantity = $request->get_param('quantity') ?: 1; // Default to 1
|
||||||
$variation_id = $request->get_param('variation_id');
|
$variation_id = $request->get_param('variation_id');
|
||||||
|
|
||||||
error_log("WooNooW Cart: Adding product {$product_id} (variation: {$variation_id}) qty: {$quantity}");
|
|
||||||
|
|
||||||
// Check if WooCommerce is available
|
|
||||||
if (!function_exists('WC')) {
|
|
||||||
error_log('WooNooW Cart Error: WooCommerce not loaded');
|
|
||||||
return new WP_Error('wc_not_loaded', 'WooCommerce is not loaded', ['status' => 500]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize WooCommerce session and cart for REST API requests
|
// Initialize WooCommerce session and cart for REST API requests
|
||||||
// WooCommerce doesn't auto-initialize these for REST API calls
|
|
||||||
if (!WC()->session) {
|
if (!WC()->session) {
|
||||||
error_log('WooNooW Cart: Initializing WC session for REST API');
|
|
||||||
WC()->initialize_session();
|
WC()->initialize_session();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!WC()->cart) {
|
if (!WC()->cart) {
|
||||||
error_log('WooNooW Cart: Initializing WC cart for REST API');
|
|
||||||
WC()->initialize_cart();
|
WC()->initialize_cart();
|
||||||
}
|
}
|
||||||
|
// CRITICAL: Set session cookie for guest users to persist cart
|
||||||
// Set session cookie for guest users
|
|
||||||
if (!WC()->session->has_session()) {
|
if (!WC()->session->has_session()) {
|
||||||
WC()->session->set_customer_session_cookie(true);
|
WC()->session->set_customer_session_cookie(true);
|
||||||
error_log('WooNooW Cart: Session cookie set for guest user');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
error_log('WooNooW Cart: WC Session and Cart initialized successfully');
|
|
||||||
|
|
||||||
// Validate product
|
// Validate product
|
||||||
$product = wc_get_product($product_id);
|
$product = wc_get_product($product_id);
|
||||||
if (!$product) {
|
if (!$product) {
|
||||||
error_log("WooNooW Cart Error: Product {$product_id} not found");
|
|
||||||
return new WP_Error('invalid_product', 'Product not found', ['status' => 404]);
|
return new WP_Error('invalid_product', 'Product not found', ['status' => 404]);
|
||||||
}
|
}
|
||||||
|
|
||||||
error_log("WooNooW Cart: Product validated - {$product->get_name()} (Type: {$product->get_type()})");
|
// For variable products, get attributes from request or variation
|
||||||
|
|
||||||
// For variable products, validate the variation and get attributes
|
|
||||||
$variation_attributes = [];
|
$variation_attributes = [];
|
||||||
if ($variation_id > 0) {
|
if ($variation_id > 0) {
|
||||||
$variation = wc_get_product($variation_id);
|
$variation = wc_get_product($variation_id);
|
||||||
if (!$variation) {
|
if (!$variation) {
|
||||||
error_log("WooNooW Cart Error: Variation {$variation_id} not found");
|
return new WP_Error('invalid_variation', "Variation not found", ['status' => 404]);
|
||||||
return new WP_Error('invalid_variation', "Variation {$variation_id} not found", ['status' => 404]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($variation->get_parent_id() != $product_id) {
|
if ($variation->get_parent_id() != $product_id) {
|
||||||
error_log("WooNooW Cart Error: Variation {$variation_id} does not belong to product {$product_id}");
|
|
||||||
return new WP_Error('invalid_variation', "Variation does not belong to this product", ['status' => 400]);
|
return new WP_Error('invalid_variation', "Variation does not belong to this product", ['status' => 400]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$variation->is_purchasable() || !$variation->is_in_stock()) {
|
if (!$variation->is_in_stock()) {
|
||||||
error_log("WooNooW Cart Error: Variation {$variation_id} is not purchasable or out of stock");
|
return new WP_Error('variation_not_available', "This variation is out of stock", ['status' => 400]);
|
||||||
return new WP_Error('variation_not_available', "This variation is not available for purchase", ['status' => 400]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get variation attributes from post meta
|
// Build attributes from request parameters (like WooCommerce does)
|
||||||
// WooCommerce stores variation attributes as post meta with 'attribute_' prefix
|
// Check for attribute_* parameters in the request
|
||||||
$variation_attributes = [];
|
$params = $request->get_params();
|
||||||
|
foreach ($params as $key => $value) {
|
||||||
|
if (strpos($key, 'attribute_') === 0) {
|
||||||
|
$variation_attributes[sanitize_title($key)] = wc_clean($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get parent product to know which attributes to look for
|
// If no attributes in request, get from variation meta directly
|
||||||
$parent_product = wc_get_product($product_id);
|
if (empty($variation_attributes)) {
|
||||||
$parent_attributes = $parent_product->get_attributes();
|
$parent = wc_get_product($product_id);
|
||||||
|
foreach ($parent->get_attributes() as $attr_name => $attribute) {
|
||||||
|
if (!$attribute->get_variation())
|
||||||
|
continue;
|
||||||
|
|
||||||
error_log("WooNooW Cart: Parent product attributes: " . print_r(array_keys($parent_attributes), true));
|
$meta_key = 'attribute_' . $attr_name;
|
||||||
|
$value = get_post_meta($variation_id, $meta_key, true);
|
||||||
|
|
||||||
// For each parent attribute, get the value from variation post meta
|
if (!empty($value)) {
|
||||||
foreach ($parent_attributes as $attribute) {
|
$variation_attributes[$meta_key] = $value;
|
||||||
if ($attribute->get_variation()) {
|
|
||||||
$attribute_name = $attribute->get_name();
|
|
||||||
$meta_key = 'attribute_' . $attribute_name;
|
|
||||||
|
|
||||||
// Get the value from post meta
|
|
||||||
$attribute_value = get_post_meta($variation_id, $meta_key, true);
|
|
||||||
|
|
||||||
error_log("WooNooW Cart: Checking attribute {$attribute_name} (meta key: {$meta_key}): {$attribute_value}");
|
|
||||||
|
|
||||||
if (!empty($attribute_value)) {
|
|
||||||
// WooCommerce expects lowercase attribute names
|
|
||||||
$wc_attribute_key = 'attribute_' . strtolower($attribute_name);
|
|
||||||
$variation_attributes[$wc_attribute_key] = $attribute_value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
error_log("WooNooW Cart: Variation validated - {$variation->get_name()}");
|
|
||||||
error_log("WooNooW Cart: Variation attributes extracted: " . print_r($variation_attributes, true));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear any existing notices before adding to cart
|
// Clear any existing notices before adding to cart
|
||||||
wc_clear_notices();
|
wc_clear_notices();
|
||||||
|
|
||||||
// Add to cart with variation attributes
|
// Add to cart with variation attributes
|
||||||
error_log("WooNooW Cart: Calling WC()->cart->add_to_cart({$product_id}, {$quantity}, {$variation_id}, attributes)");
|
|
||||||
$cart_item_key = WC()->cart->add_to_cart($product_id, $quantity, $variation_id, $variation_attributes);
|
$cart_item_key = WC()->cart->add_to_cart($product_id, $quantity, $variation_id, $variation_attributes);
|
||||||
|
|
||||||
if (!$cart_item_key) {
|
if (!$cart_item_key) {
|
||||||
// Get WooCommerce notices to provide better error message
|
|
||||||
$notices = wc_get_notices('error');
|
$notices = wc_get_notices('error');
|
||||||
$error_messages = [];
|
$error_messages = [];
|
||||||
foreach ($notices as $notice) {
|
foreach ($notices as $notice) {
|
||||||
$error_messages[] = is_array($notice) ? $notice['notice'] : $notice;
|
$error_messages[] = is_array($notice) ? $notice['notice'] : $notice;
|
||||||
}
|
}
|
||||||
$error_message = !empty($error_messages) ? implode(', ', $error_messages) : 'Failed to add product to cart';
|
$error_message = !empty($error_messages) ? implode(', ', $error_messages) : 'Failed to add product to cart';
|
||||||
wc_clear_notices(); // Clear notices after reading
|
wc_clear_notices();
|
||||||
|
|
||||||
error_log("WooNooW Cart Error: add_to_cart returned false - {$error_message}");
|
|
||||||
error_log("WooNooW Cart Error: All WC notices: " . print_r($notices, true));
|
|
||||||
return new WP_Error('add_to_cart_failed', $error_message, ['status' => 400]);
|
return new WP_Error('add_to_cart_failed', $error_message, ['status' => 400]);
|
||||||
}
|
}
|
||||||
|
|
||||||
error_log("WooNooW Cart: Product added successfully - Key: {$cart_item_key}");
|
|
||||||
|
|
||||||
return new WP_REST_Response([
|
return new WP_REST_Response([
|
||||||
'message' => 'Product added to cart',
|
'message' => 'Product added to cart',
|
||||||
'cart_item_key' => $cart_item_key,
|
'cart_item_key' => $cart_item_key,
|
||||||
@@ -270,12 +260,20 @@ class CartController {
|
|||||||
/**
|
/**
|
||||||
* Update cart item quantity
|
* Update cart item quantity
|
||||||
*/
|
*/
|
||||||
public static function update_cart(WP_REST_Request $request) {
|
public static function update_cart(WP_REST_Request $request)
|
||||||
|
{
|
||||||
$cart_item_key = $request->get_param('cart_item_key');
|
$cart_item_key = $request->get_param('cart_item_key');
|
||||||
$quantity = $request->get_param('quantity');
|
$quantity = $request->get_param('quantity');
|
||||||
|
|
||||||
|
// Initialize WooCommerce session and cart for REST API requests
|
||||||
|
if (!WC()->session) {
|
||||||
|
WC()->initialize_session();
|
||||||
|
}
|
||||||
if (!WC()->cart) {
|
if (!WC()->cart) {
|
||||||
return new WP_Error('cart_error', 'Cart not initialized', ['status' => 500]);
|
WC()->initialize_cart();
|
||||||
|
}
|
||||||
|
if (!WC()->session->has_session()) {
|
||||||
|
WC()->session->set_customer_session_cookie(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update quantity
|
// Update quantity
|
||||||
@@ -294,11 +292,25 @@ class CartController {
|
|||||||
/**
|
/**
|
||||||
* Remove item from cart
|
* Remove item from cart
|
||||||
*/
|
*/
|
||||||
public static function remove_from_cart(WP_REST_Request $request) {
|
public static function remove_from_cart(WP_REST_Request $request)
|
||||||
|
{
|
||||||
$cart_item_key = $request->get_param('cart_item_key');
|
$cart_item_key = $request->get_param('cart_item_key');
|
||||||
|
|
||||||
|
// Initialize WooCommerce session and cart for REST API requests
|
||||||
|
if (!WC()->session) {
|
||||||
|
WC()->initialize_session();
|
||||||
|
}
|
||||||
if (!WC()->cart) {
|
if (!WC()->cart) {
|
||||||
return new WP_Error('cart_error', 'Cart not initialized', ['status' => 500]);
|
WC()->initialize_cart();
|
||||||
|
}
|
||||||
|
if (!WC()->session->has_session()) {
|
||||||
|
WC()->session->set_customer_session_cookie(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if item exists in cart
|
||||||
|
$cart_contents = WC()->cart->get_cart();
|
||||||
|
if (!isset($cart_contents[$cart_item_key])) {
|
||||||
|
return new WP_Error('item_not_found', "Cart item not found", ['status' => 404]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove item
|
// Remove item
|
||||||
@@ -314,10 +326,36 @@ class CartController {
|
|||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear entire cart
|
||||||
|
*/
|
||||||
|
public static function clear_cart(WP_REST_Request $request)
|
||||||
|
{
|
||||||
|
// Initialize WooCommerce session and cart for REST API requests
|
||||||
|
if (!WC()->session) {
|
||||||
|
WC()->initialize_session();
|
||||||
|
}
|
||||||
|
if (!WC()->cart) {
|
||||||
|
WC()->initialize_cart();
|
||||||
|
}
|
||||||
|
if (!WC()->session->has_session()) {
|
||||||
|
WC()->session->set_customer_session_cookie(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty the cart
|
||||||
|
WC()->cart->empty_cart();
|
||||||
|
|
||||||
|
return new WP_REST_Response([
|
||||||
|
'message' => 'Cart cleared',
|
||||||
|
'cart' => self::format_cart(),
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply coupon to cart
|
* Apply coupon to cart
|
||||||
*/
|
*/
|
||||||
public static function apply_coupon(WP_REST_Request $request) {
|
public static function apply_coupon(WP_REST_Request $request)
|
||||||
|
{
|
||||||
$coupon_code = $request->get_param('coupon_code');
|
$coupon_code = $request->get_param('coupon_code');
|
||||||
|
|
||||||
if (!WC()->cart) {
|
if (!WC()->cart) {
|
||||||
@@ -340,7 +378,8 @@ class CartController {
|
|||||||
/**
|
/**
|
||||||
* Remove coupon from cart
|
* Remove coupon from cart
|
||||||
*/
|
*/
|
||||||
public static function remove_coupon(WP_REST_Request $request) {
|
public static function remove_coupon(WP_REST_Request $request)
|
||||||
|
{
|
||||||
$coupon_code = $request->get_param('coupon_code');
|
$coupon_code = $request->get_param('coupon_code');
|
||||||
|
|
||||||
if (!WC()->cart) {
|
if (!WC()->cart) {
|
||||||
@@ -363,7 +402,8 @@ class CartController {
|
|||||||
/**
|
/**
|
||||||
* Format cart data for API response
|
* Format cart data for API response
|
||||||
*/
|
*/
|
||||||
private static function format_cart() {
|
private static function format_cart()
|
||||||
|
{
|
||||||
$cart = WC()->cart;
|
$cart = WC()->cart;
|
||||||
|
|
||||||
if (!$cart) {
|
if (!$cart) {
|
||||||
@@ -374,6 +414,18 @@ class CartController {
|
|||||||
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
|
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
|
||||||
$product = $cart_item['data'];
|
$product = $cart_item['data'];
|
||||||
|
|
||||||
|
// Format variation attributes with clean names (Size instead of attribute_size)
|
||||||
|
$formatted_attributes = [];
|
||||||
|
if (!empty($cart_item['variation'])) {
|
||||||
|
foreach ($cart_item['variation'] as $attr_key => $attr_value) {
|
||||||
|
// Remove 'attribute_' prefix and capitalize
|
||||||
|
$clean_key = str_replace('attribute_', '', $attr_key);
|
||||||
|
$clean_key = ucfirst($clean_key);
|
||||||
|
// Capitalize value
|
||||||
|
$formatted_attributes[$clean_key] = ucfirst($attr_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$items[] = [
|
$items[] = [
|
||||||
'key' => $cart_item_key,
|
'key' => $cart_item_key,
|
||||||
'product_id' => $cart_item['product_id'],
|
'product_id' => $cart_item['product_id'],
|
||||||
@@ -385,7 +437,7 @@ class CartController {
|
|||||||
'total' => $cart_item['line_total'],
|
'total' => $cart_item['line_total'],
|
||||||
'image' => wp_get_attachment_url($product->get_image_id()),
|
'image' => wp_get_attachment_url($product->get_image_id()),
|
||||||
'permalink' => get_permalink($cart_item['product_id']),
|
'permalink' => get_permalink($cart_item['product_id']),
|
||||||
'attributes' => $cart_item['variation'] ?? [],
|
'attributes' => $formatted_attributes,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,18 @@ namespace WooNooW\Frontend;
|
|||||||
* Template Override
|
* Template Override
|
||||||
* Overrides WooCommerce templates to use WooNooW SPA
|
* Overrides WooCommerce templates to use WooNooW SPA
|
||||||
*/
|
*/
|
||||||
class TemplateOverride {
|
class TemplateOverride
|
||||||
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize
|
* Initialize
|
||||||
*/
|
*/
|
||||||
public static function init() {
|
public static function init()
|
||||||
|
{
|
||||||
|
// Hook to wp_loaded with priority 10 (BEFORE WooCommerce's priority 20)
|
||||||
|
// This ensures we process add-to-cart before WooCommerce does
|
||||||
|
add_action('wp_loaded', [__CLASS__, 'intercept_add_to_cart'], 10);
|
||||||
|
|
||||||
// Use blank template for full-page SPA
|
// Use blank template for full-page SPA
|
||||||
add_filter('template_include', [__CLASS__, 'use_spa_template'], 999);
|
add_filter('template_include', [__CLASS__, 'use_spa_template'], 999);
|
||||||
|
|
||||||
@@ -38,11 +44,61 @@ class TemplateOverride {
|
|||||||
add_action('get_footer', [__CLASS__, 'remove_theme_footer']);
|
add_action('get_footer', [__CLASS__, 'remove_theme_footer']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intercept add-to-cart redirect (NOT the add-to-cart itself)
|
||||||
|
* Let WooCommerce handle the cart operation properly, we just redirect afterward
|
||||||
|
*
|
||||||
|
* This is the proper approach - WooCommerce manages sessions correctly,
|
||||||
|
* we just customize where the redirect goes.
|
||||||
|
*/
|
||||||
|
public static function intercept_add_to_cart()
|
||||||
|
{
|
||||||
|
// Only act if add-to-cart is present
|
||||||
|
if (!isset($_GET['add-to-cart'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get SPA page from appearance settings
|
||||||
|
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
||||||
|
$spa_page_id = isset($appearance_settings['general']['spa_page']) ? $appearance_settings['general']['spa_page'] : 0;
|
||||||
|
|
||||||
|
if (!$spa_page_id) {
|
||||||
|
return; // No SPA page configured, let WooCommerce handle everything
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook into WooCommerce's redirect filter AFTER it adds to cart
|
||||||
|
// This is the proper way to customize the redirect destination
|
||||||
|
add_filter('woocommerce_add_to_cart_redirect', function ($url) use ($spa_page_id) {
|
||||||
|
// Get redirect parameter from original request
|
||||||
|
$redirect_to = isset($_GET['redirect']) ? sanitize_text_field($_GET['redirect']) : 'cart';
|
||||||
|
|
||||||
|
// Build redirect URL with hash route for SPA
|
||||||
|
$redirect_url = get_permalink($spa_page_id);
|
||||||
|
|
||||||
|
// Determine hash route based on redirect parameter
|
||||||
|
$hash_route = '/cart'; // Default
|
||||||
|
if ($redirect_to === 'checkout') {
|
||||||
|
$hash_route = '/checkout';
|
||||||
|
} elseif ($redirect_to === 'shop') {
|
||||||
|
$hash_route = '/shop';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the SPA URL with hash route
|
||||||
|
return trailingslashit($redirect_url) . '#' . $hash_route;
|
||||||
|
}, 999);
|
||||||
|
|
||||||
|
// Prevent caching
|
||||||
|
add_action('template_redirect', function () {
|
||||||
|
nocache_headers();
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable canonical redirects for SPA routes
|
* Disable canonical redirects for SPA routes
|
||||||
* This prevents WordPress from redirecting /product/slug URLs
|
* This prevents WordPress from redirecting /product/slug URLs
|
||||||
*/
|
*/
|
||||||
public static function disable_canonical_redirect($redirect_url, $requested_url) {
|
public static function disable_canonical_redirect($redirect_url, $requested_url)
|
||||||
|
{
|
||||||
$settings = get_option('woonoow_customer_spa_settings', []);
|
$settings = get_option('woonoow_customer_spa_settings', []);
|
||||||
$mode = isset($settings['mode']) ? $settings['mode'] : 'disabled';
|
$mode = isset($settings['mode']) ? $settings['mode'] : 'disabled';
|
||||||
|
|
||||||
@@ -67,7 +123,8 @@ class TemplateOverride {
|
|||||||
/**
|
/**
|
||||||
* Use SPA template (blank page)
|
* Use SPA template (blank page)
|
||||||
*/
|
*/
|
||||||
public static function use_spa_template($template) {
|
public static function use_spa_template($template)
|
||||||
|
{
|
||||||
// Check if current page is a designated SPA page
|
// Check if current page is a designated SPA page
|
||||||
if (self::is_spa_page()) {
|
if (self::is_spa_page()) {
|
||||||
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
|
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
|
||||||
@@ -84,12 +141,14 @@ class TemplateOverride {
|
|||||||
if ($mode === 'disabled') {
|
if ($mode === 'disabled') {
|
||||||
// Check if page has woonoow shortcodes
|
// Check if page has woonoow shortcodes
|
||||||
global $post;
|
global $post;
|
||||||
if ($post && (
|
if (
|
||||||
|
$post && (
|
||||||
has_shortcode($post->post_content, 'woonoow_shop') ||
|
has_shortcode($post->post_content, 'woonoow_shop') ||
|
||||||
has_shortcode($post->post_content, 'woonoow_cart') ||
|
has_shortcode($post->post_content, 'woonoow_cart') ||
|
||||||
has_shortcode($post->post_content, 'woonoow_checkout') ||
|
has_shortcode($post->post_content, 'woonoow_checkout') ||
|
||||||
has_shortcode($post->post_content, 'woonoow_account')
|
has_shortcode($post->post_content, 'woonoow_account')
|
||||||
)) {
|
)
|
||||||
|
) {
|
||||||
// Use blank template for shortcode pages too
|
// Use blank template for shortcode pages too
|
||||||
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
|
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
|
||||||
if (file_exists($spa_template)) {
|
if (file_exists($spa_template)) {
|
||||||
@@ -172,7 +231,8 @@ class TemplateOverride {
|
|||||||
/**
|
/**
|
||||||
* Start SPA wrapper
|
* Start SPA wrapper
|
||||||
*/
|
*/
|
||||||
public static function start_spa_wrapper() {
|
public static function start_spa_wrapper()
|
||||||
|
{
|
||||||
// Check if we should use SPA
|
// Check if we should use SPA
|
||||||
if (!self::should_use_spa()) {
|
if (!self::should_use_spa()) {
|
||||||
return;
|
return;
|
||||||
@@ -211,7 +271,8 @@ class TemplateOverride {
|
|||||||
/**
|
/**
|
||||||
* End SPA wrapper
|
* End SPA wrapper
|
||||||
*/
|
*/
|
||||||
public static function end_spa_wrapper() {
|
public static function end_spa_wrapper()
|
||||||
|
{
|
||||||
if (!self::should_use_spa()) {
|
if (!self::should_use_spa()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -223,7 +284,8 @@ class TemplateOverride {
|
|||||||
/**
|
/**
|
||||||
* Check if we should use SPA
|
* Check if we should use SPA
|
||||||
*/
|
*/
|
||||||
private static function should_use_spa() {
|
private static function should_use_spa()
|
||||||
|
{
|
||||||
// Check if frontend mode is enabled
|
// Check if frontend mode is enabled
|
||||||
$mode = get_option('woonoow_frontend_mode', 'shortcodes');
|
$mode = get_option('woonoow_frontend_mode', 'shortcodes');
|
||||||
|
|
||||||
@@ -247,7 +309,8 @@ class TemplateOverride {
|
|||||||
/**
|
/**
|
||||||
* Remove theme header when SPA is active
|
* Remove theme header when SPA is active
|
||||||
*/
|
*/
|
||||||
public static function remove_theme_header() {
|
public static function remove_theme_header()
|
||||||
|
{
|
||||||
if (self::should_remove_theme_elements()) {
|
if (self::should_remove_theme_elements()) {
|
||||||
remove_all_actions('wp_head');
|
remove_all_actions('wp_head');
|
||||||
// Re-add essential WordPress head actions
|
// Re-add essential WordPress head actions
|
||||||
@@ -262,7 +325,8 @@ class TemplateOverride {
|
|||||||
/**
|
/**
|
||||||
* Remove theme footer when SPA is active
|
* Remove theme footer when SPA is active
|
||||||
*/
|
*/
|
||||||
public static function remove_theme_footer() {
|
public static function remove_theme_footer()
|
||||||
|
{
|
||||||
if (self::should_remove_theme_elements()) {
|
if (self::should_remove_theme_elements()) {
|
||||||
remove_all_actions('wp_footer');
|
remove_all_actions('wp_footer');
|
||||||
// Re-add essential WordPress footer actions
|
// Re-add essential WordPress footer actions
|
||||||
@@ -273,7 +337,8 @@ class TemplateOverride {
|
|||||||
/**
|
/**
|
||||||
* Check if current page is the designated SPA page
|
* Check if current page is the designated SPA page
|
||||||
*/
|
*/
|
||||||
private static function is_spa_page() {
|
private static function is_spa_page()
|
||||||
|
{
|
||||||
global $post;
|
global $post;
|
||||||
if (!$post) {
|
if (!$post) {
|
||||||
return false;
|
return false;
|
||||||
@@ -294,7 +359,8 @@ class TemplateOverride {
|
|||||||
/**
|
/**
|
||||||
* Check if we should remove theme header/footer
|
* Check if we should remove theme header/footer
|
||||||
*/
|
*/
|
||||||
private static function should_remove_theme_elements() {
|
private static function should_remove_theme_elements()
|
||||||
|
{
|
||||||
// Remove for designated SPA pages
|
// Remove for designated SPA pages
|
||||||
if (self::is_spa_page()) {
|
if (self::is_spa_page()) {
|
||||||
return true;
|
return true;
|
||||||
@@ -312,12 +378,14 @@ class TemplateOverride {
|
|||||||
|
|
||||||
// Also remove for pages with shortcodes (even in disabled mode)
|
// Also remove for pages with shortcodes (even in disabled mode)
|
||||||
global $post;
|
global $post;
|
||||||
if ($post && (
|
if (
|
||||||
|
$post && (
|
||||||
has_shortcode($post->post_content, 'woonoow_shop') ||
|
has_shortcode($post->post_content, 'woonoow_shop') ||
|
||||||
has_shortcode($post->post_content, 'woonoow_cart') ||
|
has_shortcode($post->post_content, 'woonoow_cart') ||
|
||||||
has_shortcode($post->post_content, 'woonoow_checkout') ||
|
has_shortcode($post->post_content, 'woonoow_checkout') ||
|
||||||
has_shortcode($post->post_content, 'woonoow_account')
|
has_shortcode($post->post_content, 'woonoow_account')
|
||||||
)) {
|
)
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +406,8 @@ class TemplateOverride {
|
|||||||
/**
|
/**
|
||||||
* Override WooCommerce templates
|
* Override WooCommerce templates
|
||||||
*/
|
*/
|
||||||
public static function override_template($template, $template_name, $template_path) {
|
public static function override_template($template, $template_name, $template_path)
|
||||||
|
{
|
||||||
// Only override if SPA is enabled
|
// Only override if SPA is enabled
|
||||||
if (!self::should_use_spa()) {
|
if (!self::should_use_spa()) {
|
||||||
return $template;
|
return $template;
|
||||||
|
|||||||
@@ -12,21 +12,15 @@
|
|||||||
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
||||||
$spa_mode = isset($appearance_settings['general']['spa_mode']) ? $appearance_settings['general']['spa_mode'] : 'full';
|
$spa_mode = isset($appearance_settings['general']['spa_mode']) ? $appearance_settings['general']['spa_mode'] : 'full';
|
||||||
|
|
||||||
// Debug logging
|
|
||||||
error_log('[WooNooW SPA Template] Settings: ' . print_r($appearance_settings, true));
|
|
||||||
error_log('[WooNooW SPA Template] SPA Mode: ' . $spa_mode);
|
|
||||||
|
|
||||||
// Set initial page based on mode
|
// Set initial page based on mode
|
||||||
if ($spa_mode === 'checkout_only') {
|
if ($spa_mode === 'checkout_only') {
|
||||||
// Checkout Only mode starts at cart
|
// Checkout Only mode starts at cart
|
||||||
$page_type = 'cart';
|
$page_type = 'cart';
|
||||||
$data_attrs = 'data-page="cart" data-initial-route="/cart"';
|
$data_attrs = 'data-page="cart" data-initial-route="/cart"';
|
||||||
error_log('[WooNooW SPA Template] Using CART initial route');
|
|
||||||
} else {
|
} else {
|
||||||
// Full SPA mode starts at shop
|
// Full SPA mode starts at shop
|
||||||
$page_type = 'shop';
|
$page_type = 'shop';
|
||||||
$data_attrs = 'data-page="shop" data-initial-route="/shop"';
|
$data_attrs = 'data-page="shop" data-initial-route="/shop"';
|
||||||
error_log('[WooNooW SPA Template] Using SHOP initial route');
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user