@moontra/moonui-pro 2.0.22 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +215 -214
- package/package.json +4 -2
- package/src/__tests__/use-intersection-observer.test.tsx +216 -0
- package/src/__tests__/use-local-storage.test.tsx +174 -0
- package/src/__tests__/use-pro-access.test.tsx +183 -0
- package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
- package/src/components/advanced-chart/index.tsx +412 -0
- package/src/components/advanced-forms/index.tsx +431 -0
- package/src/components/animated-button/index.tsx +202 -0
- package/src/components/calendar/event-dialog.tsx +372 -0
- package/src/components/calendar/index.tsx +557 -0
- package/src/components/color-picker/index.tsx +434 -0
- package/src/components/dashboard/index.tsx +334 -0
- package/src/components/data-table/data-table.test.tsx +187 -0
- package/src/components/data-table/index.tsx +368 -0
- package/src/components/draggable-list/index.tsx +100 -0
- package/src/components/enhanced/button.tsx +360 -0
- package/src/components/enhanced/card.tsx +272 -0
- package/src/components/enhanced/dialog.tsx +248 -0
- package/src/components/enhanced/index.ts +3 -0
- package/src/components/error-boundary/index.tsx +111 -0
- package/src/components/file-upload/file-upload.test.tsx +242 -0
- package/src/components/file-upload/index.tsx +362 -0
- package/src/components/floating-action-button/index.tsx +209 -0
- package/src/components/github-stars/index.tsx +414 -0
- package/src/components/health-check/index.tsx +441 -0
- package/src/components/hover-card-3d/index.tsx +170 -0
- package/src/components/index.ts +76 -0
- package/src/components/kanban/index.tsx +436 -0
- package/src/components/lazy-component/index.tsx +342 -0
- package/src/components/magnetic-button/index.tsx +170 -0
- package/src/components/memory-efficient-data/index.tsx +352 -0
- package/src/components/optimized-image/index.tsx +427 -0
- package/src/components/performance-debugger/index.tsx +591 -0
- package/src/components/performance-monitor/index.tsx +775 -0
- package/src/components/pinch-zoom/index.tsx +172 -0
- package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
- package/src/components/rich-text-editor/index.tsx +1537 -0
- package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
- package/src/components/rich-text-editor/slash-commands.css +35 -0
- package/src/components/rich-text-editor/table-styles.css +65 -0
- package/src/components/spotlight-card/index.tsx +194 -0
- package/src/components/swipeable-card/index.tsx +100 -0
- package/src/components/timeline/index.tsx +333 -0
- package/src/components/ui/animated-button.tsx +185 -0
- package/src/components/ui/avatar.tsx +135 -0
- package/src/components/ui/badge.tsx +225 -0
- package/src/components/ui/button.tsx +221 -0
- package/src/components/ui/card.tsx +141 -0
- package/src/components/ui/checkbox.tsx +256 -0
- package/src/components/ui/color-picker.tsx +95 -0
- package/src/components/ui/dialog.tsx +332 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/hover-card-3d.tsx +103 -0
- package/src/components/ui/index.ts +33 -0
- package/src/components/ui/input.tsx +219 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/magnetic-button.tsx +129 -0
- package/src/components/ui/popover.tsx +183 -0
- package/src/components/ui/select.tsx +273 -0
- package/src/components/ui/separator.tsx +140 -0
- package/src/components/ui/slider.tsx +351 -0
- package/src/components/ui/spotlight-card.tsx +119 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/tabs.tsx +195 -0
- package/src/components/ui/textarea.tsx +25 -0
- package/src/components/ui/toast.tsx +313 -0
- package/src/components/ui/tooltip.tsx +152 -0
- package/src/components/virtual-list/index.tsx +369 -0
- package/src/hooks/use-chart.ts +205 -0
- package/src/hooks/use-data-table.ts +182 -0
- package/src/hooks/use-docs-pro-access.ts +13 -0
- package/src/hooks/use-license-check.ts +65 -0
- package/src/hooks/use-subscription.ts +19 -0
- package/src/index.ts +14 -0
- package/src/lib/micro-interactions.ts +255 -0
- package/src/lib/utils.ts +6 -0
- package/src/patterns/login-form/index.tsx +276 -0
- package/src/patterns/login-form/types.ts +67 -0
- package/src/setupTests.ts +41 -0
- package/src/styles/design-system.css +365 -0
- package/src/styles/index.css +4 -0
- package/src/styles/tailwind.css +6 -0
- package/src/styles/tokens.css +453 -0
- package/src/types/moonui.d.ts +22 -0
- package/src/use-intersection-observer.tsx +154 -0
- package/src/use-local-storage.tsx +71 -0
- package/src/use-paddle.ts +138 -0
- package/src/use-performance-optimizer.ts +379 -0
- package/src/use-pro-access.ts +141 -0
- package/src/use-scroll-animation.ts +221 -0
- package/src/use-subscription.ts +37 -0
- package/src/use-toast.ts +32 -0
- package/src/utils/chart-helpers.ts +257 -0
- package/src/utils/cn.ts +69 -0
- package/src/utils/data-processing.ts +151 -0
- package/src/utils/license-guard.tsx +177 -0
- package/src/utils/license-validator.tsx +183 -0
- package/src/utils/package-guard.ts +60 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
export interface DataProcessor<T = any> {
|
|
2
|
+
filter: (data: T[], predicate: (item: T) => boolean) => T[]
|
|
3
|
+
sort: (data: T[], key: keyof T, direction?: 'asc' | 'desc') => T[]
|
|
4
|
+
group: (data: T[], key: keyof T) => Record<string, T[]>
|
|
5
|
+
aggregate: (data: T[], key: keyof T, operation: 'sum' | 'avg' | 'min' | 'max' | 'count') => number
|
|
6
|
+
paginate: (data: T[], page: number, pageSize: number) => T[]
|
|
7
|
+
search: (data: T[], query: string, searchKeys?: (keyof T)[]) => T[]
|
|
8
|
+
transform: <U>(data: T[], transformer: (item: T) => U) => U[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createDataProcessor<T = any>(): DataProcessor<T> {
|
|
12
|
+
return {
|
|
13
|
+
filter: (data: T[], predicate: (item: T) => boolean) => {
|
|
14
|
+
return data.filter(predicate)
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
sort: (data: T[], key: keyof T, direction: 'asc' | 'desc' = 'asc') => {
|
|
18
|
+
return [...data].sort((a, b) => {
|
|
19
|
+
const aVal = a[key]
|
|
20
|
+
const bVal = b[key]
|
|
21
|
+
|
|
22
|
+
if (aVal === bVal) return 0
|
|
23
|
+
|
|
24
|
+
const comparison = aVal < bVal ? -1 : 1
|
|
25
|
+
return direction === 'asc' ? comparison : -comparison
|
|
26
|
+
})
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
group: (data: T[], key: keyof T) => {
|
|
30
|
+
return data.reduce((groups, item) => {
|
|
31
|
+
const groupKey = String(item[key])
|
|
32
|
+
if (!groups[groupKey]) {
|
|
33
|
+
groups[groupKey] = []
|
|
34
|
+
}
|
|
35
|
+
groups[groupKey].push(item)
|
|
36
|
+
return groups
|
|
37
|
+
}, {} as Record<string, T[]>)
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
aggregate: (data: T[], key: keyof T, operation: 'sum' | 'avg' | 'min' | 'max' | 'count') => {
|
|
41
|
+
const values = data.map(item => Number(item[key])).filter(val => !isNaN(val))
|
|
42
|
+
|
|
43
|
+
if (values.length === 0) return 0
|
|
44
|
+
|
|
45
|
+
switch (operation) {
|
|
46
|
+
case 'sum':
|
|
47
|
+
return values.reduce((sum, val) => sum + val, 0)
|
|
48
|
+
case 'avg':
|
|
49
|
+
return values.reduce((sum, val) => sum + val, 0) / values.length
|
|
50
|
+
case 'min':
|
|
51
|
+
return Math.min(...values)
|
|
52
|
+
case 'max':
|
|
53
|
+
return Math.max(...values)
|
|
54
|
+
case 'count':
|
|
55
|
+
return values.length
|
|
56
|
+
default:
|
|
57
|
+
return 0
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
paginate: (data: T[], page: number, pageSize: number) => {
|
|
62
|
+
const startIndex = (page - 1) * pageSize
|
|
63
|
+
const endIndex = startIndex + pageSize
|
|
64
|
+
return data.slice(startIndex, endIndex)
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
search: (data: T[], query: string, searchKeys?: (keyof T)[]) => {
|
|
68
|
+
if (!query.trim()) return data
|
|
69
|
+
|
|
70
|
+
const lowerQuery = query.toLowerCase()
|
|
71
|
+
|
|
72
|
+
return data.filter(item => {
|
|
73
|
+
const keysToSearch = searchKeys || Object.keys(item as Record<string, any>) as (keyof T)[]
|
|
74
|
+
|
|
75
|
+
return keysToSearch.some(key => {
|
|
76
|
+
const value = item[key]
|
|
77
|
+
if (value == null) return false
|
|
78
|
+
return String(value).toLowerCase().includes(lowerQuery)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
transform: <U>(data: T[], transformer: (item: T) => U) => {
|
|
84
|
+
return data.map(transformer)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Utility functions for common data operations
|
|
90
|
+
export function calculatePercentageChange(current: number, previous: number): number {
|
|
91
|
+
if (previous === 0) return current > 0 ? 100 : 0
|
|
92
|
+
return ((current - previous) / previous) * 100
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function formatNumber(value: number, options?: {
|
|
96
|
+
decimals?: number
|
|
97
|
+
currency?: string
|
|
98
|
+
percentage?: boolean
|
|
99
|
+
compact?: boolean
|
|
100
|
+
}): string {
|
|
101
|
+
const { decimals = 2, currency, percentage = false, compact = false } = options || {}
|
|
102
|
+
|
|
103
|
+
if (percentage) {
|
|
104
|
+
return `${value.toFixed(decimals)}%`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (currency) {
|
|
108
|
+
return new Intl.NumberFormat('en-US', {
|
|
109
|
+
style: 'currency',
|
|
110
|
+
currency,
|
|
111
|
+
minimumFractionDigits: decimals,
|
|
112
|
+
maximumFractionDigits: decimals,
|
|
113
|
+
}).format(value)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (compact && Math.abs(value) >= 1000) {
|
|
117
|
+
return new Intl.NumberFormat('en-US', {
|
|
118
|
+
notation: 'compact',
|
|
119
|
+
minimumFractionDigits: 0,
|
|
120
|
+
maximumFractionDigits: 1,
|
|
121
|
+
}).format(value)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return value.toFixed(decimals)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function generateDateRange(start: Date, end: Date, interval: 'day' | 'week' | 'month' = 'day'): Date[] {
|
|
128
|
+
const dates: Date[] = []
|
|
129
|
+
const current = new Date(start)
|
|
130
|
+
|
|
131
|
+
while (current <= end) {
|
|
132
|
+
dates.push(new Date(current))
|
|
133
|
+
|
|
134
|
+
switch (interval) {
|
|
135
|
+
case 'day':
|
|
136
|
+
current.setDate(current.getDate() + 1)
|
|
137
|
+
break
|
|
138
|
+
case 'week':
|
|
139
|
+
current.setDate(current.getDate() + 7)
|
|
140
|
+
break
|
|
141
|
+
case 'month':
|
|
142
|
+
current.setMonth(current.getMonth() + 1)
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return dates
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// debounce function removed to avoid conflicts with @moontra/moonui
|
|
151
|
+
// Use debounce from @moontra/moonui instead
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
const API_BASE = process.env.NEXT_PUBLIC_MOONUI_API || 'https://moonui.dev';
|
|
6
|
+
|
|
7
|
+
interface LicenseCheckResult {
|
|
8
|
+
valid: boolean;
|
|
9
|
+
error?: string;
|
|
10
|
+
loading: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// In-memory cache for license check
|
|
14
|
+
let licenseCache: {
|
|
15
|
+
result: boolean;
|
|
16
|
+
timestamp: number;
|
|
17
|
+
} | null = null;
|
|
18
|
+
|
|
19
|
+
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
|
20
|
+
|
|
21
|
+
export function useLicenseCheck(): LicenseCheckResult {
|
|
22
|
+
const [state, setState] = useState<LicenseCheckResult>({
|
|
23
|
+
valid: false,
|
|
24
|
+
loading: true,
|
|
25
|
+
error: undefined
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
checkLicense().then(result => {
|
|
30
|
+
setState({
|
|
31
|
+
valid: result.valid,
|
|
32
|
+
loading: false,
|
|
33
|
+
error: result.error
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
return state;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function checkLicense(): Promise<{ valid: boolean; error?: string }> {
|
|
42
|
+
// Check cache first
|
|
43
|
+
if (licenseCache && Date.now() - licenseCache.timestamp < CACHE_DURATION) {
|
|
44
|
+
return { valid: licenseCache.result };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Get auth token from localStorage or cookie
|
|
49
|
+
const token = getAuthToken();
|
|
50
|
+
|
|
51
|
+
if (!token) {
|
|
52
|
+
return { valid: false, error: 'No authentication token found' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const response = await fetch(`${API_BASE}/api/license/verify`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
'Authorization': `Bearer ${token}`,
|
|
59
|
+
'Content-Type': 'application/json'
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
component: 'runtime-check',
|
|
63
|
+
source: 'moonui-pro'
|
|
64
|
+
})
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
return { valid: false, error: 'License verification failed' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const data = await response.json();
|
|
72
|
+
|
|
73
|
+
// Cache the result
|
|
74
|
+
licenseCache = {
|
|
75
|
+
result: data.valid && data.planType !== 'free',
|
|
76
|
+
timestamp: Date.now()
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return { valid: licenseCache.result };
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return { valid: false, error: 'Network error during license check' };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getAuthToken(): string | null {
|
|
86
|
+
// First check localStorage (for client-side)
|
|
87
|
+
if (typeof window !== 'undefined') {
|
|
88
|
+
const stored = localStorage.getItem('moonui_auth');
|
|
89
|
+
if (stored) {
|
|
90
|
+
try {
|
|
91
|
+
const auth = JSON.parse(stored);
|
|
92
|
+
return auth.accessToken;
|
|
93
|
+
} catch {
|
|
94
|
+
// Invalid JSON
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check cookies (for SSR)
|
|
100
|
+
if (typeof document !== 'undefined') {
|
|
101
|
+
const cookies = document.cookie.split(';');
|
|
102
|
+
for (const cookie of cookies) {
|
|
103
|
+
const [name, value] = cookie.trim().split('=');
|
|
104
|
+
if (name === 'moonui_token') {
|
|
105
|
+
return decodeURIComponent(value);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// License Guard HOC
|
|
114
|
+
export function withLicenseGuard<P extends object>(
|
|
115
|
+
Component: React.ComponentType<P>,
|
|
116
|
+
options?: {
|
|
117
|
+
fallback?: React.ReactNode;
|
|
118
|
+
onUnauthorized?: () => void;
|
|
119
|
+
}
|
|
120
|
+
) {
|
|
121
|
+
return function LicenseGuardedComponent(props: P) {
|
|
122
|
+
const { valid, loading, error } = useLicenseCheck();
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!loading && !valid && options?.onUnauthorized) {
|
|
126
|
+
options.onUnauthorized();
|
|
127
|
+
}
|
|
128
|
+
}, [loading, valid]);
|
|
129
|
+
|
|
130
|
+
if (loading) {
|
|
131
|
+
return (
|
|
132
|
+
<div className="flex items-center justify-center p-8">
|
|
133
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!valid) {
|
|
139
|
+
if (options?.fallback) {
|
|
140
|
+
return <>{options.fallback}</>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div className="rounded-lg border border-red-200 bg-red-50 p-8 text-center">
|
|
145
|
+
<h3 className="text-lg font-semibold text-red-900 mb-2">
|
|
146
|
+
Pro License Required
|
|
147
|
+
</h3>
|
|
148
|
+
<p className="text-red-700 mb-4">
|
|
149
|
+
This component requires a MoonUI Pro license.
|
|
150
|
+
</p>
|
|
151
|
+
<a
|
|
152
|
+
href="https://moonui.dev/pricing"
|
|
153
|
+
className="inline-flex items-center justify-center rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
|
|
154
|
+
>
|
|
155
|
+
Get Pro License
|
|
156
|
+
</a>
|
|
157
|
+
{error && (
|
|
158
|
+
<p className="text-xs text-red-600 mt-4">
|
|
159
|
+
Error: {error}
|
|
160
|
+
</p>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return <Component {...props} />;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Self-protecting component wrapper
|
|
171
|
+
export function ProComponent<P extends object>({
|
|
172
|
+
component: Component,
|
|
173
|
+
...props
|
|
174
|
+
}: P & { component: React.ComponentType<P> }) {
|
|
175
|
+
const GuardedComponent = withLicenseGuard(Component);
|
|
176
|
+
return <GuardedComponent {...(props as P)} />;
|
|
177
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* License Validation System for MoonUI Pro
|
|
3
|
+
* This ensures that only licensed users can access pro components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface LicenseData {
|
|
7
|
+
key: string;
|
|
8
|
+
email: string;
|
|
9
|
+
expiresAt?: string;
|
|
10
|
+
features?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class LicenseValidator {
|
|
14
|
+
private static instance: LicenseValidator;
|
|
15
|
+
private licenseData: LicenseData | null = null;
|
|
16
|
+
private validationCache: Map<string, boolean> = new Map();
|
|
17
|
+
private readonly VALIDATION_ENDPOINT = 'https://api.moonui.dev/v1/license/validate';
|
|
18
|
+
|
|
19
|
+
private constructor() {
|
|
20
|
+
// Initialize license from environment or stored data
|
|
21
|
+
this.initializeLicense();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static getInstance(): LicenseValidator {
|
|
25
|
+
if (!LicenseValidator.instance) {
|
|
26
|
+
LicenseValidator.instance = new LicenseValidator();
|
|
27
|
+
}
|
|
28
|
+
return LicenseValidator.instance;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private initializeLicense(): void {
|
|
32
|
+
// Check environment variables
|
|
33
|
+
const licenseKey = process.env.MOONUI_LICENSE_KEY;
|
|
34
|
+
const licenseEmail = process.env.MOONUI_LICENSE_EMAIL;
|
|
35
|
+
|
|
36
|
+
if (licenseKey && licenseEmail) {
|
|
37
|
+
this.licenseData = {
|
|
38
|
+
key: licenseKey,
|
|
39
|
+
email: licenseEmail
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check for license file
|
|
44
|
+
try {
|
|
45
|
+
const fs = require('fs');
|
|
46
|
+
const path = require('path');
|
|
47
|
+
const licensePath = path.join(process.cwd(), '.moonui', 'license.json');
|
|
48
|
+
|
|
49
|
+
if (fs.existsSync(licensePath)) {
|
|
50
|
+
const fileData = fs.readFileSync(licensePath, 'utf-8');
|
|
51
|
+
this.licenseData = JSON.parse(fileData);
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// Ignore file reading errors in browser environment
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async validateLicense(): Promise<boolean> {
|
|
59
|
+
if (!this.licenseData) {
|
|
60
|
+
console.warn('[MoonUI Pro] No license found. Please run "moonui add <pro-component>" to activate your license.');
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check cache first
|
|
65
|
+
const cacheKey = `${this.licenseData.key}-${this.licenseData.email}`;
|
|
66
|
+
if (this.validationCache.has(cacheKey)) {
|
|
67
|
+
return this.validationCache.get(cacheKey)!;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check expiration date if available
|
|
71
|
+
if (this.licenseData.expiresAt) {
|
|
72
|
+
const expiryDate = new Date(this.licenseData.expiresAt);
|
|
73
|
+
if (expiryDate < new Date()) {
|
|
74
|
+
console.error('[MoonUI Pro] License has expired. Please renew your subscription.');
|
|
75
|
+
this.validationCache.set(cacheKey, false);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Validate with server (with fallback for offline mode)
|
|
81
|
+
try {
|
|
82
|
+
const response = await fetch(this.VALIDATION_ENDPOINT, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: {
|
|
85
|
+
'Content-Type': 'application/json',
|
|
86
|
+
},
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
key: this.licenseData.key,
|
|
89
|
+
email: this.licenseData.email,
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (response.ok) {
|
|
94
|
+
const result = await response.json();
|
|
95
|
+
const isValid = result.valid === true;
|
|
96
|
+
this.validationCache.set(cacheKey, isValid);
|
|
97
|
+
|
|
98
|
+
if (!isValid) {
|
|
99
|
+
console.error('[MoonUI Pro] Invalid license. Please check your license key and email.');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return isValid;
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// Fallback for offline mode or network errors
|
|
106
|
+
// Allow usage if license format is valid
|
|
107
|
+
const isValidFormat = this.isValidLicenseFormat();
|
|
108
|
+
this.validationCache.set(cacheKey, isValidFormat);
|
|
109
|
+
return isValidFormat;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private isValidLicenseFormat(): boolean {
|
|
116
|
+
if (!this.licenseData) return false;
|
|
117
|
+
|
|
118
|
+
// Basic format validation
|
|
119
|
+
const { key, email } = this.licenseData;
|
|
120
|
+
return (
|
|
121
|
+
typeof key === 'string' &&
|
|
122
|
+
key.length >= 32 &&
|
|
123
|
+
typeof email === 'string' &&
|
|
124
|
+
email.includes('@')
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
hasFeature(feature: string): boolean {
|
|
129
|
+
if (!this.licenseData || !this.licenseData.features) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return this.licenseData.features.includes(feature);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getLicenseInfo(): Readonly<LicenseData> | null {
|
|
136
|
+
return this.licenseData ? { ...this.licenseData } : null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Export singleton instance
|
|
141
|
+
export const licenseValidator = LicenseValidator.getInstance();
|
|
142
|
+
|
|
143
|
+
// Higher-order component for license protection
|
|
144
|
+
export function withLicenseProtection<P extends object>(
|
|
145
|
+
Component: React.ComponentType<P>,
|
|
146
|
+
componentName: string
|
|
147
|
+
): React.ComponentType<P> {
|
|
148
|
+
return (props: P) => {
|
|
149
|
+
const [isValid, setIsValid] = React.useState<boolean | null>(null);
|
|
150
|
+
|
|
151
|
+
React.useEffect(() => {
|
|
152
|
+
licenseValidator.validateLicense().then(setIsValid);
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
if (isValid === null) {
|
|
156
|
+
// Loading state
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!isValid) {
|
|
161
|
+
if (process.env.NODE_ENV === 'development') {
|
|
162
|
+
console.error(
|
|
163
|
+
`[MoonUI Pro] Component "${componentName}" requires a valid Pro license.\n` +
|
|
164
|
+
'Get your license at: https://moonui.dev/pricing'
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Return a placeholder in production
|
|
169
|
+
return (
|
|
170
|
+
<div className="rounded-lg border-2 border-dashed border-gray-300 p-4 text-center">
|
|
171
|
+
<p className="text-sm text-gray-500">
|
|
172
|
+
This component requires a MoonUI Pro license.
|
|
173
|
+
</p>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return <Component {...props} />;
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Export React for component usage
|
|
183
|
+
import * as React from 'react';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package Access Guard
|
|
3
|
+
* Prevents direct NPM package usage without CLI
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ALLOWED_CALLERS = [
|
|
7
|
+
'@moontra/moonui-cli',
|
|
8
|
+
'moonui-cli',
|
|
9
|
+
'.moonui/temp', // CLI temp directory
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const DEV_MODE = process.env.NODE_ENV === 'development';
|
|
13
|
+
|
|
14
|
+
export function checkPackageAccess(): void {
|
|
15
|
+
// Skip in development mode for easier testing
|
|
16
|
+
if (DEV_MODE) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check if running in a browser (client-side)
|
|
21
|
+
if (typeof window !== 'undefined') {
|
|
22
|
+
// Client-side access is allowed after server-side validation
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Server-side check
|
|
27
|
+
const stack = new Error().stack;
|
|
28
|
+
|
|
29
|
+
if (!stack) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'MoonUI Pro: This package can only be used via @moontra/moonui-cli. ' +
|
|
32
|
+
'Please install components using: npx @moontra/moonui-cli add <component>'
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check if called from allowed sources
|
|
37
|
+
const isAllowed = ALLOWED_CALLERS.some(caller => stack.includes(caller));
|
|
38
|
+
|
|
39
|
+
// Also check for local file paths that indicate CLI usage
|
|
40
|
+
const isCliGenerated = stack.includes('/components/ui/') ||
|
|
41
|
+
stack.includes('/components/pro/');
|
|
42
|
+
|
|
43
|
+
if (!isAllowed && !isCliGenerated) {
|
|
44
|
+
console.error('Stack trace:', stack);
|
|
45
|
+
throw new Error(
|
|
46
|
+
'MoonUI Pro: Unauthorized package access detected.\n' +
|
|
47
|
+
'This package can only be used via @moontra/moonui-cli.\n' +
|
|
48
|
+
'Install components using: npx @moontra/moonui-cli add <component>\n\n' +
|
|
49
|
+
'If you have a Pro license, make sure to:\n' +
|
|
50
|
+
'1. Login with: npx @moontra/moonui-cli login\n' +
|
|
51
|
+
'2. Install components via CLI\n\n' +
|
|
52
|
+
'Learn more at: https://moonui.dev/docs/installation'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Auto-check on import
|
|
58
|
+
if (typeof window === 'undefined') {
|
|
59
|
+
checkPackageAccess();
|
|
60
|
+
}
|