@mostajs/licensing-ui-react 0.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/README.md +31 -0
- package/llms.txt +43 -0
- package/package.json +38 -0
- package/src/index.ts +67 -0
- package/src/lib/license-service.ts +360 -0
- package/src/lib/licenseClient.ts +76 -0
- package/src/react/EnhancedLicenseGate.tsx +488 -0
- package/src/react/LicenseActivationForm.tsx +365 -0
- package/src/react/LicenseConfigForm.tsx +243 -0
- package/src/react/LicenseGate.tsx +504 -0
- package/src/react/LicenseProvider.tsx +36 -0
- package/src/react/PendingLicenseActivation.tsx +270 -0
- package/src/react/SimpleLicenseActivation.tsx +273 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState, useCallback } from "react";
|
|
3
|
+
import { Shield, CheckCircle, XCircle, AlertTriangle, Download, RefreshCw, Loader2 } from "lucide-react";
|
|
4
|
+
import { useLicenseUi } from "./LicenseProvider";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
project?: string;
|
|
9
|
+
version?: string;
|
|
10
|
+
gracePeriod?: number; // Jours de grâce offline
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface LicenseInfo {
|
|
14
|
+
valid: boolean;
|
|
15
|
+
reason?: string;
|
|
16
|
+
license?: {
|
|
17
|
+
key: string;
|
|
18
|
+
email: string;
|
|
19
|
+
project: string;
|
|
20
|
+
expires_at: string;
|
|
21
|
+
activations: number;
|
|
22
|
+
max_activations: number;
|
|
23
|
+
};
|
|
24
|
+
remaining_days?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default function LicenseGate({
|
|
28
|
+
children,
|
|
29
|
+
project: projectProp,
|
|
30
|
+
version = "1.0.0",
|
|
31
|
+
gracePeriod = 3
|
|
32
|
+
}: Props) {
|
|
33
|
+
const { serverUrl, project: ctxProject } = useLicenseUi(); // config .env (createLicenseUiFromEnv)
|
|
34
|
+
const project = projectProp ?? ctxProject ?? "app";
|
|
35
|
+
const [status, setStatus] = useState<"checking" | "valid" | "invalid" | "requesting" | "expired" | "warning">("checking");
|
|
36
|
+
const [email, setEmail] = useState("");
|
|
37
|
+
const [company, setCompany] = useState("");
|
|
38
|
+
const [error, setError] = useState<string | null>(null);
|
|
39
|
+
const [licenseInfo, setLicenseInfo] = useState<LicenseInfo | null>(null);
|
|
40
|
+
const [retryCount, setRetryCount] = useState(0);
|
|
41
|
+
const [isOffline, setIsOffline] = useState(false);
|
|
42
|
+
const [lastOnlineCheck, setLastOnlineCheck] = useState<Date | null>(null);
|
|
43
|
+
|
|
44
|
+
// Vérification de la connectivité
|
|
45
|
+
const checkConnectivity = useCallback(async () => {
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch("/api/license/ping", {
|
|
48
|
+
method: "GET",
|
|
49
|
+
cache: "no-cache"
|
|
50
|
+
});
|
|
51
|
+
return response.ok;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
// Vérification offline (cache local + période de grâce)
|
|
58
|
+
const checkOfflineLicense = useCallback(() => {
|
|
59
|
+
const offlineData = localStorage.getItem("license_cache");
|
|
60
|
+
if (!offlineData) return false;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const cached = JSON.parse(offlineData);
|
|
64
|
+
const cacheDate = new Date(cached.timestamp);
|
|
65
|
+
const now = new Date();
|
|
66
|
+
const diffDays = Math.floor((now.getTime() - cacheDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
67
|
+
|
|
68
|
+
if (diffDays <= gracePeriod && cached.valid) {
|
|
69
|
+
setLicenseInfo(cached.licenseInfo);
|
|
70
|
+
setLastOnlineCheck(cacheDate);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.warn("Cache licence corrompu :", e);
|
|
75
|
+
localStorage.removeItem("license_cache");
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}, [gracePeriod]);
|
|
79
|
+
|
|
80
|
+
const verifyLicense = useCallback(async (force = false) => {
|
|
81
|
+
try {
|
|
82
|
+
const isOnline = await checkConnectivity();
|
|
83
|
+
|
|
84
|
+
if (!isOnline && !force) {
|
|
85
|
+
setIsOffline(true);
|
|
86
|
+
if (checkOfflineLicense()) {
|
|
87
|
+
setStatus("warning"); // Mode offline avec licence valide
|
|
88
|
+
return;
|
|
89
|
+
} else {
|
|
90
|
+
setStatus("invalid");
|
|
91
|
+
setError("Aucune connexion et licence locale expirée");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setIsOffline(false);
|
|
97
|
+
|
|
98
|
+
// Essayer d'abord l'API Node.js
|
|
99
|
+
let response = await fetch("/api/license/verify");
|
|
100
|
+
let data: LicenseInfo = await response.json();
|
|
101
|
+
|
|
102
|
+
// Si la vérification Node.js échoue, essayer l'API Python
|
|
103
|
+
if (!data.valid && data.reason === "signature") {
|
|
104
|
+
console.log("🐍 Fallback vers vérification Python...");
|
|
105
|
+
try {
|
|
106
|
+
response = await fetch("/api/license/verify-python");
|
|
107
|
+
if (response.ok) {
|
|
108
|
+
data = await response.json();
|
|
109
|
+
console.log("✅ Vérification Python réussie:", data);
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.warn("⚠️ Fallback Python échoué:", error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
setLicenseInfo(data);
|
|
117
|
+
|
|
118
|
+
if (data.valid) {
|
|
119
|
+
setStatus("valid");
|
|
120
|
+
// Mise à jour du cache local
|
|
121
|
+
localStorage.setItem("license_cache", JSON.stringify({
|
|
122
|
+
licenseInfo: data,
|
|
123
|
+
timestamp: new Date().toISOString(),
|
|
124
|
+
valid: true
|
|
125
|
+
}));
|
|
126
|
+
setLastOnlineCheck(new Date());
|
|
127
|
+
} else {
|
|
128
|
+
setStatus("invalid");
|
|
129
|
+
localStorage.removeItem("license_cache");
|
|
130
|
+
|
|
131
|
+
if (data.reason === "expired") {
|
|
132
|
+
setStatus("expired");
|
|
133
|
+
setError(`Licence expirée depuis ${data.remaining_days ? Math.abs(data.remaining_days) : '?'} jours`);
|
|
134
|
+
} else if (data.reason === "signature") {
|
|
135
|
+
setError("Signature invalide - Fichier de licence corrompu");
|
|
136
|
+
} else if (data.reason === "not_found") {
|
|
137
|
+
setError("Aucune licence trouvée");
|
|
138
|
+
} else {
|
|
139
|
+
setError(data.reason || "Licence invalide");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.error("Erreur vérification licence :", e);
|
|
144
|
+
|
|
145
|
+
// Tentative offline en cas d'erreur réseau
|
|
146
|
+
if (checkOfflineLicense()) {
|
|
147
|
+
setStatus("warning");
|
|
148
|
+
setIsOffline(true);
|
|
149
|
+
} else {
|
|
150
|
+
setStatus("invalid");
|
|
151
|
+
setError("Erreur lors de la vérification de licence");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}, [checkConnectivity, checkOfflineLicense]);
|
|
155
|
+
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
verifyLicense();
|
|
158
|
+
|
|
159
|
+
// Vérification périodique (toutes les 24h)
|
|
160
|
+
const interval = setInterval(() => {
|
|
161
|
+
verifyLicense();
|
|
162
|
+
}, 24 * 60 * 60 * 1000);
|
|
163
|
+
|
|
164
|
+
return () => clearInterval(interval);
|
|
165
|
+
}, [verifyLicense]);
|
|
166
|
+
|
|
167
|
+
const requestLicense = async () => {
|
|
168
|
+
if (!email.trim()) {
|
|
169
|
+
setError("Email requis");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!/\S+@\S+\.\S+/.test(email)) {
|
|
174
|
+
setError("Format d'email invalide");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
setStatus("requesting");
|
|
179
|
+
setError(null);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const response = await fetch("/api/license/request", {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers: {
|
|
185
|
+
"Content-Type": "application/json",
|
|
186
|
+
},
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
email: email.trim(),
|
|
189
|
+
company: company.trim(),
|
|
190
|
+
project,
|
|
191
|
+
version
|
|
192
|
+
}),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const data = await response.json();
|
|
196
|
+
|
|
197
|
+
if (data.success) {
|
|
198
|
+
// Attendre un peu avant la vérification pour laisser le temps au serveur
|
|
199
|
+
setTimeout(() => {
|
|
200
|
+
verifyLicense(true);
|
|
201
|
+
}, 1000);
|
|
202
|
+
} else {
|
|
203
|
+
setError(data.error || "Erreur lors de la demande de licence");
|
|
204
|
+
setStatus("invalid");
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {
|
|
207
|
+
console.error("Erreur demande licence :", e);
|
|
208
|
+
setError("Erreur réseau lors de la demande de licence");
|
|
209
|
+
setStatus("invalid");
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const handleRetry = () => {
|
|
214
|
+
setRetryCount(prev => prev + 1);
|
|
215
|
+
setError(null);
|
|
216
|
+
verifyLicense(true);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const downloadLicense = () => {
|
|
220
|
+
// Trigger download of license file
|
|
221
|
+
const link = document.createElement('a');
|
|
222
|
+
link.href = '/api/license/download';
|
|
223
|
+
link.download = `${project}_licence.json`;
|
|
224
|
+
document.body.appendChild(link);
|
|
225
|
+
link.click();
|
|
226
|
+
document.body.removeChild(link);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// États d'affichage
|
|
230
|
+
if (status === "checking") {
|
|
231
|
+
return (
|
|
232
|
+
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
|
233
|
+
<div className="text-center">
|
|
234
|
+
<Loader2 className="w-8 h-8 animate-spin text-blue-500 mx-auto mb-4" />
|
|
235
|
+
<p className="text-lg text-gray-700">Vérification de la licence...</p>
|
|
236
|
+
<p className="text-sm text-gray-500 mt-2">Connexion au serveur de licences</p>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (status === "requesting") {
|
|
243
|
+
return (
|
|
244
|
+
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
|
245
|
+
<div className="text-center">
|
|
246
|
+
<Loader2 className="w-8 h-8 animate-spin text-green-500 mx-auto mb-4" />
|
|
247
|
+
<p className="text-lg text-gray-700">Demande de licence en cours...</p>
|
|
248
|
+
<p className="text-sm text-gray-500 mt-2">Génération de votre licence personnalisée</p>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Mode warning (offline avec licence valide)
|
|
255
|
+
if (status === "warning") {
|
|
256
|
+
return (
|
|
257
|
+
<div className="relative">
|
|
258
|
+
{/* Bandeau d'avertissement */}
|
|
259
|
+
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4">
|
|
260
|
+
<div className="flex items-center">
|
|
261
|
+
<AlertTriangle className="w-5 h-5 text-yellow-400 mr-3" />
|
|
262
|
+
<div className="flex-1">
|
|
263
|
+
<p className="text-sm text-yellow-700">
|
|
264
|
+
Mode hors ligne - Licence valide localement
|
|
265
|
+
{lastOnlineCheck && (
|
|
266
|
+
<span className="block text-xs text-yellow-600">
|
|
267
|
+
Dernière vérification : {lastOnlineCheck.toLocaleDateString()}
|
|
268
|
+
</span>
|
|
269
|
+
)}
|
|
270
|
+
</p>
|
|
271
|
+
</div>
|
|
272
|
+
<button
|
|
273
|
+
onClick={() => verifyLicense(true)}
|
|
274
|
+
className="text-yellow-600 hover:text-yellow-800"
|
|
275
|
+
title="Tenter une reconnexion"
|
|
276
|
+
>
|
|
277
|
+
<RefreshCw className="w-4 h-4" />
|
|
278
|
+
</button>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
{children}
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Mode licence expirée avec option de renouvellement
|
|
287
|
+
if (status === "expired") {
|
|
288
|
+
return (
|
|
289
|
+
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
|
290
|
+
<div className="max-w-lg p-8 bg-white rounded-xl shadow-lg border border-red-200">
|
|
291
|
+
<div className="text-center mb-6">
|
|
292
|
+
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
|
293
|
+
<h2 className="text-2xl font-bold text-red-600 mb-2">Licence expirée</h2>
|
|
294
|
+
<p className="text-gray-600">{error}</p>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
{licenseInfo?.license && (
|
|
298
|
+
<div className="bg-red-50 p-4 rounded-lg mb-6">
|
|
299
|
+
<h3 className="font-semibold text-red-800 mb-2">Informations de licence</h3>
|
|
300
|
+
<div className="text-sm text-red-700 space-y-1">
|
|
301
|
+
<p><strong>Email :</strong> {licenseInfo.license.email}</p>
|
|
302
|
+
<p><strong>Projet :</strong> {licenseInfo.license.project}</p>
|
|
303
|
+
<p><strong>Clé :</strong> {licenseInfo.license.key}</p>
|
|
304
|
+
<p><strong>Expiré le :</strong> {new Date(licenseInfo.license.expires_at).toLocaleDateString()}</p>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
|
|
309
|
+
<div className="space-y-4">
|
|
310
|
+
<button
|
|
311
|
+
onClick={() => {
|
|
312
|
+
setEmail(licenseInfo?.license?.email || "");
|
|
313
|
+
setStatus("invalid");
|
|
314
|
+
}}
|
|
315
|
+
className="w-full px-4 py-3 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 font-medium"
|
|
316
|
+
>
|
|
317
|
+
Renouveler la licence
|
|
318
|
+
</button>
|
|
319
|
+
<button
|
|
320
|
+
onClick={handleRetry}
|
|
321
|
+
className="w-full px-4 py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500"
|
|
322
|
+
>
|
|
323
|
+
<RefreshCw className="w-4 h-4 inline mr-2" />
|
|
324
|
+
Vérifier à nouveau
|
|
325
|
+
</button>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Formulaire de demande de licence
|
|
333
|
+
if (status === "invalid") {
|
|
334
|
+
return (
|
|
335
|
+
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
|
336
|
+
<div className="max-w-md p-8 bg-white rounded-xl shadow-lg">
|
|
337
|
+
<div className="text-center mb-6">
|
|
338
|
+
<Shield className="w-16 h-16 text-blue-500 mx-auto mb-4" />
|
|
339
|
+
<h2 className="text-2xl font-bold text-gray-800 mb-2">Activation requise</h2>
|
|
340
|
+
<p className="text-gray-600">Activez {project} avec votre licence</p>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
{error && (
|
|
344
|
+
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
|
345
|
+
<div className="flex items-center">
|
|
346
|
+
<XCircle className="w-5 h-5 text-red-500 mr-2" />
|
|
347
|
+
<p className="text-red-700 text-sm">{error}</p>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
)}
|
|
351
|
+
|
|
352
|
+
{isOffline && (
|
|
353
|
+
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
|
354
|
+
<div className="flex items-center">
|
|
355
|
+
<AlertTriangle className="w-5 h-5 text-yellow-500 mr-2" />
|
|
356
|
+
<p className="text-yellow-700 text-sm">Mode hors ligne détecté</p>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
)}
|
|
360
|
+
|
|
361
|
+
<form onSubmit={(e) => { e.preventDefault(); requestLicense(); }} className="space-y-4">
|
|
362
|
+
<div>
|
|
363
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
364
|
+
Email professionnel *
|
|
365
|
+
</label>
|
|
366
|
+
<input
|
|
367
|
+
type="email"
|
|
368
|
+
value={email}
|
|
369
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
370
|
+
placeholder="votre@entreprise.com"
|
|
371
|
+
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
372
|
+
required
|
|
373
|
+
/>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<div>
|
|
377
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
378
|
+
Entreprise (optionnel)
|
|
379
|
+
</label>
|
|
380
|
+
<input
|
|
381
|
+
type="text"
|
|
382
|
+
value={company}
|
|
383
|
+
onChange={(e) => setCompany(e.target.value)}
|
|
384
|
+
placeholder="Nom de votre entreprise"
|
|
385
|
+
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
386
|
+
/>
|
|
387
|
+
</div>
|
|
388
|
+
|
|
389
|
+
<div className="bg-blue-50 p-4 rounded-lg">
|
|
390
|
+
<h4 className="font-medium text-blue-800 mb-2">Informations de licence</h4>
|
|
391
|
+
<div className="text-sm text-blue-700 space-y-1">
|
|
392
|
+
<p><strong>Produit :</strong> {project}</p>
|
|
393
|
+
<p><strong>Version :</strong> {version}</p>
|
|
394
|
+
<p><strong>Type :</strong> Licence professionnelle</p>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
<button
|
|
399
|
+
type="submit"
|
|
400
|
+
disabled={!email.trim()}
|
|
401
|
+
className="w-full px-4 py-3 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
402
|
+
>
|
|
403
|
+
Demander une licence
|
|
404
|
+
</button>
|
|
405
|
+
|
|
406
|
+
<div className="text-center space-y-2">
|
|
407
|
+
<button
|
|
408
|
+
type="button"
|
|
409
|
+
onClick={handleRetry}
|
|
410
|
+
className="text-sm text-gray-500 hover:text-gray-700 inline-flex items-center"
|
|
411
|
+
>
|
|
412
|
+
<RefreshCw className="w-4 h-4 mr-1" />
|
|
413
|
+
Vérifier à nouveau ({retryCount + 1})
|
|
414
|
+
</button>
|
|
415
|
+
|
|
416
|
+
{licenseInfo && (
|
|
417
|
+
<button
|
|
418
|
+
type="button"
|
|
419
|
+
onClick={downloadLicense}
|
|
420
|
+
className="block w-full text-sm text-blue-500 hover:text-blue-700"
|
|
421
|
+
>
|
|
422
|
+
<Download className="w-4 h-4 inline mr-1" />
|
|
423
|
+
Télécharger le fichier de licence
|
|
424
|
+
</button>
|
|
425
|
+
)}
|
|
426
|
+
</div>
|
|
427
|
+
</form>
|
|
428
|
+
|
|
429
|
+
<div className="mt-6 pt-6 border-t border-gray-200 text-center">
|
|
430
|
+
<p className="text-xs text-gray-500">
|
|
431
|
+
Une licence sera générée automatiquement et liée à cet appareil.
|
|
432
|
+
<br />
|
|
433
|
+
Contactez le support en cas de problème.
|
|
434
|
+
</p>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Licence valide - Afficher l'application avec bandeau en bas
|
|
442
|
+
return (
|
|
443
|
+
<div className="relative min-h-screen flex flex-col">
|
|
444
|
+
{/* Contenu principal */}
|
|
445
|
+
<div className="flex-1">
|
|
446
|
+
{children}
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
{/* Bandeau de licence en bas de page */}
|
|
450
|
+
{licenseInfo?.license && (
|
|
451
|
+
<div className="fixed bottom-0 left-0 right-0 bg-green-50 border-t-4 border-green-400 p-3 text-sm shadow-lg z-50">
|
|
452
|
+
<div className="container mx-auto flex items-center justify-between">
|
|
453
|
+
<div className="flex items-center">
|
|
454
|
+
<CheckCircle className="w-4 h-4 text-green-400 mr-2" />
|
|
455
|
+
<span className="text-green-700">
|
|
456
|
+
<strong>Licence valide</strong> • {licenseInfo.license.email} • {licenseInfo.license.project}
|
|
457
|
+
{licenseInfo.remaining_days !== undefined && (
|
|
458
|
+
<span className={`ml-2 ${licenseInfo.remaining_days < 30 ? 'text-yellow-600' : 'text-green-600'}`}>
|
|
459
|
+
{licenseInfo.remaining_days < 30
|
|
460
|
+
? `⚠️ Expire dans ${licenseInfo.remaining_days} jours`
|
|
461
|
+
: `✅ ${licenseInfo.remaining_days} jours restants`
|
|
462
|
+
}
|
|
463
|
+
</span>
|
|
464
|
+
)}
|
|
465
|
+
</span>
|
|
466
|
+
</div>
|
|
467
|
+
<div className="flex items-center space-x-3">
|
|
468
|
+
<span className="text-xs text-green-600">
|
|
469
|
+
Clé: {licenseInfo.license.key}
|
|
470
|
+
</span>
|
|
471
|
+
<button
|
|
472
|
+
onClick={() => verifyLicense(true)}
|
|
473
|
+
className="text-green-600 hover:text-green-800 p-1 rounded transition-colors"
|
|
474
|
+
title="Actualiser la licence"
|
|
475
|
+
>
|
|
476
|
+
<RefreshCw className="w-4 h-4" />
|
|
477
|
+
</button>
|
|
478
|
+
<button
|
|
479
|
+
onClick={() => {
|
|
480
|
+
const info = document.getElementById('license-info');
|
|
481
|
+
if (info) info.style.display = info.style.display === 'none' ? 'block' : 'none';
|
|
482
|
+
}}
|
|
483
|
+
className="text-green-600 hover:text-green-800 text-xs px-2 py-1 border border-green-300 rounded"
|
|
484
|
+
title="Voir détails"
|
|
485
|
+
>
|
|
486
|
+
Info
|
|
487
|
+
</button>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
|
|
491
|
+
{/* Détails de licence (masqués par défaut) */}
|
|
492
|
+
<div id="license-info" className="mt-2 pt-2 border-t border-green-200 text-xs text-green-600" style={{ display: 'none' }}>
|
|
493
|
+
<div className="container mx-auto grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
494
|
+
<div><strong>Activations:</strong> {licenseInfo.license.activations}/{licenseInfo.license.max_activations}</div>
|
|
495
|
+
<div><strong>Expire le:</strong> {new Date(licenseInfo.license.expires_at).toLocaleDateString()}</div>
|
|
496
|
+
<div><strong>Status:</strong> <span className="text-green-600">ACTIVE</span></div>
|
|
497
|
+
<div><strong>Dernière vérif:</strong> {lastOnlineCheck?.toLocaleTimeString() || 'Maintenant'}</div>
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
)}
|
|
502
|
+
</div>
|
|
503
|
+
);
|
|
504
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// @mostajs/licensing-ui — contexte React alimenté par createLicenseUiFromEnv.
|
|
3
|
+
// Centralise la CONFIG .env (serveur, projet, clé publique) + le contrôleur composant
|
|
4
|
+
// @mostajs/licensing. Les composants (LicenseGate, LicenseActivationForm…) le consomment via
|
|
5
|
+
// useLicenseUi() au lieu d'URLs/projet codés en dur. @author Dr Hamid MADANI <drmdh@msn.com>
|
|
6
|
+
import React, { createContext, useContext, useMemo } from "react";
|
|
7
|
+
import { createLicenseUi, licenseUiConfigFromEnv, type LicenseUiOptions } from "../index";
|
|
8
|
+
|
|
9
|
+
export interface LicenseContextValue {
|
|
10
|
+
serverUrl: string;
|
|
11
|
+
project?: string;
|
|
12
|
+
ui: ReturnType<typeof createLicenseUi>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const LicenseCtx = createContext<LicenseContextValue | null>(null);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Fournit la config de licence à l'arbre React.
|
|
19
|
+
* <LicenseProvider env={publicEnv}> … </LicenseProvider>
|
|
20
|
+
* `env` : objet de variables (NEXT_PUBLIC_LICENSE_* / LICENSE_*) ; à défaut, lit process.env.
|
|
21
|
+
* `config` : surcharge directe (LicenseUiOptions) si fournie.
|
|
22
|
+
*/
|
|
23
|
+
export function LicenseProvider({ config, env, children }: { config?: LicenseUiOptions; env?: Record<string, string | undefined>; children: React.ReactNode }) {
|
|
24
|
+
const value = useMemo<LicenseContextValue>(() => {
|
|
25
|
+
const opts = config ?? licenseUiConfigFromEnv(env ?? (typeof process !== "undefined" ? (process.env as any) : {}));
|
|
26
|
+
return { serverUrl: opts.baseUrl, project: opts.project, ui: createLicenseUi(opts) };
|
|
27
|
+
}, [config, env]);
|
|
28
|
+
return <LicenseCtx.Provider value={value}>{children}</LicenseCtx.Provider>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Accès à la config + contrôleur de licence (compose @mostajs/licensing). */
|
|
32
|
+
export function useLicenseUi(): LicenseContextValue {
|
|
33
|
+
const v = useContext(LicenseCtx);
|
|
34
|
+
if (!v) throw new Error("useLicenseUi: entourez vos composants de <LicenseProvider> (config via createLicenseUiFromEnv).");
|
|
35
|
+
return v;
|
|
36
|
+
}
|