From 2efc6a760584eba5b9dbbbfd2e26e29d5407e039 Mon Sep 17 00:00:00 2001 From: Dwindi Ramadhana Date: Mon, 5 Jan 2026 17:32:49 +0700 Subject: [PATCH] feat: Add variation-level license duration to product editor - Added license_duration_days field to ProductVariant type - Added License Duration input to each variation card - Backend: ProductsController saves/loads variation-level _license_duration_days meta - Allows different license periods per variation (e.g., 1-year, 2-year, lifetime) --- .../Products/partials/tabs/VariationsTab.tsx | 33 +++++++++++++++---- includes/Api/ProductsController.php | 6 ++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/admin-spa/src/routes/Products/partials/tabs/VariationsTab.tsx b/admin-spa/src/routes/Products/partials/tabs/VariationsTab.tsx index 66118f7..4a7ca95 100644 --- a/admin-spa/src/routes/Products/partials/tabs/VariationsTab.tsx +++ b/admin-spa/src/routes/Products/partials/tabs/VariationsTab.tsx @@ -22,6 +22,7 @@ export type ProductVariant = { manage_stock?: boolean; stock_status?: 'instock' | 'outofstock' | 'onbackorder'; image?: string; + license_duration_days?: string; }; type VariationsTabProps = { @@ -43,10 +44,10 @@ export function VariationsTab({ }: VariationsTabProps) { const store = getStoreCurrency(); const [copiedLink, setCopiedLink] = useState(null); - + const siteUrl = window.location.origin; const spaPagePath = '/store'; - + const generateLink = (variationId: number, redirect: 'cart' | 'checkout' = 'cart') => { if (!productId) return ''; const params = new URLSearchParams(); @@ -55,7 +56,7 @@ export function VariationsTab({ params.set('redirect', redirect); return `${siteUrl}${spaPagePath}?${params.toString()}`; }; - + const copyToClipboard = async (link: string, label: string) => { try { await navigator.clipboard.writeText(link); @@ -66,7 +67,7 @@ export function VariationsTab({ toast.error('Failed to copy link'); } }; - + const addAttribute = () => { setAttributes([...attributes, { name: '', options: [], variation: false }]); }; @@ -83,7 +84,7 @@ export function VariationsTab({ const generateVariations = () => { const variationAttrs = attributes.filter(attr => attr.variation && attr.options.length > 0); - + if (variationAttrs.length === 0) { toast.warning(__('Please add at least one variation attribute with options')); return; @@ -276,6 +277,26 @@ export function VariationsTab({ )} + + {/* License Duration - only show if licensing is enabled on product */} +
+ + { + const updated = [...variations]; + updated[index].license_duration_days = e.target.value; + setVariations(updated); + }} + className="mt-1" + /> +

+ {__('Override license duration for this variation. 0 = never expires.')} +

+
- + {/* Direct Cart Links */} {productId && variation.id && (
diff --git a/includes/Api/ProductsController.php b/includes/Api/ProductsController.php index 3777e37..ea22900 100644 --- a/includes/Api/ProductsController.php +++ b/includes/Api/ProductsController.php @@ -898,6 +898,7 @@ class ProductsController { 'image_id' => $variation->get_image_id(), 'image_url' => $image_url, 'image' => $image_url, // For form compatibility + 'license_duration_days' => get_post_meta($variation->get_id(), '_license_duration_days', true) ?: '', ]; } } @@ -994,6 +995,11 @@ class ProductsController { $saved_id = $variation->save(); $variations_to_keep[] = $saved_id; + // Save variation-level license duration + if (isset($var_data['license_duration_days'])) { + update_post_meta($saved_id, '_license_duration_days', self::sanitize_number($var_data['license_duration_days'])); + } + // Manually save attributes using direct database insert if (!empty($wc_attributes)) { global $wpdb;