@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.
@@ -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
+ }