Enhance Subscriptions, Affiliates, and Software Distribution modules
This commit is contained in:
@@ -30,7 +30,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Search, Package, Plus, History, Download, Loader2 } from 'lucide-react';
|
import { Search, Package, Plus, History, Download, Loader2, ChevronDown, ChevronRight, X, Pencil } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { __ } from '@/lib/i18n';
|
import { __ } from '@/lib/i18n';
|
||||||
|
|
||||||
@@ -43,11 +43,21 @@ interface SoftwareProduct {
|
|||||||
total_downloads: number;
|
total_downloads: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ChangelogPoint {
|
||||||
|
type: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChangelogData {
|
||||||
|
narrative: string;
|
||||||
|
points: ChangelogPoint[];
|
||||||
|
}
|
||||||
|
|
||||||
interface SoftwareVersion {
|
interface SoftwareVersion {
|
||||||
id: number;
|
id: number;
|
||||||
product_id: number;
|
product_id: number;
|
||||||
version: string;
|
version: string;
|
||||||
changelog: string;
|
changelog: ChangelogData | string;
|
||||||
release_date: string;
|
release_date: string;
|
||||||
is_current: boolean;
|
is_current: boolean;
|
||||||
download_count: number;
|
download_count: number;
|
||||||
@@ -72,7 +82,13 @@ export default function SoftwareVersions() {
|
|||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [selectedProduct, setSelectedProduct] = useState<number | null>(null);
|
const [selectedProduct, setSelectedProduct] = useState<number | null>(null);
|
||||||
const [isAddVersionOpen, setIsAddVersionOpen] = useState(false);
|
const [isAddVersionOpen, setIsAddVersionOpen] = useState(false);
|
||||||
const [newVersion, setNewVersion] = useState({ version: '', changelog: '' });
|
const [editingVersionId, setEditingVersionId] = useState<number | null>(null);
|
||||||
|
const [newVersion, setNewVersion] = useState({
|
||||||
|
version: '',
|
||||||
|
changelog: { narrative: '', points: [] as ChangelogPoint[] }
|
||||||
|
});
|
||||||
|
const [expandedVersions, setExpandedVersions] = useState<Record<number, boolean>>({});
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// Fetch software-enabled products
|
// Fetch software-enabled products
|
||||||
@@ -80,15 +96,14 @@ export default function SoftwareVersions() {
|
|||||||
queryKey: ['software-products'],
|
queryKey: ['software-products'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await api.get('/products?software_enabled=true&per_page=100');
|
const response = await api.get('/products?software_enabled=true&per_page=100');
|
||||||
// Filter products that have software distribution enabled
|
const products = (response as any).rows || [];
|
||||||
const products = (response as any).products || [];
|
|
||||||
return {
|
return {
|
||||||
products: products.filter((p: any) => p.meta?._woonoow_software_enabled === 'yes').map((p: any) => ({
|
products: products.map((p: any) => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
slug: p.meta?._woonoow_software_slug || '',
|
slug: p.software_slug || p.meta?._woonoow_software_slug || '',
|
||||||
current_version: p.meta?._woonoow_software_current_version || '',
|
current_version: p.software_current_version || p.meta?._woonoow_software_current_version || '',
|
||||||
wp_enabled: p.meta?._woonoow_software_wp_enabled === 'yes',
|
wp_enabled: p.software_wp_enabled || p.meta?._woonoow_software_wp_enabled === 'yes',
|
||||||
total_downloads: 0,
|
total_downloads: 0,
|
||||||
}))
|
}))
|
||||||
} as ProductsResponse;
|
} as ProductsResponse;
|
||||||
@@ -107,21 +122,121 @@ export default function SoftwareVersions() {
|
|||||||
|
|
||||||
// Add new version mutation
|
// Add new version mutation
|
||||||
const addVersion = useMutation({
|
const addVersion = useMutation({
|
||||||
mutationFn: async (data: { version: string; changelog: string }) => {
|
mutationFn: async (data: { version: string; changelog: ChangelogData }) => {
|
||||||
return await api.post(`/software/products/${selectedProduct}/versions`, data);
|
return await api.post(`/software/products/${selectedProduct}/versions`, data);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['software-versions', selectedProduct] });
|
queryClient.invalidateQueries({ queryKey: ['software-versions', selectedProduct] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['software-products'] });
|
queryClient.invalidateQueries({ queryKey: ['software-products'] });
|
||||||
toast.success(__('Version added successfully'));
|
toast.success(__('Version added successfully'));
|
||||||
setIsAddVersionOpen(false);
|
closeModal();
|
||||||
setNewVersion({ version: '', changelog: '' });
|
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
toast.error(error.message || __('Failed to add version'));
|
toast.error(error.message || __('Failed to add version'));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Edit version mutation
|
||||||
|
const editVersion = useMutation({
|
||||||
|
mutationFn: async (data: { version_id: number; version: string; changelog: ChangelogData }) => {
|
||||||
|
return await api.put(`/software/products/${selectedProduct}/versions/${data.version_id}`, {
|
||||||
|
version: data.version,
|
||||||
|
changelog: data.changelog
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['software-versions', selectedProduct] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['software-products'] });
|
||||||
|
toast.success(__('Version updated successfully'));
|
||||||
|
closeModal();
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.message || __('Failed to update version'));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const openAddModal = () => {
|
||||||
|
setEditingVersionId(null);
|
||||||
|
setNewVersion({ version: '', changelog: { narrative: '', points: [] } });
|
||||||
|
setIsAddVersionOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditModal = (version: SoftwareVersion, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const cl = typeof version.changelog === 'object' && version.changelog !== null
|
||||||
|
? (version.changelog as ChangelogData)
|
||||||
|
: { narrative: version.changelog as string, points: [] };
|
||||||
|
|
||||||
|
setEditingVersionId(version.id);
|
||||||
|
setNewVersion({
|
||||||
|
version: version.version,
|
||||||
|
changelog: { narrative: cl.narrative || '', points: cl.points || [] }
|
||||||
|
});
|
||||||
|
setIsAddVersionOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsAddVersionOpen(false);
|
||||||
|
setEditingVersionId(null);
|
||||||
|
setNewVersion({ version: '', changelog: { narrative: '', points: [] } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveVersion = () => {
|
||||||
|
if (editingVersionId) {
|
||||||
|
editVersion.mutate({
|
||||||
|
version_id: editingVersionId,
|
||||||
|
...newVersion
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
addVersion.mutate(newVersion);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleVersion = (id: number) => {
|
||||||
|
setExpandedVersions(prev => ({
|
||||||
|
...prev,
|
||||||
|
[id]: !prev[id]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addChangelogPoint = () => {
|
||||||
|
setNewVersion(prev => ({
|
||||||
|
...prev,
|
||||||
|
changelog: {
|
||||||
|
...prev.changelog,
|
||||||
|
points: [...prev.changelog.points, { type: 'ADD', text: '' }]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateChangelogPoint = (index: number, field: 'type' | 'text', value: string) => {
|
||||||
|
setNewVersion(prev => {
|
||||||
|
const newPoints = [...prev.changelog.points];
|
||||||
|
newPoints[index] = { ...newPoints[index], [field]: value };
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
changelog: {
|
||||||
|
...prev.changelog,
|
||||||
|
points: newPoints
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeChangelogPoint = (index: number) => {
|
||||||
|
setNewVersion(prev => {
|
||||||
|
const newPoints = [...prev.changelog.points];
|
||||||
|
newPoints.splice(index, 1);
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
changelog: {
|
||||||
|
...prev.changelog,
|
||||||
|
points: newPoints
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const filteredProducts = productsData?.products?.filter(p =>
|
const filteredProducts = productsData?.products?.filter(p =>
|
||||||
p.name.toLowerCase().includes(search.toLowerCase()) ||
|
p.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
p.slug.toLowerCase().includes(search.toLowerCase())
|
p.slug.toLowerCase().includes(search.toLowerCase())
|
||||||
@@ -129,9 +244,20 @@ export default function SoftwareVersions() {
|
|||||||
|
|
||||||
const selectedProductData = productsData?.products?.find(p => p.id === selectedProduct);
|
const selectedProductData = productsData?.products?.find(p => p.id === selectedProduct);
|
||||||
|
|
||||||
|
const getBadgeColor = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'ADD': return 'bg-emerald-500 hover:bg-emerald-600';
|
||||||
|
case 'FIX': return 'bg-orange-500 hover:bg-orange-600';
|
||||||
|
case 'IMPROVE': return 'bg-blue-500 hover:bg-blue-600';
|
||||||
|
case 'DROP': return 'bg-rose-500 hover:bg-rose-600';
|
||||||
|
default: return 'bg-gray-500 hover:bg-gray-600';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSaving = addVersion.isPending || editVersion.isPending;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold">{__('Software Versions')}</h1>
|
<h1 className="text-2xl font-bold">{__('Software Versions')}</h1>
|
||||||
@@ -142,7 +268,6 @@ export default function SoftwareVersions() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
{/* Products List */}
|
|
||||||
<div className="lg:col-span-1 border rounded-lg bg-card">
|
<div className="lg:col-span-1 border rounded-lg bg-card">
|
||||||
<div className="p-4 border-b">
|
<div className="p-4 border-b">
|
||||||
<h2 className="font-semibold mb-3">{__('Software Products')}</h2>
|
<h2 className="font-semibold mb-3">{__('Software Products')}</h2>
|
||||||
@@ -176,8 +301,7 @@ export default function SoftwareVersions() {
|
|||||||
<button
|
<button
|
||||||
key={product.id}
|
key={product.id}
|
||||||
onClick={() => setSelectedProduct(product.id)}
|
onClick={() => setSelectedProduct(product.id)}
|
||||||
className={`w-full p-4 text-left hover:bg-accent transition-colors ${selectedProduct === product.id ? 'bg-accent' : ''
|
className={`w-full p-4 text-left hover:bg-accent transition-colors ${selectedProduct === product.id ? 'bg-accent' : ''}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -186,7 +310,7 @@ export default function SoftwareVersions() {
|
|||||||
{product.slug}
|
{product.slug}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="secondary" className="ml-2">
|
<Badge variant="secondary" className="ml-2 whitespace-nowrap">
|
||||||
v{product.current_version || '—'}
|
v{product.current_version || '—'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -202,7 +326,6 @@ export default function SoftwareVersions() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Version Details */}
|
|
||||||
<div className="lg:col-span-2 border rounded-lg bg-card">
|
<div className="lg:col-span-2 border rounded-lg bg-card">
|
||||||
{!selectedProduct ? (
|
{!selectedProduct ? (
|
||||||
<div className="flex items-center justify-center h-full min-h-[400px] text-muted-foreground">
|
<div className="flex items-center justify-center h-full min-h-[400px] text-muted-foreground">
|
||||||
@@ -217,7 +340,6 @@ export default function SoftwareVersions() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Version Header */}
|
|
||||||
<div className="p-4 border-b flex items-center justify-between">
|
<div className="p-4 border-b flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-semibold">{selectedProductData?.name}</h2>
|
<h2 className="font-semibold">{selectedProductData?.name}</h2>
|
||||||
@@ -225,21 +347,25 @@ export default function SoftwareVersions() {
|
|||||||
{__('Current version')}: <span className="font-mono">{versionsData?.config?.current_version || '—'}</span>
|
{__('Current version')}: <span className="font-mono">{versionsData?.config?.current_version || '—'}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Dialog open={isAddVersionOpen} onOpenChange={setIsAddVersionOpen}>
|
<Dialog open={isAddVersionOpen} onOpenChange={(open) => !open ? closeModal() : setIsAddVersionOpen(true)}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button>
|
<Button onClick={openAddModal}>
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
{__('New Version')}
|
{__('New Version')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-lg">
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{__('Add New Version')}</DialogTitle>
|
<DialogTitle>
|
||||||
|
{editingVersionId ? __('Edit Version') : __('Add New Version')}
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{__('Release a new version of')} {selectedProductData?.name}
|
{editingVersionId
|
||||||
|
? `${__('Modify release details for')} ${selectedProductData?.name}`
|
||||||
|
: `${__('Release a new version of')} ${selectedProductData?.name}`}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 px-6 py-4">
|
<div className="space-y-6 px-6 py-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="version">{__('Version Number')}</Label>
|
<Label htmlFor="version">{__('Version Number')}</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -248,92 +374,195 @@ export default function SoftwareVersions() {
|
|||||||
value={newVersion.version}
|
value={newVersion.version}
|
||||||
onChange={(e) => setNewVersion(prev => ({ ...prev, version: e.target.value }))}
|
onChange={(e) => setNewVersion(prev => ({ ...prev, version: e.target.value }))}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{__('Use semantic versioning (e.g., 1.0.0, 1.2.3)')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
<Label htmlFor="changelog">{__('Changelog')}</Label>
|
<div className="space-y-2">
|
||||||
<Textarea
|
<Label htmlFor="narrative">{__('Changelog Narrative (Optional)')}</Label>
|
||||||
id="changelog"
|
<Textarea
|
||||||
placeholder="## What's New - Added new feature - Fixed bug"
|
id="narrative"
|
||||||
value={newVersion.changelog}
|
placeholder={__('Provide a general overview of this release...')}
|
||||||
onChange={(e) => setNewVersion(prev => ({ ...prev, changelog: e.target.value }))}
|
value={newVersion.changelog.narrative}
|
||||||
rows={8}
|
onChange={(e) => setNewVersion(prev => ({
|
||||||
className="font-mono text-sm"
|
...prev,
|
||||||
/>
|
changelog: { ...prev.changelog, narrative: e.target.value }
|
||||||
<p className="text-xs text-muted-foreground">
|
}))}
|
||||||
{__('Supports Markdown formatting')}
|
className="min-h-[100px]"
|
||||||
</p>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>{__('Changes List')}</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{newVersion.changelog.points.length === 0 ? (
|
||||||
|
<div className="text-center p-6 border rounded-md border-dashed text-muted-foreground">
|
||||||
|
<p className="text-sm">{__('No changes added yet.')}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{newVersion.changelog.points.map((point, index) => (
|
||||||
|
<div key={index} className="flex items-start gap-2">
|
||||||
|
<Select
|
||||||
|
value={point.type}
|
||||||
|
onValueChange={(val) => updateChangelogPoint(index, 'type', val)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[130px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ADD">{__('ADD')}</SelectItem>
|
||||||
|
<SelectItem value="FIX">{__('FIX')}</SelectItem>
|
||||||
|
<SelectItem value="IMPROVE">{__('IMPROVE')}</SelectItem>
|
||||||
|
<SelectItem value="DROP">{__('DROP')}</SelectItem>
|
||||||
|
<SelectItem value="OTHER">{__('OTHER')}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Input
|
||||||
|
value={point.text}
|
||||||
|
onChange={(e) => updateChangelogPoint(index, 'text', e.target.value)}
|
||||||
|
placeholder={__('Describe the change...')}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => removeChangelogPoint(index)}
|
||||||
|
className="text-muted-foreground hover:text-destructive shrink-0"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button variant="outline" size="sm" onClick={addChangelogPoint} className="w-full">
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
{__('Add Change Item')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setIsAddVersionOpen(false)}>
|
<Button variant="outline" onClick={closeModal} disabled={isSaving}>
|
||||||
{__('Cancel')}
|
{__('Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => addVersion.mutate(newVersion)}
|
onClick={handleSaveVersion}
|
||||||
disabled={!newVersion.version || addVersion.isPending}
|
disabled={!newVersion.version || isSaving}
|
||||||
>
|
>
|
||||||
{addVersion.isPending && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
|
{isSaving && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
|
||||||
{__('Release Version')}
|
{editingVersionId ? __('Save Changes') : __('Release Version')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Version History */}
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{versionsData?.versions?.length === 0 ? (
|
{versionsData?.versions?.length === 0 ? (
|
||||||
<div className="text-center py-12 text-muted-foreground">
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
<History className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
<History className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||||
<p>{__('No versions released yet')}</p>
|
<p>{__('No versions released yet')}</p>
|
||||||
<p className="text-sm mt-1">
|
|
||||||
{__('Click "New Version" to release your first version')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
<TableHead className="w-8"></TableHead>
|
||||||
<TableHead>{__('Version')}</TableHead>
|
<TableHead>{__('Version')}</TableHead>
|
||||||
<TableHead>{__('Release Date')}</TableHead>
|
<TableHead>{__('Release Date')}</TableHead>
|
||||||
<TableHead>{__('Downloads')}</TableHead>
|
<TableHead>{__('Downloads')}</TableHead>
|
||||||
<TableHead>{__('Changelog')}</TableHead>
|
<TableHead>{__('Summary')}</TableHead>
|
||||||
|
<TableHead className="w-12"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{versionsData?.versions?.map((version) => (
|
{versionsData?.versions?.map((version) => {
|
||||||
<TableRow key={version.id}>
|
const isExpanded = !!expandedVersions[version.id];
|
||||||
<TableCell>
|
const cl = typeof version.changelog === 'object' && version.changelog !== null
|
||||||
<div className="flex items-center gap-2">
|
? (version.changelog as ChangelogData)
|
||||||
<span className="font-mono font-medium">
|
: { narrative: version.changelog as string, points: [] };
|
||||||
v{version.version}
|
|
||||||
</span>
|
return (
|
||||||
{version.is_current && (
|
<React.Fragment key={version.id}>
|
||||||
<Badge variant="default" className="text-xs">
|
<TableRow className="cursor-pointer hover:bg-muted/50 group" onClick={() => toggleVersion(version.id)}>
|
||||||
{__('Current')}
|
<TableCell>
|
||||||
</Badge>
|
<Button variant="ghost" size="icon" className="w-6 h-6">
|
||||||
)}
|
{isExpanded ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
|
||||||
</div>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-muted-foreground">
|
<TableCell>
|
||||||
{version.release_date ? new Date(version.release_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'}
|
<div className="flex items-center gap-2">
|
||||||
</TableCell>
|
<span className="font-mono font-medium">
|
||||||
<TableCell>
|
v{version.version}
|
||||||
<div className="flex items-center gap-1 text-muted-foreground">
|
</span>
|
||||||
<Download className="w-3 h-3" />
|
{version.is_current && (
|
||||||
{version.download_count}
|
<Badge variant="default" className="text-xs">
|
||||||
</div>
|
{__('Current')}
|
||||||
</TableCell>
|
</Badge>
|
||||||
<TableCell className="max-w-xs">
|
)}
|
||||||
<p className="text-sm text-muted-foreground truncate">
|
</div>
|
||||||
{version.changelog?.split('\n')[0] || '—'}
|
</TableCell>
|
||||||
</p>
|
<TableCell className="text-muted-foreground">
|
||||||
</TableCell>
|
{version.release_date ? new Date(version.release_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'}
|
||||||
</TableRow>
|
</TableCell>
|
||||||
))}
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-1 text-muted-foreground">
|
||||||
|
<Download className="w-3 h-3" />
|
||||||
|
{version.download_count}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
|
{cl.points?.length > 0 ? `${cl.points.length} changes` : (cl.narrative ? 'Notes attached' : '—')}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="w-8 h-8 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
onClick={(e) => openEditModal(version, e)}
|
||||||
|
title={__('Edit version')}
|
||||||
|
>
|
||||||
|
<Pencil className="w-4 h-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<TableRow className="bg-muted/30">
|
||||||
|
<TableCell colSpan={6} className="p-0">
|
||||||
|
<div className="p-6 text-sm border-t">
|
||||||
|
{cl.narrative && (
|
||||||
|
<div className="mb-4 text-foreground whitespace-pre-wrap">
|
||||||
|
{cl.narrative}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{cl.points && cl.points.length > 0 && (
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{cl.points.map((pt, idx) => (
|
||||||
|
<li key={idx} className="flex items-start gap-3">
|
||||||
|
<Badge className={`${getBadgeColor(pt.type)} text-[10px] uppercase font-bold mt-0.5`}>
|
||||||
|
{pt.type}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-muted-foreground">{pt.text}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!cl.narrative && (!cl.points || cl.points.length === 0) && (
|
||||||
|
<p className="text-muted-foreground italic">{__('No changelog details provided.')}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { api } from '@/lib/api';
|
|||||||
import { __ } from '@/lib/i18n';
|
import { __ } from '@/lib/i18n';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { VerticalTabForm, FormSection } from '@/components/VerticalTabForm';
|
import { VerticalTabForm, FormSection } from '@/components/VerticalTabForm';
|
||||||
import { Package, DollarSign, Layers, Tag, Download } from 'lucide-react';
|
import { Package, DollarSign, Layers, Tag, Download, Cloud } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { GeneralTab } from './tabs/GeneralTab';
|
import { GeneralTab } from './tabs/GeneralTab';
|
||||||
import { InventoryTab } from './tabs/InventoryTab';
|
import { InventoryTab } from './tabs/InventoryTab';
|
||||||
import { VariationsTab, ProductVariant } from './tabs/VariationsTab';
|
import { VariationsTab, ProductVariant } from './tabs/VariationsTab';
|
||||||
import { OrganizationTab } from './tabs/OrganizationTab';
|
import { OrganizationTab } from './tabs/OrganizationTab';
|
||||||
import { DownloadsTab, DownloadableFile } from './tabs/DownloadsTab';
|
import { DownloadsTab, DownloadableFile } from './tabs/DownloadsTab';
|
||||||
|
import { SoftwareTab } from './tabs/SoftwareTab';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
export type ProductFormData = {
|
export type ProductFormData = {
|
||||||
@@ -50,6 +51,13 @@ export type ProductFormData = {
|
|||||||
// Affiliate
|
// Affiliate
|
||||||
affiliate_enabled?: boolean;
|
affiliate_enabled?: boolean;
|
||||||
affiliate_commission_rate?: string;
|
affiliate_commission_rate?: string;
|
||||||
|
// Software
|
||||||
|
software_enabled?: boolean;
|
||||||
|
software_slug?: string;
|
||||||
|
software_wp_enabled?: boolean;
|
||||||
|
software_requires_wp?: string;
|
||||||
|
software_tested_wp?: string;
|
||||||
|
software_requires_php?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -109,6 +117,13 @@ export function ProductFormTabbed({
|
|||||||
// Affiliate state
|
// Affiliate state
|
||||||
const [affiliateEnabled, setAffiliateEnabled] = useState(initial?.affiliate_enabled || false);
|
const [affiliateEnabled, setAffiliateEnabled] = useState(initial?.affiliate_enabled || false);
|
||||||
const [affiliateCommissionRate, setAffiliateCommissionRate] = useState(initial?.affiliate_commission_rate || '');
|
const [affiliateCommissionRate, setAffiliateCommissionRate] = useState(initial?.affiliate_commission_rate || '');
|
||||||
|
// Software state
|
||||||
|
const [softwareEnabled, setSoftwareEnabled] = useState(initial?.software_enabled || false);
|
||||||
|
const [softwareSlug, setSoftwareSlug] = useState(initial?.software_slug || '');
|
||||||
|
const [softwareWpEnabled, setSoftwareWpEnabled] = useState(initial?.software_wp_enabled || false);
|
||||||
|
const [softwareRequiresWp, setSoftwareRequiresWp] = useState(initial?.software_requires_wp || '');
|
||||||
|
const [softwareTestedWp, setSoftwareTestedWp] = useState(initial?.software_tested_wp || '');
|
||||||
|
const [softwareRequiresPhp, setSoftwareRequiresPhp] = useState(initial?.software_requires_php || '');
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
// Update form state when initial data changes (for edit mode)
|
// Update form state when initial data changes (for edit mode)
|
||||||
@@ -149,6 +164,13 @@ export function ProductFormTabbed({
|
|||||||
// Affiliate
|
// Affiliate
|
||||||
setAffiliateEnabled(initial.affiliate_enabled || false);
|
setAffiliateEnabled(initial.affiliate_enabled || false);
|
||||||
setAffiliateCommissionRate(initial.affiliate_commission_rate || '');
|
setAffiliateCommissionRate(initial.affiliate_commission_rate || '');
|
||||||
|
// Software
|
||||||
|
setSoftwareEnabled(initial.software_enabled || false);
|
||||||
|
setSoftwareSlug(initial.software_slug || '');
|
||||||
|
setSoftwareWpEnabled(initial.software_wp_enabled || false);
|
||||||
|
setSoftwareRequiresWp(initial.software_requires_wp || '');
|
||||||
|
setSoftwareTestedWp(initial.software_tested_wp || '');
|
||||||
|
setSoftwareRequiresPhp(initial.software_requires_php || '');
|
||||||
}
|
}
|
||||||
}, [initial, mode]);
|
}, [initial, mode]);
|
||||||
|
|
||||||
@@ -221,6 +243,13 @@ export function ProductFormTabbed({
|
|||||||
// Affiliate
|
// Affiliate
|
||||||
affiliate_enabled: affiliateEnabled,
|
affiliate_enabled: affiliateEnabled,
|
||||||
affiliate_commission_rate: affiliateEnabled ? affiliateCommissionRate : undefined,
|
affiliate_commission_rate: affiliateEnabled ? affiliateCommissionRate : undefined,
|
||||||
|
// Software
|
||||||
|
software_enabled: softwareEnabled,
|
||||||
|
software_slug: softwareEnabled ? softwareSlug : undefined,
|
||||||
|
software_wp_enabled: softwareEnabled ? softwareWpEnabled : undefined,
|
||||||
|
software_requires_wp: (softwareEnabled && softwareWpEnabled) ? softwareRequiresWp : undefined,
|
||||||
|
software_tested_wp: (softwareEnabled && softwareWpEnabled) ? softwareTestedWp : undefined,
|
||||||
|
software_requires_php: (softwareEnabled && softwareWpEnabled) ? softwareRequiresPhp : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
await onSubmit(payload);
|
await onSubmit(payload);
|
||||||
@@ -238,6 +267,7 @@ export function ProductFormTabbed({
|
|||||||
...(downloadable ? [{ id: 'downloads', label: __('Downloads'), icon: <Download className="w-4 h-4" /> }] : []),
|
...(downloadable ? [{ id: 'downloads', label: __('Downloads'), icon: <Download className="w-4 h-4" /> }] : []),
|
||||||
...(type === 'variable' ? [{ id: 'variations', label: __('Variations'), icon: <Layers className="w-4 h-4" /> }] : []),
|
...(type === 'variable' ? [{ id: 'variations', label: __('Variations'), icon: <Layers className="w-4 h-4" /> }] : []),
|
||||||
{ id: 'organization', label: __('Organization'), icon: <Tag className="w-4 h-4" /> },
|
{ id: 'organization', label: __('Organization'), icon: <Tag className="w-4 h-4" /> },
|
||||||
|
{ id: 'software', label: __('Software'), icon: <Cloud className="w-4 h-4" /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -348,6 +378,24 @@ export function ProductFormTabbed({
|
|||||||
/>
|
/>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
|
{/* Software Tab */}
|
||||||
|
<FormSection id="software">
|
||||||
|
<SoftwareTab
|
||||||
|
softwareEnabled={softwareEnabled}
|
||||||
|
setSoftwareEnabled={setSoftwareEnabled}
|
||||||
|
softwareSlug={softwareSlug}
|
||||||
|
setSoftwareSlug={setSoftwareSlug}
|
||||||
|
softwareWpEnabled={softwareWpEnabled}
|
||||||
|
setSoftwareWpEnabled={setSoftwareWpEnabled}
|
||||||
|
softwareRequiresWp={softwareRequiresWp}
|
||||||
|
setSoftwareRequiresWp={setSoftwareRequiresWp}
|
||||||
|
softwareTestedWp={softwareTestedWp}
|
||||||
|
setSoftwareTestedWp={setSoftwareTestedWp}
|
||||||
|
softwareRequiresPhp={softwareRequiresPhp}
|
||||||
|
setSoftwareRequiresPhp={setSoftwareRequiresPhp}
|
||||||
|
/>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
{!hideSubmitButton && (
|
{!hideSubmitButton && (
|
||||||
<div className="mt-6 flex gap-3">
|
<div className="mt-6 flex gap-3">
|
||||||
|
|||||||
115
admin-spa/src/routes/Products/partials/tabs/SoftwareTab.tsx
Normal file
115
admin-spa/src/routes/Products/partials/tabs/SoftwareTab.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { __ } from '@/lib/i18n';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
softwareEnabled: boolean;
|
||||||
|
setSoftwareEnabled: (val: boolean) => void;
|
||||||
|
softwareSlug: string;
|
||||||
|
setSoftwareSlug: (val: string) => void;
|
||||||
|
softwareWpEnabled: boolean;
|
||||||
|
setSoftwareWpEnabled: (val: boolean) => void;
|
||||||
|
softwareRequiresWp: string;
|
||||||
|
setSoftwareRequiresWp: (val: string) => void;
|
||||||
|
softwareTestedWp: string;
|
||||||
|
setSoftwareTestedWp: (val: string) => void;
|
||||||
|
softwareRequiresPhp: string;
|
||||||
|
setSoftwareRequiresPhp: (val: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SoftwareTab({
|
||||||
|
softwareEnabled,
|
||||||
|
setSoftwareEnabled,
|
||||||
|
softwareSlug,
|
||||||
|
setSoftwareSlug,
|
||||||
|
softwareWpEnabled,
|
||||||
|
setSoftwareWpEnabled,
|
||||||
|
softwareRequiresWp,
|
||||||
|
setSoftwareRequiresWp,
|
||||||
|
softwareTestedWp,
|
||||||
|
setSoftwareTestedWp,
|
||||||
|
softwareRequiresPhp,
|
||||||
|
setSoftwareRequiresPhp,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">{__('Software Distribution')}</h3>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{__('Enable this to distribute software updates, manage versioning, and secure downloads.')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg border">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label className="text-base">{__('Enable Software Distribution')}</Label>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{__('Allow this product to serve OTA updates and track versions.')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch checked={softwareEnabled} onCheckedChange={setSoftwareEnabled} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{softwareEnabled && (
|
||||||
|
<div className="space-y-4 pt-4 border-t">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="softwareSlug">{__('Software Slug (Unique Identifier)')}</Label>
|
||||||
|
<Input
|
||||||
|
id="softwareSlug"
|
||||||
|
value={softwareSlug}
|
||||||
|
onChange={(e) => setSoftwareSlug(e.target.value)}
|
||||||
|
placeholder="e.g. acme-seo-pro"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{__('The unique slug that the software client will use to check for updates.')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg border mt-6">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label className="text-base">{__('WordPress Product')}</Label>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{__('Check if this software is a WordPress Plugin or Theme.')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch checked={softwareWpEnabled} onCheckedChange={setSoftwareWpEnabled} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{softwareWpEnabled && (
|
||||||
|
<div className="grid grid-cols-3 gap-4 pt-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="requiresWp">{__('Requires WP')}</Label>
|
||||||
|
<Input
|
||||||
|
id="requiresWp"
|
||||||
|
value={softwareRequiresWp}
|
||||||
|
onChange={(e) => setSoftwareRequiresWp(e.target.value)}
|
||||||
|
placeholder="e.g. 5.8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="testedWp">{__('Tested up to WP')}</Label>
|
||||||
|
<Input
|
||||||
|
id="testedWp"
|
||||||
|
value={softwareTestedWp}
|
||||||
|
onChange={(e) => setSoftwareTestedWp(e.target.value)}
|
||||||
|
placeholder="e.g. 6.4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="requiresPhp">{__('Requires PHP')}</Label>
|
||||||
|
<Input
|
||||||
|
id="requiresPhp"
|
||||||
|
value={softwareRequiresPhp}
|
||||||
|
onChange={(e) => setSoftwareRequiresPhp(e.target.value)}
|
||||||
|
placeholder="e.g. 7.4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -25,6 +25,10 @@ export type ProductVariant = {
|
|||||||
stock_status?: 'instock' | 'outofstock' | 'onbackorder';
|
stock_status?: 'instock' | 'outofstock' | 'onbackorder';
|
||||||
image?: string;
|
image?: string;
|
||||||
license_duration_days?: string;
|
license_duration_days?: string;
|
||||||
|
subscription_signup_fee?: string;
|
||||||
|
subscription_trial_days?: string;
|
||||||
|
subscription_period?: 'day' | 'week' | 'month' | 'year';
|
||||||
|
subscription_interval?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type VariationsTabProps = {
|
type VariationsTabProps = {
|
||||||
@@ -282,8 +286,83 @@ export function VariationsTab({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Subscription Fields */}
|
||||||
|
<div className="col-span-2 md:col-span-4 space-y-3">
|
||||||
|
<Label className="text-xs font-semibold">{__('Subscription Settings (Optional)')}</Label>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">{__('Period')}</Label>
|
||||||
|
<select
|
||||||
|
value={variation.subscription_period || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updated = [...variations];
|
||||||
|
updated[index].subscription_period = e.target.value as any;
|
||||||
|
setVariations(updated);
|
||||||
|
}}
|
||||||
|
className="flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 mt-1"
|
||||||
|
>
|
||||||
|
<option value="">{__('Parent Default')}</option>
|
||||||
|
<option value="day">{__('Day')}</option>
|
||||||
|
<option value="week">{__('Week')}</option>
|
||||||
|
<option value="month">{__('Month')}</option>
|
||||||
|
<option value="year">{__('Year')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">{__('Interval')}</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
placeholder={__('Parent')}
|
||||||
|
value={variation.subscription_interval || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updated = [...variations];
|
||||||
|
updated[index].subscription_interval = e.target.value;
|
||||||
|
setVariations(updated);
|
||||||
|
}}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">{__('Trial Days')}</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
placeholder={__('Parent')}
|
||||||
|
value={variation.subscription_trial_days || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updated = [...variations];
|
||||||
|
updated[index].subscription_trial_days = e.target.value;
|
||||||
|
setVariations(updated);
|
||||||
|
}}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<Label className="text-xs">{__('Signup Fee')}</Label>
|
||||||
|
<div className="relative mt-1">
|
||||||
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground font-medium">
|
||||||
|
{store.symbol}
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step={store.decimals === 0 ? '1' : '0.01'}
|
||||||
|
placeholder={__('Parent')}
|
||||||
|
value={variation.subscription_signup_fee || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updated = [...variations];
|
||||||
|
updated[index].subscription_signup_fee = e.target.value;
|
||||||
|
setVariations(updated);
|
||||||
|
}}
|
||||||
|
className="pl-8 pr-3 text-right"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* License Duration - only show if licensing is enabled on product */}
|
{/* License Duration - only show if licensing is enabled on product */}
|
||||||
<div className="col-span-2 md:col-span-4">
|
<div className="col-span-2 md:col-span-4 mt-2">
|
||||||
<Label className="text-xs">{__('License Duration (Days)')}</Label>
|
<Label className="text-xs">{__('License Duration (Days)')}</Label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ class ProductsController
|
|||||||
$category = $request->get_param('category');
|
$category = $request->get_param('category');
|
||||||
$type = $request->get_param('type');
|
$type = $request->get_param('type');
|
||||||
$stock_status = $request->get_param('stock_status');
|
$stock_status = $request->get_param('stock_status');
|
||||||
|
$software_enabled = $request->get_param('software_enabled');
|
||||||
$orderby = $request->get_param('orderby') ?: 'date';
|
$orderby = $request->get_param('orderby') ?: 'date';
|
||||||
$order = $request->get_param('order') ?: 'DESC';
|
$order = $request->get_param('order') ?: 'DESC';
|
||||||
|
|
||||||
@@ -266,11 +267,19 @@ class ProductsController
|
|||||||
|
|
||||||
// Stock status filter
|
// Stock status filter
|
||||||
if ($stock_status) {
|
if ($stock_status) {
|
||||||
$args['meta_query'] = [
|
$args['meta_query'] = $args['meta_query'] ?? [];
|
||||||
[
|
$args['meta_query'][] = [
|
||||||
'key' => '_stock_status',
|
'key' => '_stock_status',
|
||||||
'value' => $stock_status,
|
'value' => $stock_status,
|
||||||
],
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Software enabled filter
|
||||||
|
if ($software_enabled === 'true' || $software_enabled === '1') {
|
||||||
|
$args['meta_query'] = $args['meta_query'] ?? [];
|
||||||
|
$args['meta_query'][] = [
|
||||||
|
'key' => '_woonoow_software_enabled',
|
||||||
|
'value' => 'yes',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -660,6 +669,26 @@ class ProductsController
|
|||||||
update_post_meta($product->get_id(), '_woonoow_affiliate_commission_rate', self::sanitize_number($data['affiliate_commission_rate']));
|
update_post_meta($product->get_id(), '_woonoow_affiliate_commission_rate', self::sanitize_number($data['affiliate_commission_rate']));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Software meta
|
||||||
|
if (isset($data['software_enabled'])) {
|
||||||
|
update_post_meta($product->get_id(), '_woonoow_software_enabled', $data['software_enabled'] ? 'yes' : 'no');
|
||||||
|
}
|
||||||
|
if (isset($data['software_slug'])) {
|
||||||
|
update_post_meta($product->get_id(), '_woonoow_software_slug', sanitize_title($data['software_slug']));
|
||||||
|
}
|
||||||
|
if (isset($data['software_wp_enabled'])) {
|
||||||
|
update_post_meta($product->get_id(), '_woonoow_software_wp_enabled', $data['software_wp_enabled'] ? 'yes' : 'no');
|
||||||
|
}
|
||||||
|
if (isset($data['software_requires_wp'])) {
|
||||||
|
update_post_meta($product->get_id(), '_woonoow_software_requires_wp', sanitize_text_field($data['software_requires_wp']));
|
||||||
|
}
|
||||||
|
if (isset($data['software_tested_wp'])) {
|
||||||
|
update_post_meta($product->get_id(), '_woonoow_software_tested_wp', sanitize_text_field($data['software_tested_wp']));
|
||||||
|
}
|
||||||
|
if (isset($data['software_requires_php'])) {
|
||||||
|
update_post_meta($product->get_id(), '_woonoow_software_requires_php', sanitize_text_field($data['software_requires_php']));
|
||||||
|
}
|
||||||
|
|
||||||
// Allow plugins to perform additional updates (Level 1 compatibility)
|
// Allow plugins to perform additional updates (Level 1 compatibility)
|
||||||
do_action('woonoow/product_updated', $product, $data, $request);
|
do_action('woonoow/product_updated', $product, $data, $request);
|
||||||
|
|
||||||
@@ -819,6 +848,10 @@ class ProductsController
|
|||||||
'permalink' => get_permalink($product->get_id()),
|
'permalink' => get_permalink($product->get_id()),
|
||||||
'date_created' => $product->get_date_created() ? $product->get_date_created()->date('Y-m-d H:i:s') : '',
|
'date_created' => $product->get_date_created() ? $product->get_date_created()->date('Y-m-d H:i:s') : '',
|
||||||
'date_modified' => $product->get_date_modified() ? $product->get_date_modified()->date('Y-m-d H:i:s') : '',
|
'date_modified' => $product->get_date_modified() ? $product->get_date_modified()->date('Y-m-d H:i:s') : '',
|
||||||
|
'software_enabled' => get_post_meta($product->get_id(), '_woonoow_software_enabled', true) === 'yes',
|
||||||
|
'software_slug' => get_post_meta($product->get_id(), '_woonoow_software_slug', true),
|
||||||
|
'software_current_version' => get_post_meta($product->get_id(), '_woonoow_software_current_version', true),
|
||||||
|
'software_wp_enabled' => get_post_meta($product->get_id(), '_woonoow_software_wp_enabled', true) === 'yes',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -877,6 +910,14 @@ class ProductsController
|
|||||||
$data['affiliate_enabled'] = get_post_meta($product->get_id(), '_woonoow_affiliate_enabled', true) === 'yes';
|
$data['affiliate_enabled'] = get_post_meta($product->get_id(), '_woonoow_affiliate_enabled', true) === 'yes';
|
||||||
$data['affiliate_commission_rate'] = get_post_meta($product->get_id(), '_woonoow_affiliate_commission_rate', true) ?: '';
|
$data['affiliate_commission_rate'] = get_post_meta($product->get_id(), '_woonoow_affiliate_commission_rate', true) ?: '';
|
||||||
|
|
||||||
|
// Software fields
|
||||||
|
$data['software_enabled'] = get_post_meta($product->get_id(), '_woonoow_software_enabled', true) === 'yes';
|
||||||
|
$data['software_slug'] = get_post_meta($product->get_id(), '_woonoow_software_slug', true) ?: '';
|
||||||
|
$data['software_wp_enabled'] = get_post_meta($product->get_id(), '_woonoow_software_wp_enabled', true) === 'yes';
|
||||||
|
$data['software_requires_wp'] = get_post_meta($product->get_id(), '_woonoow_software_requires_wp', true) ?: '';
|
||||||
|
$data['software_tested_wp'] = get_post_meta($product->get_id(), '_woonoow_software_tested_wp', true) ?: '';
|
||||||
|
$data['software_requires_php'] = get_post_meta($product->get_id(), '_woonoow_software_requires_php', true) ?: '';
|
||||||
|
|
||||||
// Images array (URLs) for frontend - featured + gallery
|
// Images array (URLs) for frontend - featured + gallery
|
||||||
$images = [];
|
$images = [];
|
||||||
$featured_image_id = $product->get_image_id();
|
$featured_image_id = $product->get_image_id();
|
||||||
@@ -1078,6 +1119,10 @@ class ProductsController
|
|||||||
'image_url' => $image_url,
|
'image_url' => $image_url,
|
||||||
'image' => $image_url, // For form compatibility
|
'image' => $image_url, // For form compatibility
|
||||||
'license_duration_days' => get_post_meta($variation->get_id(), '_license_duration_days', true) ?: '',
|
'license_duration_days' => get_post_meta($variation->get_id(), '_license_duration_days', true) ?: '',
|
||||||
|
'subscription_signup_fee' => get_post_meta($variation->get_id(), '_woonoow_subscription_signup_fee', true) ?: '',
|
||||||
|
'subscription_trial_days' => get_post_meta($variation->get_id(), '_woonoow_subscription_trial_days', true) ?: '',
|
||||||
|
'subscription_period' => get_post_meta($variation->get_id(), '_woonoow_subscription_period', true) ?: '',
|
||||||
|
'subscription_interval' => get_post_meta($variation->get_id(), '_woonoow_subscription_interval', true) ?: '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1214,6 +1259,20 @@ class ProductsController
|
|||||||
update_post_meta($saved_id, '_license_duration_days', self::sanitize_number($var_data['license_duration_days']));
|
update_post_meta($saved_id, '_license_duration_days', self::sanitize_number($var_data['license_duration_days']));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save variation-level subscription fields
|
||||||
|
if (isset($var_data['subscription_signup_fee'])) {
|
||||||
|
update_post_meta($saved_id, '_woonoow_subscription_signup_fee', self::sanitize_number($var_data['subscription_signup_fee']));
|
||||||
|
}
|
||||||
|
if (isset($var_data['subscription_trial_days'])) {
|
||||||
|
update_post_meta($saved_id, '_woonoow_subscription_trial_days', absint($var_data['subscription_trial_days']));
|
||||||
|
}
|
||||||
|
if (isset($var_data['subscription_period'])) {
|
||||||
|
update_post_meta($saved_id, '_woonoow_subscription_period', sanitize_key($var_data['subscription_period']));
|
||||||
|
}
|
||||||
|
if (isset($var_data['subscription_interval'])) {
|
||||||
|
update_post_meta($saved_id, '_woonoow_subscription_interval', absint($var_data['subscription_interval']));
|
||||||
|
}
|
||||||
|
|
||||||
// Manually save attributes using direct database insert
|
// Manually save attributes using direct database insert
|
||||||
if (!empty($wc_attributes)) {
|
if (!empty($wc_attributes)) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|||||||
@@ -90,6 +90,15 @@ class SoftwareController
|
|||||||
return current_user_can('manage_woocommerce');
|
return current_user_can('manage_woocommerce');
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Edit version
|
||||||
|
register_rest_route($namespace, '/software/products/(?P<product_id>\d+)/versions/(?P<version_id>\d+)', [
|
||||||
|
'methods' => 'PUT',
|
||||||
|
'callback' => [__CLASS__, 'edit_version'],
|
||||||
|
'permission_callback' => function () {
|
||||||
|
return current_user_can('manage_woocommerce');
|
||||||
|
},
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -127,7 +136,7 @@ class SoftwareController
|
|||||||
// Log the check (optional - for analytics)
|
// Log the check (optional - for analytics)
|
||||||
do_action('woonoow/software/update_check', $slug, $current_version, $site_url, $license_key);
|
do_action('woonoow/software/update_check', $slug, $current_version, $site_url, $license_key);
|
||||||
|
|
||||||
$result = SoftwareManager::check_update($license_key, $slug, $current_version);
|
$result = SoftwareManager::check_update($license_key, $slug, $current_version, $site_url);
|
||||||
|
|
||||||
$status_code = isset($result['success']) && $result['success'] === false ? 400 : 200;
|
$status_code = isset($result['success']) && $result['success'] === false ? 400 : 200;
|
||||||
|
|
||||||
@@ -242,7 +251,7 @@ class SoftwareController
|
|||||||
return [
|
return [
|
||||||
'version' => $v['version'],
|
'version' => $v['version'],
|
||||||
'release_date' => $v['release_date'],
|
'release_date' => $v['release_date'],
|
||||||
'changelog' => $v['changelog'],
|
'changelog' => is_string($v['changelog']) && strpos(trim($v['changelog']), '{') === 0 ? json_decode($v['changelog'], true) : $v['changelog'],
|
||||||
'download_count' => (int) $v['download_count'],
|
'download_count' => (int) $v['download_count'],
|
||||||
];
|
];
|
||||||
}, $versions),
|
}, $versions),
|
||||||
@@ -283,9 +292,30 @@ class SoftwareController
|
|||||||
$params = $request->get_json_params();
|
$params = $request->get_json_params();
|
||||||
|
|
||||||
$version = sanitize_text_field($params['version'] ?? '');
|
$version = sanitize_text_field($params['version'] ?? '');
|
||||||
$changelog = wp_kses_post($params['changelog'] ?? '');
|
|
||||||
$set_current = (bool) ($params['set_current'] ?? true);
|
$set_current = (bool) ($params['set_current'] ?? true);
|
||||||
|
|
||||||
|
$raw_changelog = $params['changelog'] ?? [];
|
||||||
|
$changelog_data = [];
|
||||||
|
if (is_array($raw_changelog)) {
|
||||||
|
$changelog_data = [
|
||||||
|
'narrative' => wp_kses_post($raw_changelog['narrative'] ?? ''),
|
||||||
|
'points' => []
|
||||||
|
];
|
||||||
|
if (isset($raw_changelog['points']) && is_array($raw_changelog['points'])) {
|
||||||
|
foreach ($raw_changelog['points'] as $pt) {
|
||||||
|
if (isset($pt['type']) && isset($pt['text'])) {
|
||||||
|
$changelog_data['points'][] = [
|
||||||
|
'type' => sanitize_text_field($pt['type']),
|
||||||
|
'text' => sanitize_text_field($pt['text']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$changelog_data = ['narrative' => sanitize_textarea_field($raw_changelog), 'points' => []];
|
||||||
|
}
|
||||||
|
$changelog = wp_json_encode($changelog_data);
|
||||||
|
|
||||||
if (empty($version)) {
|
if (empty($version)) {
|
||||||
return new WP_REST_Response([
|
return new WP_REST_Response([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
@@ -318,4 +348,62 @@ class SoftwareController
|
|||||||
'message' => __('Version added successfully', 'woonoow'),
|
'message' => __('Version added successfully', 'woonoow'),
|
||||||
], 201);
|
], 201);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Edit an existing version
|
||||||
|
*/
|
||||||
|
public static function edit_version(WP_REST_Request $request)
|
||||||
|
{
|
||||||
|
$product_id = $request->get_param('product_id');
|
||||||
|
$version_id = $request->get_param('version_id');
|
||||||
|
$version = sanitize_text_field($request->get_param('version'));
|
||||||
|
$changelog = $request->get_param('changelog');
|
||||||
|
$set_current = filter_var($request->get_param('set_current'), FILTER_VALIDATE_BOOLEAN);
|
||||||
|
|
||||||
|
// If changelog is an array/object, encode it to JSON
|
||||||
|
if (is_array($changelog) || is_object($changelog)) {
|
||||||
|
if (isset($changelog['narrative'])) {
|
||||||
|
$changelog['narrative'] = sanitize_textarea_field($changelog['narrative']);
|
||||||
|
}
|
||||||
|
if (isset($changelog['points']) && is_array($changelog['points'])) {
|
||||||
|
foreach ($changelog['points'] as &$point) {
|
||||||
|
$point['type'] = sanitize_text_field($point['type'] ?? '');
|
||||||
|
$point['text'] = sanitize_text_field($point['text'] ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$changelog = wp_json_encode($changelog);
|
||||||
|
} else {
|
||||||
|
$changelog = wp_kses_post($changelog);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($version)) {
|
||||||
|
return new WP_REST_Response([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'missing_version',
|
||||||
|
'message' => __('Version number is required', 'woonoow'),
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$product = wc_get_product($product_id);
|
||||||
|
if (!$product) {
|
||||||
|
return new WP_REST_Response([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'product_not_found',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = SoftwareManager::update_version($version_id, $product_id, $version, $changelog, $set_current);
|
||||||
|
|
||||||
|
if (is_wp_error($result)) {
|
||||||
|
return new WP_REST_Response([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $result->get_error_code(),
|
||||||
|
'message' => $result->get_error_message(),
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WP_REST_Response([
|
||||||
|
'success' => true,
|
||||||
|
'message' => __('Version updated successfully', 'woonoow'),
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -608,7 +608,7 @@ class LicenseManager
|
|||||||
/**
|
/**
|
||||||
* Validate license (check if valid without activating)
|
* Validate license (check if valid without activating)
|
||||||
*/
|
*/
|
||||||
public static function validate($license_key)
|
public static function validate($license_key, $domain = null)
|
||||||
{
|
{
|
||||||
$license = self::get_license_by_key($license_key);
|
$license = self::get_license_by_key($license_key);
|
||||||
|
|
||||||
@@ -626,8 +626,31 @@ class LicenseManager
|
|||||||
$subscription_status = self::get_order_subscription_status($license['order_id']);
|
$subscription_status = self::get_order_subscription_status($license['order_id']);
|
||||||
$is_subscription_valid = $subscription_status === null || in_array($subscription_status, ['active', 'pending-cancel']);
|
$is_subscription_valid = $subscription_status === null || in_array($subscription_status, ['active', 'pending-cancel']);
|
||||||
|
|
||||||
|
// Check domain activation if domain is provided
|
||||||
|
$is_domain_active = true;
|
||||||
|
if ($domain) {
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . self::$activations_table;
|
||||||
|
$activation = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT id FROM $table WHERE license_id = %d AND domain = %s AND status = 'active' LIMIT 1",
|
||||||
|
$license['id'],
|
||||||
|
$domain
|
||||||
|
));
|
||||||
|
if (!$activation) {
|
||||||
|
$is_domain_active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($domain && !$is_domain_active) {
|
||||||
|
return [
|
||||||
|
'valid' => false,
|
||||||
|
'error' => 'domain_not_activated',
|
||||||
|
'message' => __('License is not activated for this domain', 'woonoow'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'valid' => $license['status'] === 'active' && !$is_expired && $is_subscription_valid,
|
'valid' => $license['status'] === 'active' && !$is_expired && $is_subscription_valid && $is_domain_active,
|
||||||
'license_key' => $license['license_key'],
|
'license_key' => $license['license_key'],
|
||||||
'status' => $license['status'],
|
'status' => $license['status'],
|
||||||
'activation_limit' => (int) $license['activation_limit'],
|
'activation_limit' => (int) $license['activation_limit'],
|
||||||
@@ -639,6 +662,7 @@ class LicenseManager
|
|||||||
'is_expired' => $is_expired,
|
'is_expired' => $is_expired,
|
||||||
'subscription_status' => $subscription_status,
|
'subscription_status' => $subscription_status,
|
||||||
'subscription_active' => $is_subscription_valid,
|
'subscription_active' => $is_subscription_valid,
|
||||||
|
'domain_active' => $is_domain_active,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,10 +127,10 @@ class SoftwareManager
|
|||||||
/**
|
/**
|
||||||
* Check for updates
|
* Check for updates
|
||||||
*/
|
*/
|
||||||
public static function check_update($license_key, $slug, $current_version)
|
public static function check_update($license_key, $slug, $current_version, $site_url = null)
|
||||||
{
|
{
|
||||||
// Validate license
|
// Validate license
|
||||||
$license_validation = LicenseManager::validate($license_key);
|
$license_validation = LicenseManager::validate($license_key, $site_url);
|
||||||
|
|
||||||
if (!$license_validation['valid']) {
|
if (!$license_validation['valid']) {
|
||||||
return [
|
return [
|
||||||
@@ -255,10 +255,21 @@ class SoftwareManager
|
|||||||
global $wpdb;
|
global $wpdb;
|
||||||
$table = $wpdb->prefix . self::$versions_table;
|
$table = $wpdb->prefix . self::$versions_table;
|
||||||
|
|
||||||
return $wpdb->get_results($wpdb->prepare(
|
$results = $wpdb->get_results($wpdb->prepare(
|
||||||
"SELECT * FROM $table WHERE product_id = %d ORDER BY release_date DESC",
|
"SELECT * FROM $table WHERE product_id = %d ORDER BY release_date DESC",
|
||||||
$product_id
|
$product_id
|
||||||
), ARRAY_A);
|
), ARRAY_A);
|
||||||
|
|
||||||
|
if ($results) {
|
||||||
|
foreach ($results as &$row) {
|
||||||
|
$decoded_changelog = json_decode($row['changelog'], true);
|
||||||
|
if (json_last_error() === JSON_ERROR_NONE) {
|
||||||
|
$row['changelog'] = $decoded_changelog;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -308,6 +319,39 @@ class SoftwareManager
|
|||||||
return $wpdb->insert_id;
|
return $wpdb->insert_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a version
|
||||||
|
*/
|
||||||
|
public static function update_version($version_id, $product_id, $version, $changelog = '', $set_current = false)
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . self::$versions_table;
|
||||||
|
|
||||||
|
// Reset other current versions if this one is set to current
|
||||||
|
if ($set_current) {
|
||||||
|
$wpdb->update($table, ['is_current' => 0], ['product_id' => $product_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update version
|
||||||
|
$wpdb->update($table, [
|
||||||
|
'version' => $version,
|
||||||
|
'changelog' => $changelog,
|
||||||
|
'is_current' => $set_current ? 1 : 0,
|
||||||
|
], [
|
||||||
|
'id' => $version_id,
|
||||||
|
'product_id' => $product_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update product meta
|
||||||
|
if ($set_current) {
|
||||||
|
update_post_meta($product_id, '_woonoow_software_current_version', $version);
|
||||||
|
}
|
||||||
|
|
||||||
|
do_action('woonoow/software/version_updated', $version_id, $product_id, $version);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate secure download token
|
* Generate secure download token
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ class SubscriptionModule
|
|||||||
add_action('deleted_post', [__CLASS__, 'on_post_deleted']);
|
add_action('deleted_post', [__CLASS__, 'on_post_deleted']);
|
||||||
add_action('delete_user', [__CLASS__, 'on_user_deleted']);
|
add_action('delete_user', [__CLASS__, 'on_user_deleted']);
|
||||||
|
|
||||||
|
// Prevent guest checkout for subscriptions
|
||||||
|
add_filter('woocommerce_add_to_cart_validation', [__CLASS__, 'validate_subscription_add_to_cart'], 10, 3);
|
||||||
|
|
||||||
// Modify add to cart button text for subscription products
|
// Modify add to cart button text for subscription products
|
||||||
add_filter('woocommerce_product_single_add_to_cart_text', [__CLASS__, 'subscription_add_to_cart_text'], 10, 2);
|
add_filter('woocommerce_product_single_add_to_cart_text', [__CLASS__, 'subscription_add_to_cart_text'], 10, 2);
|
||||||
add_filter('woocommerce_product_add_to_cart_text', [__CLASS__, 'subscription_add_to_cart_text'], 10, 2);
|
add_filter('woocommerce_product_add_to_cart_text', [__CLASS__, 'subscription_add_to_cart_text'], 10, 2);
|
||||||
@@ -271,6 +274,24 @@ class SubscriptionModule
|
|||||||
$order->save();
|
$order->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent guest checkout for subscriptions
|
||||||
|
*/
|
||||||
|
public static function validate_subscription_add_to_cart($passed, $product_id, $quantity) {
|
||||||
|
if (!ModuleRegistry::is_enabled('subscription')) {
|
||||||
|
return $passed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get_post_meta($product_id, '_woonoow_subscription_enabled', true) === 'yes') {
|
||||||
|
if (!is_user_logged_in()) {
|
||||||
|
wc_add_notice(__('You must be logged in to purchase a subscription.', 'woonoow'), 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $passed;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modify add to cart button text for subscription products
|
* Modify add to cart button text for subscription products
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user