@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,488 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState, useCallback } from "react";
|
|
3
|
+
import { Shield, CheckCircle, XCircle, AlertTriangle, RefreshCw, Loader2, ExternalLink, ToggleLeft, ToggleRight } from "lucide-react";
|
|
4
|
+
import LicenseActivationForm from "./LicenseActivationForm";
|
|
5
|
+
import { useLicenseUi } from "./LicenseProvider";
|
|
6
|
+
import SimpleLicenseActivation from "./SimpleLicenseActivation";
|
|
7
|
+
import PendingLicenseActivation from "./PendingLicenseActivation";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
project?: string;
|
|
12
|
+
version?: string;
|
|
13
|
+
gracePeriod?: number; // Jours de grâce offline
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface LicenseInfo {
|
|
17
|
+
valid: boolean;
|
|
18
|
+
reason?: string;
|
|
19
|
+
license?: {
|
|
20
|
+
key: string;
|
|
21
|
+
email: string;
|
|
22
|
+
project: string;
|
|
23
|
+
expires_at: string;
|
|
24
|
+
activations: number;
|
|
25
|
+
max_activations: number;
|
|
26
|
+
};
|
|
27
|
+
remaining_days?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function EnhancedLicenseGate({
|
|
31
|
+
children,
|
|
32
|
+
project: projectProp,
|
|
33
|
+
version = "1.0.0",
|
|
34
|
+
gracePeriod = 3
|
|
35
|
+
}: Props) {
|
|
36
|
+
const { serverUrl, project: ctxProject } = useLicenseUi(); // config .env (createLicenseUiFromEnv)
|
|
37
|
+
const project = projectProp ?? ctxProject ?? "app";
|
|
38
|
+
const [status, setStatus] = useState<"checking" | "valid" | "invalid" | "activation" | "expired" | "warning" | "pending">("checking");
|
|
39
|
+
const [error, setError] = useState<string | null>(null);
|
|
40
|
+
const [licenseInfo, setLicenseInfo] = useState<LicenseInfo | null>(null);
|
|
41
|
+
const [isOffline, setIsOffline] = useState(false);
|
|
42
|
+
const [lastOnlineCheck, setLastOnlineCheck] = useState<Date | null>(null);
|
|
43
|
+
const [retryCount, setRetryCount] = useState(0);
|
|
44
|
+
const [useSimpleActivation, setUseSimpleActivation] = useState(true);
|
|
45
|
+
const [pendingLicenseData, setPendingLicenseData] = useState<any>(null);
|
|
46
|
+
const [pendingActivationCode, setPendingActivationCode] = useState<string | null>(null);
|
|
47
|
+
|
|
48
|
+
// Vérification de la connectivité
|
|
49
|
+
const checkConnectivity = useCallback(async () => {
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch("/api/license/ping", {
|
|
52
|
+
method: "GET",
|
|
53
|
+
cache: "no-cache"
|
|
54
|
+
});
|
|
55
|
+
return response.ok;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
// Vérification offline (cache local + période de grâce)
|
|
62
|
+
const checkOfflineLicense = useCallback(() => {
|
|
63
|
+
const offlineData = localStorage.getItem("license_cache");
|
|
64
|
+
if (!offlineData) return false;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const cached = JSON.parse(offlineData);
|
|
68
|
+
const cacheDate = new Date(cached.timestamp);
|
|
69
|
+
const now = new Date();
|
|
70
|
+
const diffDays = Math.floor((now.getTime() - cacheDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
71
|
+
|
|
72
|
+
if (diffDays <= gracePeriod && cached.valid) {
|
|
73
|
+
setLicenseInfo(cached.licenseInfo);
|
|
74
|
+
setLastOnlineCheck(cacheDate);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.warn("Cache licence corrompu :", e);
|
|
79
|
+
localStorage.removeItem("license_cache");
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}, [gracePeriod]);
|
|
83
|
+
|
|
84
|
+
const verifyLicense = useCallback(async (force = false) => {
|
|
85
|
+
try {
|
|
86
|
+
const isOnline = await checkConnectivity();
|
|
87
|
+
|
|
88
|
+
if (!isOnline && !force) {
|
|
89
|
+
setIsOffline(true);
|
|
90
|
+
if (checkOfflineLicense()) {
|
|
91
|
+
setStatus("warning"); // Mode offline avec licence valide
|
|
92
|
+
return;
|
|
93
|
+
} else {
|
|
94
|
+
setStatus("invalid");
|
|
95
|
+
setError("Aucune connexion et licence locale expirée");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
setIsOffline(false);
|
|
101
|
+
|
|
102
|
+
// Vérification via l'API locale (qui vérifie le fichier local)
|
|
103
|
+
const response = await fetch("/api/license/verify");
|
|
104
|
+
const data: LicenseInfo = await response.json();
|
|
105
|
+
|
|
106
|
+
setLicenseInfo(data);
|
|
107
|
+
|
|
108
|
+
if (data.valid) {
|
|
109
|
+
setStatus("valid");
|
|
110
|
+
// Mise à jour du cache local
|
|
111
|
+
localStorage.setItem("license_cache", JSON.stringify({
|
|
112
|
+
licenseInfo: data,
|
|
113
|
+
timestamp: new Date().toISOString(),
|
|
114
|
+
valid: true
|
|
115
|
+
}));
|
|
116
|
+
setLastOnlineCheck(new Date());
|
|
117
|
+
} else {
|
|
118
|
+
setStatus("invalid");
|
|
119
|
+
localStorage.removeItem("license_cache");
|
|
120
|
+
|
|
121
|
+
if (data.reason === "expired") {
|
|
122
|
+
setStatus("expired");
|
|
123
|
+
setError(`Licence expirée depuis ${data.remaining_days ? Math.abs(data.remaining_days) : '?'} jours`);
|
|
124
|
+
} else if (data.reason === "signature") {
|
|
125
|
+
setError("Signature invalide - Fichier de licence corrompu");
|
|
126
|
+
} else if (data.reason === "not_found") {
|
|
127
|
+
setError("Aucune licence trouvée - Activation requise");
|
|
128
|
+
setStatus("activation");
|
|
129
|
+
} else {
|
|
130
|
+
setError(data.reason || "Licence invalide");
|
|
131
|
+
setStatus("activation");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.error("Erreur vérification licence :", e);
|
|
136
|
+
|
|
137
|
+
// Tentative offline en cas d'erreur réseau
|
|
138
|
+
if (checkOfflineLicense()) {
|
|
139
|
+
setStatus("warning");
|
|
140
|
+
setIsOffline(true);
|
|
141
|
+
} else {
|
|
142
|
+
setStatus("activation");
|
|
143
|
+
setError("Licence non trouvée - Activation requise");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}, [checkConnectivity, checkOfflineLicense]);
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
// Vérifier d'abord s'il y a une licence en attente
|
|
150
|
+
const pendingLicense = localStorage.getItem('pending_license');
|
|
151
|
+
if (pendingLicense) {
|
|
152
|
+
try {
|
|
153
|
+
const pending = JSON.parse(pendingLicense);
|
|
154
|
+
setPendingLicenseData(pending.licenseData);
|
|
155
|
+
setPendingActivationCode(pending.activationCode);
|
|
156
|
+
setLicenseInfo({
|
|
157
|
+
valid: false,
|
|
158
|
+
license: {
|
|
159
|
+
...pending.licenseInfo,
|
|
160
|
+
pendingData: pending
|
|
161
|
+
},
|
|
162
|
+
reason: 'pending_activation'
|
|
163
|
+
});
|
|
164
|
+
setStatus("pending");
|
|
165
|
+
return; // Ne pas vérifier la licence normale
|
|
166
|
+
} catch (e) {
|
|
167
|
+
console.error('Erreur lecture licence en attente:', e);
|
|
168
|
+
localStorage.removeItem('pending_license');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Sinon, vérification normale
|
|
173
|
+
verifyLicense();
|
|
174
|
+
|
|
175
|
+
// Vérification périodique (toutes les 24h)
|
|
176
|
+
const interval = setInterval(() => {
|
|
177
|
+
verifyLicense();
|
|
178
|
+
}, 24 * 60 * 60 * 1000);
|
|
179
|
+
|
|
180
|
+
return () => clearInterval(interval);
|
|
181
|
+
}, [verifyLicense]);
|
|
182
|
+
|
|
183
|
+
const handleRetry = () => {
|
|
184
|
+
setRetryCount(prev => prev + 1);
|
|
185
|
+
setError(null);
|
|
186
|
+
verifyLicense(true);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const handleActivationSuccess = (licenseInfo: any) => {
|
|
190
|
+
// Vérifier si c'est une licence en attente (PENDING)
|
|
191
|
+
if (licenseInfo.status === 'PENDING' && licenseInfo.pendingData) {
|
|
192
|
+
// Stocker les données pour l'activation différée
|
|
193
|
+
setPendingLicenseData(licenseInfo.pendingData.licenseData);
|
|
194
|
+
setPendingActivationCode(licenseInfo.pendingData.activationCode);
|
|
195
|
+
setLicenseInfo({
|
|
196
|
+
valid: false,
|
|
197
|
+
license: licenseInfo,
|
|
198
|
+
reason: 'pending_activation'
|
|
199
|
+
});
|
|
200
|
+
setStatus("pending");
|
|
201
|
+
setError(null);
|
|
202
|
+
} else {
|
|
203
|
+
// Activation normale (pour compatibilité)
|
|
204
|
+
setLicenseInfo({
|
|
205
|
+
valid: true,
|
|
206
|
+
license: licenseInfo
|
|
207
|
+
});
|
|
208
|
+
setStatus("valid");
|
|
209
|
+
setError(null);
|
|
210
|
+
|
|
211
|
+
// Vérification immédiate après activation
|
|
212
|
+
setTimeout(() => {
|
|
213
|
+
verifyLicense(true);
|
|
214
|
+
}, 1000);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const handlePendingActivation = (result: any) => {
|
|
219
|
+
// La licence a été activée avec succès
|
|
220
|
+
setLicenseInfo({
|
|
221
|
+
valid: true,
|
|
222
|
+
license: result.activationInfo
|
|
223
|
+
});
|
|
224
|
+
setStatus("valid");
|
|
225
|
+
setPendingLicenseData(null);
|
|
226
|
+
setPendingActivationCode(null);
|
|
227
|
+
|
|
228
|
+
// Recharger la page pour appliquer la licence
|
|
229
|
+
setTimeout(() => {
|
|
230
|
+
window.location.reload();
|
|
231
|
+
}, 2000);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const handleCancelPending = () => {
|
|
235
|
+
// Annuler l'activation en attente
|
|
236
|
+
setPendingLicenseData(null);
|
|
237
|
+
setPendingActivationCode(null);
|
|
238
|
+
setStatus("activation");
|
|
239
|
+
localStorage.removeItem('pending_license');
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// États d'affichage
|
|
243
|
+
if (status === "checking") {
|
|
244
|
+
return (
|
|
245
|
+
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
|
246
|
+
<div className="text-center">
|
|
247
|
+
<Loader2 className="w-8 h-8 animate-spin text-blue-500 mx-auto mb-4" />
|
|
248
|
+
<p className="text-lg text-gray-700">Vérification de la licence...</p>
|
|
249
|
+
<p className="text-sm text-gray-500 mt-2">Connexion au système de licences</p>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Formulaire d'activation
|
|
256
|
+
if (status === "activation" || status === "invalid") {
|
|
257
|
+
return (
|
|
258
|
+
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
|
259
|
+
<div className="w-full max-w-4xl p-6">
|
|
260
|
+
{/* En-tête avec informations */}
|
|
261
|
+
<div className="text-center mb-8">
|
|
262
|
+
<h1 className="text-3xl font-bold text-gray-800 mb-4">
|
|
263
|
+
Activation de Licence - {project}
|
|
264
|
+
</h1>
|
|
265
|
+
|
|
266
|
+
{/* Toggle pour changer de mode */}
|
|
267
|
+
<div className="flex items-center justify-center mb-6">
|
|
268
|
+
<button
|
|
269
|
+
onClick={() => setUseSimpleActivation(!useSimpleActivation)}
|
|
270
|
+
className="flex items-center space-x-3 px-4 py-2 rounded-lg bg-gray-100 hover:bg-gray-200 transition-colors"
|
|
271
|
+
>
|
|
272
|
+
<span className={`text-sm font-medium ${!useSimpleActivation ? 'text-gray-700' : 'text-gray-500'}`}>
|
|
273
|
+
Activation manuelle
|
|
274
|
+
</span>
|
|
275
|
+
{useSimpleActivation ? (
|
|
276
|
+
<ToggleRight className="w-8 h-8 text-blue-500" />
|
|
277
|
+
) : (
|
|
278
|
+
<ToggleLeft className="w-8 h-8 text-gray-400" />
|
|
279
|
+
)}
|
|
280
|
+
<span className={`text-sm font-medium ${useSimpleActivation ? 'text-gray-700' : 'text-gray-500'}`}>
|
|
281
|
+
Activation rapide
|
|
282
|
+
</span>
|
|
283
|
+
</button>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
{!useSimpleActivation && (
|
|
287
|
+
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
|
288
|
+
<div className="flex items-start">
|
|
289
|
+
<AlertTriangle className="w-5 h-5 text-blue-500 mr-3 mt-0.5 flex-shrink-0" />
|
|
290
|
+
<div className="text-left">
|
|
291
|
+
<h3 className="text-blue-800 font-medium mb-2">Activation manuelle</h3>
|
|
292
|
+
<div className="text-sm text-blue-700 space-y-2">
|
|
293
|
+
<div className="flex items-start">
|
|
294
|
+
<span className="font-bold mr-2">1.</span>
|
|
295
|
+
<div>
|
|
296
|
+
Rendez-vous sur <a href={serverUrl} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:text-blue-800 font-medium inline-flex items-center">
|
|
297
|
+
licences.amia.fr <ExternalLink className="w-3 h-3 ml-1" />
|
|
298
|
+
</a> pour télécharger votre fichier de licence
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
<div className="flex items-start">
|
|
302
|
+
<span className="font-bold mr-2">2.</span>
|
|
303
|
+
<div>Récupérez votre code d'activation fourni par email lors de l'achat</div>
|
|
304
|
+
</div>
|
|
305
|
+
<div className="flex items-start">
|
|
306
|
+
<span className="font-bold mr-2">3.</span>
|
|
307
|
+
<div>Utilisez le formulaire ci-dessous pour activer votre licence</div>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
{error && (
|
|
317
|
+
<div className="max-w-lg mx-auto mb-6">
|
|
318
|
+
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
319
|
+
<div className="flex items-center">
|
|
320
|
+
<XCircle className="w-5 h-5 text-red-500 mr-2 flex-shrink-0" />
|
|
321
|
+
<p className="text-red-700 text-sm">{error}</p>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
326
|
+
|
|
327
|
+
{/* Afficher le formulaire approprié */}
|
|
328
|
+
{useSimpleActivation ? (
|
|
329
|
+
<SimpleLicenseActivation
|
|
330
|
+
onActivationSuccess={handleActivationSuccess}
|
|
331
|
+
onSwitchToManual={() => setUseSimpleActivation(false)}
|
|
332
|
+
/>
|
|
333
|
+
) : (
|
|
334
|
+
<LicenseActivationForm
|
|
335
|
+
onActivationSuccess={handleActivationSuccess}
|
|
336
|
+
/>
|
|
337
|
+
)}
|
|
338
|
+
|
|
339
|
+
{/* Actions supplémentaires */}
|
|
340
|
+
<div className="max-w-lg mx-auto mt-6 text-center">
|
|
341
|
+
<button
|
|
342
|
+
onClick={handleRetry}
|
|
343
|
+
className="text-sm text-gray-500 hover:text-gray-700 inline-flex items-center"
|
|
344
|
+
>
|
|
345
|
+
<RefreshCw className="w-4 h-4 mr-1" />
|
|
346
|
+
Vérifier à nouveau ({retryCount + 1})
|
|
347
|
+
</button>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Mode warning (offline avec licence valide)
|
|
355
|
+
if (status === "warning") {
|
|
356
|
+
return (
|
|
357
|
+
<div className="relative">
|
|
358
|
+
{/* Bandeau d'avertissement */}
|
|
359
|
+
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4">
|
|
360
|
+
<div className="flex items-center">
|
|
361
|
+
<AlertTriangle className="w-5 h-5 text-yellow-400 mr-3" />
|
|
362
|
+
<div className="flex-1">
|
|
363
|
+
<p className="text-sm text-yellow-700">
|
|
364
|
+
Mode hors ligne - Licence valide localement
|
|
365
|
+
{lastOnlineCheck && (
|
|
366
|
+
<span className="block text-xs text-yellow-600">
|
|
367
|
+
Dernière vérification : {lastOnlineCheck.toLocaleDateString()}
|
|
368
|
+
</span>
|
|
369
|
+
)}
|
|
370
|
+
</p>
|
|
371
|
+
</div>
|
|
372
|
+
<button
|
|
373
|
+
onClick={() => verifyLicense(true)}
|
|
374
|
+
className="text-yellow-600 hover:text-yellow-800"
|
|
375
|
+
title="Tenter une reconnexion"
|
|
376
|
+
>
|
|
377
|
+
<RefreshCw className="w-4 h-4" />
|
|
378
|
+
</button>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
{children}
|
|
382
|
+
</div>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Mode licence en attente d'activation (PENDING)
|
|
387
|
+
if (status === "pending" && pendingLicenseData && pendingActivationCode) {
|
|
388
|
+
return (
|
|
389
|
+
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
|
390
|
+
<PendingLicenseActivation
|
|
391
|
+
licenseInfo={licenseInfo?.license}
|
|
392
|
+
verificationInfo={licenseInfo?.license?.pendingData?.verificationInfo}
|
|
393
|
+
activationCode={pendingActivationCode}
|
|
394
|
+
licenseData={pendingLicenseData}
|
|
395
|
+
onActivate={handlePendingActivation}
|
|
396
|
+
onCancel={handleCancelPending}
|
|
397
|
+
/>
|
|
398
|
+
</div>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Mode licence expirée
|
|
403
|
+
if (status === "expired") {
|
|
404
|
+
return (
|
|
405
|
+
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
|
406
|
+
<div className="max-w-lg p-8 bg-white rounded-xl shadow-lg border border-red-200">
|
|
407
|
+
<div className="text-center mb-6">
|
|
408
|
+
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
|
409
|
+
<h2 className="text-2xl font-bold text-red-600 mb-2">Licence expirée</h2>
|
|
410
|
+
<p className="text-gray-600">{error}</p>
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
{licenseInfo?.license && (
|
|
414
|
+
<div className="bg-red-50 p-4 rounded-lg mb-6">
|
|
415
|
+
<h3 className="font-semibold text-red-800 mb-2">Informations de licence</h3>
|
|
416
|
+
<div className="text-sm text-red-700 space-y-1">
|
|
417
|
+
<p><strong>Email :</strong> {licenseInfo.license.email}</p>
|
|
418
|
+
<p><strong>Projet :</strong> {licenseInfo.license.project}</p>
|
|
419
|
+
<p><strong>Clé :</strong> {licenseInfo.license.key}</p>
|
|
420
|
+
<p><strong>Expiré le :</strong> {new Date(licenseInfo.license.expires_at).toLocaleDateString()}</p>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
)}
|
|
424
|
+
|
|
425
|
+
<div className="space-y-4">
|
|
426
|
+
<button
|
|
427
|
+
onClick={() => setStatus("activation")}
|
|
428
|
+
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"
|
|
429
|
+
>
|
|
430
|
+
Renouveler la licence
|
|
431
|
+
</button>
|
|
432
|
+
<button
|
|
433
|
+
onClick={handleRetry}
|
|
434
|
+
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"
|
|
435
|
+
>
|
|
436
|
+
<RefreshCw className="w-4 h-4 inline mr-2" />
|
|
437
|
+
Vérifier à nouveau
|
|
438
|
+
</button>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Licence valide - Afficher l'application avec bandeau en bas
|
|
446
|
+
return (
|
|
447
|
+
<div className="relative min-h-screen flex flex-col">
|
|
448
|
+
{/* Contenu principal */}
|
|
449
|
+
<div className="flex-1">
|
|
450
|
+
{children}
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
{/* Bandeau de licence en bas de page */}
|
|
454
|
+
{licenseInfo?.license && (
|
|
455
|
+
<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">
|
|
456
|
+
<div className="container mx-auto flex items-center justify-between">
|
|
457
|
+
<div className="flex items-center">
|
|
458
|
+
<CheckCircle className="w-4 h-4 text-green-400 mr-2" />
|
|
459
|
+
<span className="text-green-700">
|
|
460
|
+
<strong>Licence valide</strong> • {licenseInfo.license.email} • {licenseInfo.license.project}
|
|
461
|
+
{licenseInfo.remaining_days !== undefined && (
|
|
462
|
+
<span className={`ml-2 ${licenseInfo.remaining_days < 30 ? 'text-yellow-600' : 'text-green-600'}`}>
|
|
463
|
+
{licenseInfo.remaining_days < 30
|
|
464
|
+
? `⚠️ Expire dans ${licenseInfo.remaining_days} jours`
|
|
465
|
+
: `✅ ${licenseInfo.remaining_days} jours restants`
|
|
466
|
+
}
|
|
467
|
+
</span>
|
|
468
|
+
)}
|
|
469
|
+
</span>
|
|
470
|
+
</div>
|
|
471
|
+
<div className="flex items-center space-x-3">
|
|
472
|
+
<span className="text-xs text-green-600">
|
|
473
|
+
Activations: {licenseInfo.license.activations}/{licenseInfo.license.max_activations}
|
|
474
|
+
</span>
|
|
475
|
+
<button
|
|
476
|
+
onClick={() => verifyLicense(true)}
|
|
477
|
+
className="text-green-600 hover:text-green-800 p-1 rounded transition-colors"
|
|
478
|
+
title="Actualiser la licence"
|
|
479
|
+
>
|
|
480
|
+
<RefreshCw className="w-4 h-4" />
|
|
481
|
+
</button>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
)}
|
|
486
|
+
</div>
|
|
487
|
+
);
|
|
488
|
+
}
|