@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,270 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Shield, CheckCircle, Clock, AlertTriangle, Loader2, Key, Server, Calendar, Users, Zap, XCircle } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
interface PendingLicenseActivationProps {
|
|
6
|
+
licenseInfo: any;
|
|
7
|
+
verificationInfo?: any;
|
|
8
|
+
activationCode: string;
|
|
9
|
+
licenseData: any;
|
|
10
|
+
onActivate?: (result: any) => void;
|
|
11
|
+
onCancel?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function PendingLicenseActivation({
|
|
15
|
+
licenseInfo,
|
|
16
|
+
verificationInfo,
|
|
17
|
+
activationCode,
|
|
18
|
+
licenseData,
|
|
19
|
+
onActivate,
|
|
20
|
+
onCancel
|
|
21
|
+
}: PendingLicenseActivationProps) {
|
|
22
|
+
const [isActivating, setIsActivating] = useState(false);
|
|
23
|
+
const [error, setError] = useState<string | null>(null);
|
|
24
|
+
const [activationResult, setActivationResult] = useState<any>(null);
|
|
25
|
+
|
|
26
|
+
const handleStartActivation = async () => {
|
|
27
|
+
setIsActivating(true);
|
|
28
|
+
setError(null);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Appeler l'API pour démarrer l'activation et le comptage
|
|
32
|
+
const response = await fetch('/api/license/start-activation', {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: {
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
activationCode,
|
|
39
|
+
licenseData
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
const errorData = await response.json();
|
|
45
|
+
throw new Error(errorData.error || 'Échec de l\'activation');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = await response.json();
|
|
49
|
+
setActivationResult(result);
|
|
50
|
+
|
|
51
|
+
// Mettre à jour le localStorage
|
|
52
|
+
localStorage.setItem('license_cache', JSON.stringify({
|
|
53
|
+
licenseInfo: {
|
|
54
|
+
valid: true,
|
|
55
|
+
license: {
|
|
56
|
+
...licenseInfo,
|
|
57
|
+
status: 'ACTIVE',
|
|
58
|
+
activated_at: result.activationInfo?.started_at,
|
|
59
|
+
expires_at: result.activationInfo?.expires_at
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
valid: true
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
// Supprimer la licence en attente
|
|
67
|
+
localStorage.removeItem('pending_license');
|
|
68
|
+
|
|
69
|
+
// Notifier le parent après un court délai
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
onActivate?.(result);
|
|
72
|
+
}, 1500);
|
|
73
|
+
|
|
74
|
+
} catch (err: any) {
|
|
75
|
+
console.error('Erreur activation:', err);
|
|
76
|
+
setError(err.message || 'Erreur lors de l\'activation');
|
|
77
|
+
setIsActivating(false);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Calculer les jours restants
|
|
82
|
+
const calculateRemainingDays = () => {
|
|
83
|
+
if (verificationInfo?.code_info?.expires_at) {
|
|
84
|
+
const expires = new Date(verificationInfo.code_info.expires_at);
|
|
85
|
+
const now = new Date();
|
|
86
|
+
const diff = Math.floor((expires.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
87
|
+
return diff;
|
|
88
|
+
}
|
|
89
|
+
return 365; // Par défaut
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const remainingDays = calculateRemainingDays();
|
|
93
|
+
const activationsRemaining = verificationInfo?.code_info?.activations_remaining ||
|
|
94
|
+
(verificationInfo?.code_info?.max_activations - verificationInfo?.code_info?.activations_used) ||
|
|
95
|
+
4;
|
|
96
|
+
|
|
97
|
+
if (activationResult) {
|
|
98
|
+
// Afficher le succès de l'activation
|
|
99
|
+
return (
|
|
100
|
+
<div className="max-w-2xl mx-auto p-8 bg-white rounded-xl shadow-lg">
|
|
101
|
+
<div className="text-center">
|
|
102
|
+
<CheckCircle className="w-20 h-20 text-green-500 mx-auto mb-6" />
|
|
103
|
+
<h2 className="text-3xl font-bold text-gray-800 mb-4">
|
|
104
|
+
Licence Activée avec Succès !
|
|
105
|
+
</h2>
|
|
106
|
+
<p className="text-lg text-gray-600 mb-6">
|
|
107
|
+
Le comptage de votre licence a commencé.
|
|
108
|
+
</p>
|
|
109
|
+
|
|
110
|
+
<div className="bg-green-50 border border-green-200 rounded-lg p-6 text-left">
|
|
111
|
+
<h3 className="font-semibold text-green-800 mb-3">Détails de l'activation :</h3>
|
|
112
|
+
<div className="space-y-2 text-sm text-green-700">
|
|
113
|
+
<p><strong>Date d'activation :</strong> {new Date().toLocaleDateString()}</p>
|
|
114
|
+
<p><strong>Expire le :</strong> {new Date(activationResult.activationInfo?.expires_at).toLocaleDateString()}</p>
|
|
115
|
+
<p><strong>Jours restants :</strong> {activationResult.activationInfo?.remaining_days} jours</p>
|
|
116
|
+
<p><strong>Activations utilisées :</strong> {activationResult.activationInfo?.activations_used}/{activationResult.activationInfo?.max_activations}</p>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<p className="text-sm text-gray-500 mt-6">
|
|
121
|
+
L'application va redémarrer dans quelques instants...
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div className="max-w-2xl mx-auto p-8 bg-white rounded-xl shadow-lg">
|
|
130
|
+
{/* En-tête */}
|
|
131
|
+
<div className="text-center mb-8">
|
|
132
|
+
<div className="relative inline-block">
|
|
133
|
+
<Shield className="w-20 h-20 text-blue-500 mx-auto" />
|
|
134
|
+
<Clock className="w-8 h-8 text-orange-500 absolute -bottom-1 -right-1 bg-white rounded-full p-1" />
|
|
135
|
+
</div>
|
|
136
|
+
<h2 className="text-3xl font-bold text-gray-800 mt-4 mb-2">
|
|
137
|
+
Licence Prête pour Activation
|
|
138
|
+
</h2>
|
|
139
|
+
<p className="text-gray-600">
|
|
140
|
+
Votre licence a été téléchargée et vérifiée avec succès
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* Informations de la licence */}
|
|
145
|
+
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-6">
|
|
146
|
+
<h3 className="font-semibold text-blue-800 mb-4 flex items-center">
|
|
147
|
+
<Key className="w-5 h-5 mr-2" />
|
|
148
|
+
Détails de la licence
|
|
149
|
+
</h3>
|
|
150
|
+
|
|
151
|
+
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
152
|
+
<div>
|
|
153
|
+
<p className="text-blue-600 font-medium">Code d'activation</p>
|
|
154
|
+
<p className="text-blue-800 font-mono">{activationCode}</p>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div>
|
|
158
|
+
<p className="text-blue-600 font-medium">Projet</p>
|
|
159
|
+
<p className="text-blue-800">{licenseInfo?.project || 'app'}</p>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div>
|
|
163
|
+
<p className="text-blue-600 font-medium">Email</p>
|
|
164
|
+
<p className="text-blue-800">{licenseInfo?.email}</p>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div>
|
|
168
|
+
<p className="text-blue-600 font-medium">Durée de validité</p>
|
|
169
|
+
<p className="text-blue-800">{remainingDays} jours</p>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Statut des activations */}
|
|
175
|
+
<div className="bg-orange-50 border border-orange-200 rounded-lg p-6 mb-6">
|
|
176
|
+
<h3 className="font-semibold text-orange-800 mb-3 flex items-center">
|
|
177
|
+
<Users className="w-5 h-5 mr-2" />
|
|
178
|
+
Statut des activations
|
|
179
|
+
</h3>
|
|
180
|
+
|
|
181
|
+
<div className="flex items-center justify-between">
|
|
182
|
+
<div className="text-sm text-orange-700">
|
|
183
|
+
<p className="mb-1">Activations disponibles :</p>
|
|
184
|
+
<p className="text-2xl font-bold text-orange-800">
|
|
185
|
+
{activationsRemaining} / {verificationInfo?.code_info?.max_activations || 4}
|
|
186
|
+
</p>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{activationsRemaining <= 1 && (
|
|
190
|
+
<div className="flex items-center text-orange-600">
|
|
191
|
+
<AlertTriangle className="w-5 h-5 mr-2" />
|
|
192
|
+
<span className="text-sm">Dernière activation disponible</span>
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
{/* Avertissement important */}
|
|
199
|
+
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
|
200
|
+
<div className="flex items-start">
|
|
201
|
+
<AlertTriangle className="w-5 h-5 text-yellow-600 mr-3 mt-0.5 flex-shrink-0" />
|
|
202
|
+
<div className="text-sm">
|
|
203
|
+
<p className="text-yellow-800 font-medium mb-1">Information importante</p>
|
|
204
|
+
<p className="text-yellow-700">
|
|
205
|
+
En cliquant sur "Activer maintenant", le comptage de votre licence démarrera immédiatement
|
|
206
|
+
et sera comptabilisé sur le serveur de licences. Cette action est irréversible.
|
|
207
|
+
</p>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{/* Message d'erreur */}
|
|
213
|
+
{error && (
|
|
214
|
+
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
|
215
|
+
<div className="flex items-center">
|
|
216
|
+
<XCircle className="w-5 h-5 text-red-500 mr-2 flex-shrink-0" />
|
|
217
|
+
<p className="text-red-700 text-sm">{error}</p>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
{/* Boutons d'action */}
|
|
223
|
+
<div className="flex space-x-4">
|
|
224
|
+
<button
|
|
225
|
+
onClick={handleStartActivation}
|
|
226
|
+
disabled={isActivating}
|
|
227
|
+
className="flex-1 px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-all transform hover:scale-105 flex items-center justify-center"
|
|
228
|
+
>
|
|
229
|
+
{isActivating ? (
|
|
230
|
+
<>
|
|
231
|
+
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
|
232
|
+
Activation en cours...
|
|
233
|
+
</>
|
|
234
|
+
) : (
|
|
235
|
+
<>
|
|
236
|
+
<Zap className="w-5 h-5 mr-2" />
|
|
237
|
+
Activer Maintenant
|
|
238
|
+
</>
|
|
239
|
+
)}
|
|
240
|
+
</button>
|
|
241
|
+
|
|
242
|
+
{onCancel && (
|
|
243
|
+
<button
|
|
244
|
+
onClick={onCancel}
|
|
245
|
+
disabled={isActivating}
|
|
246
|
+
className="px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 transition-colors"
|
|
247
|
+
>
|
|
248
|
+
Annuler
|
|
249
|
+
</button>
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{/* Footer informatif */}
|
|
254
|
+
<div className="mt-8 pt-6 border-t border-gray-200">
|
|
255
|
+
<div className="flex items-center justify-center text-xs text-gray-500">
|
|
256
|
+
<Server className="w-4 h-4 mr-2" />
|
|
257
|
+
<span>
|
|
258
|
+
Connexion au serveur de licences :
|
|
259
|
+
<span className="text-green-600 font-medium ml-1">Active</span>
|
|
260
|
+
</span>
|
|
261
|
+
<span className="mx-2">•</span>
|
|
262
|
+
<Calendar className="w-4 h-4 mr-2" />
|
|
263
|
+
<span>
|
|
264
|
+
Date : {new Date().toLocaleDateString()}
|
|
265
|
+
</span>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Shield, CheckCircle, XCircle, AlertTriangle, Loader2, Key, Download } from 'lucide-react';
|
|
4
|
+
import { useLicenseUi } from './LicenseProvider';
|
|
5
|
+
|
|
6
|
+
interface SimpleLicenseActivationProps {
|
|
7
|
+
onActivationSuccess?: (licenseInfo: any) => void;
|
|
8
|
+
onBack?: () => void;
|
|
9
|
+
onSwitchToManual?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function SimpleLicenseActivation({
|
|
13
|
+
onActivationSuccess,
|
|
14
|
+
onBack,
|
|
15
|
+
onSwitchToManual
|
|
16
|
+
}: SimpleLicenseActivationProps) {
|
|
17
|
+
const { serverUrl } = useLicenseUi(); // config .env
|
|
18
|
+
const [activationCode, setActivationCode] = useState('');
|
|
19
|
+
const [loading, setLoading] = useState(false);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
const [success, setSuccess] = useState<string | null>(null);
|
|
22
|
+
const [progress, setProgress] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
const validateActivationCode = (code: string): boolean => {
|
|
25
|
+
// Format attendu: XXXX-XXXX-XXXX-XXXX (16 caractères + 3 tirets)
|
|
26
|
+
const codeRegex = /^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/;
|
|
27
|
+
return codeRegex.test(code.toUpperCase());
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handleActivation = async () => {
|
|
31
|
+
if (!activationCode.trim()) {
|
|
32
|
+
setError('Veuillez entrer votre code d\'activation');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!validateActivationCode(activationCode)) {
|
|
37
|
+
setError('Format de code d\'activation invalide (attendu: XXXX-XXXX-XXXX-XXXX)');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setLoading(true);
|
|
42
|
+
setError(null);
|
|
43
|
+
setSuccess(null);
|
|
44
|
+
setProgress('Connexion au serveur de licences...');
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// Étape 1: Télécharger la licence depuis le serveur
|
|
48
|
+
setProgress('Téléchargement de la licence et de la clé publique...');
|
|
49
|
+
|
|
50
|
+
const downloadResponse = await fetch('/api/license/download-by-code', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
activationCode: activationCode.toUpperCase(),
|
|
57
|
+
downloadPublicKey: true // Télécharger automatiquement le public.pem
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!downloadResponse.ok) {
|
|
62
|
+
const errorData = await downloadResponse.json();
|
|
63
|
+
throw new Error(errorData.error || 'Impossible de télécharger la licence');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { licenseData, licenseInfo, publicKeyDownloaded, verificationInfo } = await downloadResponse.json();
|
|
67
|
+
|
|
68
|
+
setProgress('Vérification de la licence...');
|
|
69
|
+
|
|
70
|
+
// La licence est maintenant en état PENDING
|
|
71
|
+
// Sauvegarder les données pour l'activation ultérieure
|
|
72
|
+
const pendingLicense = {
|
|
73
|
+
licenseData,
|
|
74
|
+
licenseInfo: {
|
|
75
|
+
...licenseInfo,
|
|
76
|
+
status: 'PENDING'
|
|
77
|
+
},
|
|
78
|
+
activationCode: activationCode.toUpperCase(),
|
|
79
|
+
verificationInfo,
|
|
80
|
+
publicKeyDownloaded,
|
|
81
|
+
downloadedAt: new Date().toISOString()
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Sauvegarder dans le localStorage comme licence en attente
|
|
85
|
+
localStorage.setItem('pending_license', JSON.stringify(pendingLicense));
|
|
86
|
+
|
|
87
|
+
setSuccess('Licence téléchargée et vérifiée avec succès !');
|
|
88
|
+
setProgress(null);
|
|
89
|
+
|
|
90
|
+
// Informer le parent que la licence est prête pour activation
|
|
91
|
+
setTimeout(() => {
|
|
92
|
+
onActivationSuccess?.({
|
|
93
|
+
...licenseInfo,
|
|
94
|
+
status: 'PENDING',
|
|
95
|
+
pendingData: pendingLicense
|
|
96
|
+
});
|
|
97
|
+
}, 1500);
|
|
98
|
+
|
|
99
|
+
} catch (err: any) {
|
|
100
|
+
console.error('Erreur d\'activation:', err);
|
|
101
|
+
setProgress(null);
|
|
102
|
+
|
|
103
|
+
if (err.message.includes('fetch')) {
|
|
104
|
+
setError('Impossible de se connecter au serveur. Vérifiez votre connexion internet.');
|
|
105
|
+
} else if (err.message.includes('404')) {
|
|
106
|
+
setError('Code d\'activation non trouvé. Vérifiez votre code.');
|
|
107
|
+
} else if (err.message.includes('expired')) {
|
|
108
|
+
setError('Ce code d\'activation a expiré.');
|
|
109
|
+
} else if (err.message.includes('used')) {
|
|
110
|
+
setError('Ce code a déjà été utilisé le nombre maximum de fois.');
|
|
111
|
+
} else {
|
|
112
|
+
setError(err.message || 'Erreur lors de l\'activation de la licence');
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
setLoading(false);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const clearForm = () => {
|
|
120
|
+
setActivationCode('');
|
|
121
|
+
setError(null);
|
|
122
|
+
setSuccess(null);
|
|
123
|
+
setProgress(null);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div className="max-w-md mx-auto p-8 bg-white rounded-xl shadow-lg">
|
|
128
|
+
<div className="text-center mb-8">
|
|
129
|
+
<Shield className="w-16 h-16 text-blue-500 mx-auto mb-4" />
|
|
130
|
+
<h2 className="text-2xl font-bold text-gray-800 mb-2">Activation Rapide</h2>
|
|
131
|
+
<p className="text-gray-600">
|
|
132
|
+
Entrez votre code d'activation pour télécharger et activer automatiquement votre licence
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{/* Messages d'état */}
|
|
137
|
+
{error && (
|
|
138
|
+
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
|
139
|
+
<div className="flex items-center">
|
|
140
|
+
<XCircle className="w-5 h-5 text-red-500 mr-2 flex-shrink-0" />
|
|
141
|
+
<p className="text-red-700 text-sm">{error}</p>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{success && (
|
|
147
|
+
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
|
|
148
|
+
<div className="flex items-center">
|
|
149
|
+
<CheckCircle className="w-5 h-5 text-green-500 mr-2 flex-shrink-0" />
|
|
150
|
+
<p className="text-green-700 text-sm">{success}</p>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
{progress && (
|
|
156
|
+
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
|
157
|
+
<div className="flex items-center">
|
|
158
|
+
<Loader2 className="w-5 h-5 text-blue-500 mr-2 animate-spin flex-shrink-0" />
|
|
159
|
+
<p className="text-blue-700 text-sm">{progress}</p>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{/* Code d'activation */}
|
|
165
|
+
<div className="mb-6">
|
|
166
|
+
<label className="block text-sm font-medium text-gray-700 mb-3">
|
|
167
|
+
<Key className="w-4 h-4 inline mr-2" />
|
|
168
|
+
Code d'activation
|
|
169
|
+
</label>
|
|
170
|
+
<input
|
|
171
|
+
type="text"
|
|
172
|
+
value={activationCode}
|
|
173
|
+
onChange={(e) => {
|
|
174
|
+
// Auto-format with dashes
|
|
175
|
+
let value = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
176
|
+
if (value.length > 4) value = value.slice(0, 4) + '-' + value.slice(4);
|
|
177
|
+
if (value.length > 9) value = value.slice(0, 9) + '-' + value.slice(9);
|
|
178
|
+
if (value.length > 14) value = value.slice(0, 14) + '-' + value.slice(14);
|
|
179
|
+
if (value.length > 19) value = value.slice(0, 19);
|
|
180
|
+
setActivationCode(value);
|
|
181
|
+
}}
|
|
182
|
+
placeholder="XXXX-XXXX-XXXX-XXXX"
|
|
183
|
+
maxLength={19}
|
|
184
|
+
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 font-mono text-center tracking-widest text-lg"
|
|
185
|
+
disabled={loading}
|
|
186
|
+
/>
|
|
187
|
+
<p className="text-xs text-gray-500 mt-2">
|
|
188
|
+
Entrez le code d'activation fourni lors de l'achat
|
|
189
|
+
</p>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{/* Info box */}
|
|
193
|
+
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
|
194
|
+
<div className="flex items-start">
|
|
195
|
+
<Download className="w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0" />
|
|
196
|
+
<div>
|
|
197
|
+
<h4 className="text-sm font-medium text-blue-800 mb-1">Activation automatique</h4>
|
|
198
|
+
<p className="text-xs text-blue-700">
|
|
199
|
+
La licence sera automatiquement téléchargée depuis nos serveurs et activée sur cette machine.
|
|
200
|
+
</p>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
{/* Boutons d'action */}
|
|
206
|
+
<div className="space-y-3">
|
|
207
|
+
<button
|
|
208
|
+
onClick={handleActivation}
|
|
209
|
+
disabled={!activationCode.trim() || loading}
|
|
210
|
+
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 flex items-center justify-center"
|
|
211
|
+
>
|
|
212
|
+
{loading ? (
|
|
213
|
+
<>
|
|
214
|
+
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
|
215
|
+
Activation en cours...
|
|
216
|
+
</>
|
|
217
|
+
) : (
|
|
218
|
+
<>
|
|
219
|
+
<Download className="w-5 h-5 mr-2" />
|
|
220
|
+
Télécharger et Activer
|
|
221
|
+
</>
|
|
222
|
+
)}
|
|
223
|
+
</button>
|
|
224
|
+
|
|
225
|
+
<div className="flex space-x-3">
|
|
226
|
+
<button
|
|
227
|
+
onClick={clearForm}
|
|
228
|
+
disabled={loading}
|
|
229
|
+
className="flex-1 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 transition-colors"
|
|
230
|
+
>
|
|
231
|
+
Effacer
|
|
232
|
+
</button>
|
|
233
|
+
|
|
234
|
+
{onBack && (
|
|
235
|
+
<button
|
|
236
|
+
onClick={onBack}
|
|
237
|
+
disabled={loading}
|
|
238
|
+
className="flex-1 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 transition-colors"
|
|
239
|
+
>
|
|
240
|
+
Retour
|
|
241
|
+
</button>
|
|
242
|
+
)}
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
{/* Lien vers activation manuelle */}
|
|
246
|
+
{onSwitchToManual && (
|
|
247
|
+
<button
|
|
248
|
+
onClick={onSwitchToManual}
|
|
249
|
+
disabled={loading}
|
|
250
|
+
className="w-full text-sm text-blue-500 hover:text-blue-700 font-medium transition-colors"
|
|
251
|
+
>
|
|
252
|
+
Activation manuelle avec fichier de licence →
|
|
253
|
+
</button>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
{/* Footer */}
|
|
258
|
+
<div className="mt-6 pt-6 border-t border-gray-200 text-center">
|
|
259
|
+
<p className="text-xs text-gray-500">
|
|
260
|
+
Vous n'avez pas de code d'activation ?
|
|
261
|
+
</p>
|
|
262
|
+
<a
|
|
263
|
+
href={serverUrl}
|
|
264
|
+
target="_blank"
|
|
265
|
+
rel="noopener noreferrer"
|
|
266
|
+
className="text-sm text-blue-500 hover:text-blue-700 font-medium"
|
|
267
|
+
>
|
|
268
|
+
Obtenir une licence →
|
|
269
|
+
</a>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
);
|
|
273
|
+
}
|