feat: Add Copy Cart/Checkout links and licensing settings to product editor
Copy Cart/Checkout Links: - Added to GeneralTab for simple products (same pattern as variations) - Link generation with add-to-cart and redirect params Licensing Settings: - 'Enable licensing for this product' checkbox in Additional Options - License settings panel: activation limit, duration (days) - State management in ProductFormTabbed - Backend: ProductsController saves/loads licensing meta fields Backend: - _licensing_enabled, _license_activation_limit, _license_duration_days post meta
This commit is contained in:
@@ -36,6 +36,10 @@ export type ProductFormData = {
|
||||
downloads?: DownloadableFile[];
|
||||
download_limit?: string;
|
||||
download_expiry?: string;
|
||||
// Licensing
|
||||
licensing_enabled?: boolean;
|
||||
license_activation_limit?: string;
|
||||
license_duration_days?: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@@ -82,6 +86,9 @@ export function ProductFormTabbed({
|
||||
const [downloads, setDownloads] = useState<DownloadableFile[]>(initial?.downloads || []);
|
||||
const [downloadLimit, setDownloadLimit] = useState(initial?.download_limit || '');
|
||||
const [downloadExpiry, setDownloadExpiry] = useState(initial?.download_expiry || '');
|
||||
const [licensingEnabled, setLicensingEnabled] = useState(initial?.licensing_enabled || false);
|
||||
const [licenseActivationLimit, setLicenseActivationLimit] = useState(initial?.license_activation_limit || '');
|
||||
const [licenseDurationDays, setLicenseDurationDays] = useState(initial?.license_duration_days || '');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// Update form state when initial data changes (for edit mode)
|
||||
@@ -109,6 +116,9 @@ export function ProductFormTabbed({
|
||||
setDownloads(initial.downloads || []);
|
||||
setDownloadLimit(initial.download_limit || '');
|
||||
setDownloadExpiry(initial.download_expiry || '');
|
||||
setLicensingEnabled(initial.licensing_enabled || false);
|
||||
setLicenseActivationLimit(initial.license_activation_limit || '');
|
||||
setLicenseDurationDays(initial.license_duration_days || '');
|
||||
}
|
||||
}, [initial, mode]);
|
||||
|
||||
@@ -168,6 +178,9 @@ export function ProductFormTabbed({
|
||||
downloads: downloadable ? downloads : undefined,
|
||||
download_limit: downloadable ? downloadLimit : undefined,
|
||||
download_expiry: downloadable ? downloadExpiry : undefined,
|
||||
licensing_enabled: licensingEnabled,
|
||||
license_activation_limit: licensingEnabled ? licenseActivationLimit : undefined,
|
||||
license_duration_days: licensingEnabled ? licenseDurationDays : undefined,
|
||||
};
|
||||
|
||||
await onSubmit(payload);
|
||||
@@ -217,6 +230,13 @@ export function ProductFormTabbed({
|
||||
setRegularPrice={setRegularPrice}
|
||||
salePrice={salePrice}
|
||||
setSalePrice={setSalePrice}
|
||||
productId={productId}
|
||||
licensingEnabled={licensingEnabled}
|
||||
setLicensingEnabled={setLicensingEnabled}
|
||||
licenseActivationLimit={licenseActivationLimit}
|
||||
setLicenseActivationLimit={setLicenseActivationLimit}
|
||||
licenseDurationDays={licenseDurationDays}
|
||||
setLicenseDurationDays={setLicenseDurationDays}
|
||||
/>
|
||||
</FormSection>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { __ } from '@/lib/i18n';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -8,7 +8,8 @@ import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { DollarSign, Upload, X, Image as ImageIcon } from 'lucide-react';
|
||||
import { DollarSign, Upload, X, Image as ImageIcon, Copy, Check, Key } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { getStoreCurrency } from '@/lib/currency';
|
||||
import { RichTextEditor } from '@/components/RichTextEditor';
|
||||
import { openWPMediaGallery } from '@/lib/wp-media';
|
||||
@@ -40,6 +41,15 @@ type GeneralTabProps = {
|
||||
setRegularPrice: (value: string) => void;
|
||||
salePrice: string;
|
||||
setSalePrice: (value: string) => void;
|
||||
// For copy links
|
||||
productId?: number;
|
||||
// Licensing
|
||||
licensingEnabled?: boolean;
|
||||
setLicensingEnabled?: (value: boolean) => void;
|
||||
licenseActivationLimit?: string;
|
||||
setLicenseActivationLimit?: (value: string) => void;
|
||||
licenseDurationDays?: string;
|
||||
setLicenseDurationDays?: (value: string) => void;
|
||||
};
|
||||
|
||||
export function GeneralTab({
|
||||
@@ -67,6 +77,13 @@ export function GeneralTab({
|
||||
setRegularPrice,
|
||||
salePrice,
|
||||
setSalePrice,
|
||||
productId,
|
||||
licensingEnabled,
|
||||
setLicensingEnabled,
|
||||
licenseActivationLimit,
|
||||
setLicenseActivationLimit,
|
||||
licenseDurationDays,
|
||||
setLicenseDurationDays,
|
||||
}: GeneralTabProps) {
|
||||
const savingsPercent =
|
||||
salePrice && regularPrice && parseFloat(salePrice) < parseFloat(regularPrice)
|
||||
@@ -75,6 +92,30 @@ export function GeneralTab({
|
||||
|
||||
const store = getStoreCurrency();
|
||||
|
||||
// Copy link state and helpers
|
||||
const [copiedLink, setCopiedLink] = useState<string | null>(null);
|
||||
const siteUrl = window.location.origin;
|
||||
const spaPagePath = '/store';
|
||||
|
||||
const generateSimpleLink = (redirect: 'cart' | 'checkout' = 'cart') => {
|
||||
if (!productId) return '';
|
||||
const params = new URLSearchParams();
|
||||
params.set('add-to-cart', productId.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');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -387,8 +428,104 @@ export function GeneralTab({
|
||||
{__('Featured product (show in featured sections)')}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{/* Licensing option */}
|
||||
{setLicensingEnabled && (
|
||||
<>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="licensing-enabled"
|
||||
checked={licensingEnabled || false}
|
||||
onCheckedChange={(checked) => setLicensingEnabled(checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="licensing-enabled" className="cursor-pointer font-normal flex items-center gap-1">
|
||||
<Key className="h-3 w-3" />
|
||||
{__('Enable licensing for this product')}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{/* Licensing settings panel */}
|
||||
{licensingEnabled && (
|
||||
<div className="ml-6 p-3 bg-muted/50 rounded-lg space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-xs">{__('Activation Limit')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder={__('0 = unlimited')}
|
||||
value={licenseActivationLimit || ''}
|
||||
onChange={(e) => setLicenseActivationLimit?.(e.target.value)}
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{__('0 or empty = use global default')}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">{__('License Duration (Days)')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder={__('365')}
|
||||
value={licenseDurationDays || ''}
|
||||
onChange={(e) => setLicenseDurationDays?.(e.target.value)}
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{__('0 = never expires')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Direct Cart Links - Simple products only */}
|
||||
{productId && type === 'simple' && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-3">
|
||||
<Label>{__('Direct-to-Cart Links')}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{__('Share these links to add this product directly to cart or checkout')}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => copyToClipboard(generateSimpleLink('cart'), 'Cart')}
|
||||
className="flex-1"
|
||||
>
|
||||
{copiedLink === generateSimpleLink('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(generateSimpleLink('checkout'), 'Checkout')}
|
||||
className="flex-1"
|
||||
>
|
||||
{copiedLink === generateSimpleLink('checkout') ? (
|
||||
<Check className="h-3 w-3 mr-1" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3 mr-1" />
|
||||
)}
|
||||
{__('Copy Checkout Link')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -413,6 +413,17 @@ class ProductsController {
|
||||
|
||||
$product->save();
|
||||
|
||||
// Licensing meta
|
||||
if (isset($data['licensing_enabled'])) {
|
||||
update_post_meta($product->get_id(), '_licensing_enabled', $data['licensing_enabled'] ? 'yes' : 'no');
|
||||
}
|
||||
if (isset($data['license_activation_limit'])) {
|
||||
update_post_meta($product->get_id(), '_license_activation_limit', self::sanitize_number($data['license_activation_limit']));
|
||||
}
|
||||
if (isset($data['license_duration_days'])) {
|
||||
update_post_meta($product->get_id(), '_license_duration_days', self::sanitize_number($data['license_duration_days']));
|
||||
}
|
||||
|
||||
// Handle variations for variable products
|
||||
if ($type === 'variable' && !empty($data['attributes']) && is_array($data['attributes'])) {
|
||||
self::save_product_attributes($product, $data['attributes']);
|
||||
@@ -546,6 +557,17 @@ class ProductsController {
|
||||
|
||||
$product->save();
|
||||
|
||||
// Licensing meta
|
||||
if (isset($data['licensing_enabled'])) {
|
||||
update_post_meta($product->get_id(), '_licensing_enabled', $data['licensing_enabled'] ? 'yes' : 'no');
|
||||
}
|
||||
if (isset($data['license_activation_limit'])) {
|
||||
update_post_meta($product->get_id(), '_license_activation_limit', self::sanitize_number($data['license_activation_limit']));
|
||||
}
|
||||
if (isset($data['license_duration_days'])) {
|
||||
update_post_meta($product->get_id(), '_license_duration_days', self::sanitize_number($data['license_duration_days']));
|
||||
}
|
||||
|
||||
// Allow plugins to perform additional updates (Level 1 compatibility)
|
||||
do_action('woonoow/product_updated', $product, $data, $request);
|
||||
|
||||
@@ -738,6 +760,11 @@ class ProductsController {
|
||||
$data['download_expiry'] = $product->get_download_expiry() !== -1 ? (string) $product->get_download_expiry() : '';
|
||||
}
|
||||
|
||||
// Licensing fields
|
||||
$data['licensing_enabled'] = get_post_meta($product->get_id(), '_licensing_enabled', true) === 'yes';
|
||||
$data['license_activation_limit'] = get_post_meta($product->get_id(), '_license_activation_limit', true) ?: '';
|
||||
$data['license_duration_days'] = get_post_meta($product->get_id(), '_license_duration_days', true) ?: '';
|
||||
|
||||
// Images array (URLs) for frontend - featured + gallery
|
||||
$images = [];
|
||||
$featured_image_id = $product->get_image_id();
|
||||
|
||||
Reference in New Issue
Block a user