PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.7
Yatra – Travel Booking & Tour Operator Software v3.0.2.7
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / pages / License.tsx

License.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.2.7, at resources/js/pages/License.tsx

1,094 lines 44.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useState, useEffect } from "react";
2 import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3 import { apiClient } from "../lib/api-client";
4 import { __ } from "../lib/i18n";
5 import { useToast } from "../components/ui/toast";
6 import {
7 Key,
8 Check,
9 XCircle,
10 ExternalLink,
11 Eye,
12 EyeOff,
13 RefreshCw,
14 Bug,
15 ChevronDown,
16 } from "lucide-react";
17
18 interface LicenseInfo {
19 key: string;
20 status: string;
21 last_checked: number;
22 server_response: {
23 expires?: string;
24 customer_name?: string;
25 customer_email?: string;
26 license_limit?: number;
27 site_count?: number;
28 activations_left?: number;
29 };
30 }
31
32 interface LicenseData {
33 is_pro: boolean;
34 license_info?: LicenseInfo;
35 upgrade_url: string;
36 }
37
38 const License: React.FC = () => {
39 const [licenseKey, setLicenseKey] = useState("");
40 const [showKey, setShowKey] = useState(false);
41 const [debugMode, setDebugMode] = useState(false);
42 const [debugData, setDebugData] = useState<any>(null);
43 const [showDeactivateConfirm, setShowDeactivateConfirm] = useState(false);
44 const queryClient = useQueryClient();
45 const { showToast } = useToast();
46
47 // Check if Pro is active from global config
48 const yatraAdmin = (window as any)?.yatraAdmin;
49 const isPro = yatraAdmin?.isPro || false;
50
51 // Fetch license data
52 const { data: licenseData, isLoading } = useQuery<LicenseData>({
53 queryKey: ["license"],
54 queryFn: async () => {
55 const response = await apiClient.get("/license");
56
57 return response;
58 },
59 });
60
61 // Set license key from data
62 useEffect(() => {
63 if (licenseData?.license_info?.key) {
64 setLicenseKey(licenseData.license_info.key);
65 } else {
66 }
67 }, [licenseData]);
68
69 // Activate license mutation
70 const activateMutation = useMutation({
71 mutationFn: async (key: string) => {
72 try {
73 const response = await apiClient.post("/license/activate", {
74 license_key: key,
75 });
76
77 return response;
78 } catch (error) {
79 console.error("License activation error:", error);
80 throw error;
81 }
82 },
83 onSuccess: (data) => {
84 // Always store debug data (will be shown if debug mode is enabled)
85
86 const debugInfo = {
87 type: "activate",
88 request: { license_key: licenseKey },
89 response: data,
90 timestamp: new Date().toISOString(),
91 };
92
93 setDebugData(debugInfo);
94
95 if (data?.status === "valid") {
96 showToast(data.notice || "License activated successfully!", "success");
97 // Refetch license data to get updated status
98 queryClient.invalidateQueries({ queryKey: ["license"] });
99 // Dispatch event for real-time badge update
100 window.dispatchEvent(
101 new CustomEvent("yatra-license-status-updated", {
102 detail: { status: "active" },
103 }),
104 );
105 } else {
106 showToast(data?.notice || "License activation failed.", "error");
107 }
108 },
109 onError: (error: any) => {
110 const errorMsg =
111 error.response?.data?.message ||
112 "Failed to activate license. Please try again.";
113 showToast(errorMsg, "error");
114 // Always store debug data (will be shown if debug mode is enabled)
115 setDebugData({
116 type: "activate",
117 request: { license_key: licenseKey },
118 response: error.response?.data,
119 error: error.response?.data || error.message,
120 timestamp: new Date().toISOString(),
121 });
122 },
123 });
124
125 // Deactivate license mutation
126 const deactivateMutation = useMutation({
127 mutationFn: async () => {
128 const response = await apiClient.post("/license/deactivate");
129 return response.data;
130 },
131 onSuccess: (data) => {
132 // Always store debug data (will be shown if debug mode is enabled)
133 setDebugData({
134 type: "deactivate",
135 request: {},
136 response: data,
137 timestamp: new Date().toISOString(),
138 });
139
140 setLicenseKey("");
141 showToast(data?.notice || "License deactivated successfully!", "success");
142 // Refetch license data to get updated status
143 queryClient.invalidateQueries({ queryKey: ["license"] });
144 // Dispatch event for real-time badge update
145 window.dispatchEvent(
146 new CustomEvent("yatra-license-status-updated", {
147 detail: { status: "inactive" },
148 }),
149 );
150 },
151 onError: (error: any) => {
152 const errorMsg =
153 error.response?.data?.message ||
154 "Failed to deactivate license. Please try again.";
155 showToast(errorMsg, "error");
156 // Always store debug data (will be shown if debug mode is enabled)
157 setDebugData({
158 type: "deactivate",
159 request: {},
160 response: error.response?.data,
161 error: error.response?.data || error.message,
162 timestamp: new Date().toISOString(),
163 });
164 },
165 });
166
167 // Save license mutation
168 const saveMutation = useMutation({
169 mutationFn: async (key: string) => {
170 const response = await apiClient.post("/license/save", {
171 license_key: key,
172 });
173 return response;
174 },
175 onSuccess: (data) => {
176 showToast(data?.notice || "License key saved successfully!", "success");
177 // Refetch license data to show the saved key
178 queryClient.invalidateQueries({ queryKey: ["license"] });
179 },
180 onError: (error: any) => {
181 const errorMsg =
182 error.response?.data?.message || "Failed to save license key.";
183 showToast(errorMsg, "error");
184 },
185 });
186
187 // Check license mutation
188 const checkMutation = useMutation({
189 mutationFn: async () => {
190 const response = await apiClient.post("/license/check");
191 return response;
192 },
193 onSuccess: (data) => {
194 // Always store debug data (will be shown if debug mode is enabled)
195 setDebugData({
196 type: "check",
197 request: {},
198 response: data,
199 timestamp: new Date().toISOString(),
200 });
201
202 showToast(data?.notice || "License status updated.", "success");
203 // Refetch license data to get updated status
204 queryClient.invalidateQueries({ queryKey: ["license"] });
205 // Dispatch event for real-time badge update with actual status from response
206 if (data?.license_info?.status) {
207 window.dispatchEvent(
208 new CustomEvent("yatra-license-status-updated", {
209 detail: { status: data.license_info.status },
210 }),
211 );
212 }
213 },
214 onError: (error: any) => {
215 const errorMsg =
216 error.response?.data?.message || "Failed to check license status.";
217 showToast(errorMsg, "error");
218 // Always store debug data (will be shown if debug mode is enabled)
219 setDebugData({
220 type: "check",
221 request: {},
222 response: error.response?.data,
223 error: error.response?.data || error.message,
224 timestamp: new Date().toISOString(),
225 });
226 },
227 });
228
229 const handleActivate = (e: React.FormEvent) => {
230 e.preventDefault();
231 if (!licenseKey.trim()) {
232 showToast("Please enter a license key.", "error");
233 return;
234 }
235 activateMutation.mutate(licenseKey);
236 };
237
238 const handleDeactivate = () => {
239 setShowDeactivateConfirm(true);
240 };
241
242 const confirmDeactivate = () => {
243 setShowDeactivateConfirm(false);
244 deactivateMutation.mutate();
245 };
246
247 const handleCheckStatus = () => {
248 checkMutation.mutate();
249 };
250
251 const maskLicenseKey = (key: string) => {
252 if (!key || key.length < 8) return key;
253 return key.substring(0, 4) + "••••••••••••" + key.substring(key.length - 4);
254 };
255
256 const getStatusBadge = (status: string) => {
257 const statusConfig: Record<
258 string,
259 { bg: string; text: string; label: string }
260 > = {
261 active: {
262 bg: "bg-green-100 dark:bg-green-900/20",
263 text: "text-green-800 dark:text-green-400",
264 label: "Active",
265 },
266 valid: {
267 bg: "bg-green-100 dark:bg-green-900/20",
268 text: "text-green-800 dark:text-green-400",
269 label: "Active",
270 },
271 inactive: {
272 bg: "bg-gray-100 dark:bg-gray-800",
273 text: "text-gray-800 dark:text-gray-300",
274 label: "Inactive",
275 },
276 expired: {
277 bg: "bg-red-100 dark:bg-red-900/20",
278 text: "text-red-800 dark:text-red-400",
279 label: "Expired",
280 },
281 disabled: {
282 bg: "bg-red-100 dark:bg-red-900/20",
283 text: "text-red-800 dark:text-red-400",
284 label: "Disabled",
285 },
286 invalid: {
287 bg: "bg-yellow-100 dark:bg-yellow-900/20",
288 text: "text-yellow-800 dark:text-yellow-400",
289 label: "Invalid",
290 },
291 };
292
293 const config = statusConfig[status] || statusConfig.inactive;
294
295 return (
296 <span
297 className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${config.bg} ${config.text}`}
298 >
299 {config.label}
300 </span>
301 );
302 };
303
304 // Skeleton loading component
305 const SkeletonLoader = () => (
306 <div className="max-w-4xl mx-auto p-6">
307 <div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
308 {/* Header Skeleton */}
309 <div className="border-b border-gray-200 dark:border-gray-700 px-6 py-4">
310 <div className="flex items-center gap-3">
311 <div className="w-6 h-6 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
312 <div className="h-8 w-48 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
313 </div>
314 </div>
315
316 {/* Content Skeleton */}
317 <div className="p-6 space-y-6">
318 <div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-6">
319 <div className="h-6 w-64 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-4"></div>
320 <div className="space-y-3">
321 <div className="h-4 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
322 <div className="h-4 w-3/4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
323 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse mt-4"></div>
324 </div>
325 </div>
326 </div>
327 </div>
328 </div>
329 );
330
331 if (isLoading) {
332 return <SkeletonLoader />;
333 }
334
335 // Yatra Free - Show upgrade message
336 if (!isPro) {
337 return (
338 <div className="max-w-4xl mx-auto p-6">
339 <div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
340 {/* Header */}
341 <div className="border-b border-gray-200 dark:border-gray-700 px-6 py-4">
342 <div className="flex items-center gap-3">
343 <Key className="w-6 h-6 text-gray-600 dark:text-gray-400" />
344 <h1 className="text-2xl font-semibold text-gray-900 dark:text-white">
345 {__("License", "yatra")}
346 </h1>
347 </div>
348 </div>
349
350 {/* Content */}
351 <div className="p-6">
352 <div className="text-center py-12">
353 <div className="inline-flex items-center justify-center w-16 h-16 bg-blue-100 dark:bg-blue-900/20 rounded-full mb-4">
354 <Key className="w-8 h-8 text-blue-600 dark:text-blue-400" />
355 </div>
356
357 <h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
358 Yatra Free Version
359 </h2>
360
361 <p className="text-gray-600 dark:text-gray-400 mb-6 max-w-md mx-auto">
362 You're using the free version of Yatra. No license is required
363 for the free version.
364 </p>
365
366 <div className="bg-blue-50 dark:bg-blue-900/10 border border-blue-200 dark:border-blue-800 rounded-lg p-6 mb-6 max-w-2xl mx-auto">
367 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
368 Upgrade to Yatra Pro
369 </h3>
370 <p className="text-gray-600 dark:text-gray-400 mb-4">
371 Unlock premium features with Yatra Pro:
372 </p>
373 <ul className="text-left space-y-2 mb-6">
374 <li className="flex items-start gap-2">
375 <Check className="w-5 h-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
376 <span className="text-gray-700 dark:text-gray-300">
377 Dynamic Pricing & Revenue Management
378 </span>
379 </li>
380 <li className="flex items-start gap-2">
381 <Check className="w-5 h-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
382 <span className="text-gray-700 dark:text-gray-300">
383 {__("Advanced Booking Management", "yatra")}
384 </span>
385 </li>
386 <li className="flex items-start gap-2">
387 <Check className="w-5 h-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
388 <span className="text-gray-700 dark:text-gray-300">
389 {__("Premium Payment Gateways", "yatra")}
390 </span>
391 </li>
392 <li className="flex items-start gap-2">
393 <Check className="w-5 h-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
394 <span className="text-gray-700 dark:text-gray-300">
395 Priority Support & Updates
396 </span>
397 </li>
398 </ul>
399 <a
400 href="https://wpyatra.com/pricing"
401 target="_blank"
402 rel="noopener noreferrer"
403 className="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors"
404 >
405 Upgrade to Pro
406 <ExternalLink className="w-4 h-4" />
407 </a>
408 </div>
409 </div>
410 </div>
411 </div>
412 </div>
413 );
414 }
415
416 // Yatra Pro - Show license management
417 const licenseInfo = licenseData?.license_info;
418 const isActive =
419 licenseInfo?.status === "active" || licenseInfo?.status === "valid";
420
421 return (
422 <div className="max-w-4xl mx-auto p-6">
423 <div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
424 {/* Header */}
425 <div className="border-b border-gray-200 dark:border-gray-700 px-6 py-4">
426 <div className="flex items-center justify-between">
427 <div className="flex items-center gap-3">
428 <Key className="w-6 h-6 text-gray-600 dark:text-gray-400" />
429 <h1 className="text-2xl font-semibold text-gray-900 dark:text-white">
430 {__("License Management", "yatra")}
431 </h1>
432 </div>
433 <div className="flex items-center gap-3">
434 {licenseInfo?.status && getStatusBadge(licenseInfo.status)}
435 <button
436 onClick={() => setDebugMode(!debugMode)}
437 className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
438 debugMode
439 ? "bg-purple-100 dark:bg-purple-900/20 text-purple-700 dark:text-purple-400"
440 : "bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600"
441 }`}
442 title="Toggle debug mode to see API requests and responses"
443 >
444 <Bug className="w-4 h-4" />
445 Debug
446 </button>
447 </div>
448 </div>
449 </div>
450
451 {/* Content */}
452 <div className="p-6 space-y-6">
453 {/* License Activation Form */}
454 {!isActive && (
455 <div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-6">
456 <h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
457 Activate Your License
458 </h2>
459 <form onSubmit={handleActivate} className="space-y-4">
460 <div>
461 <label
462 htmlFor="license-key"
463 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
464 >
465 License Key
466 </label>
467 <input
468 type="text"
469 id="license-key"
470 value={licenseKey}
471 onChange={(e) => setLicenseKey(e.target.value)}
472 placeholder="Enter your license key"
473 className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
474 disabled={activateMutation.isPending}
475 />
476 </div>
477 <div className="flex flex-wrap gap-3">
478 <button
479 type="button"
480 onClick={(e) => {
481 e.preventDefault();
482 if (!licenseKey.trim()) {
483 showToast("Please enter a license key.", "error");
484 return;
485 }
486 // Save the license key without activating
487 saveMutation.mutate(licenseKey);
488 }}
489 disabled={
490 activateMutation.isPending || saveMutation.isPending
491 }
492 className="inline-flex items-center justify-center gap-2 px-6 py-2.5 bg-gray-900 hover:bg-black dark:bg-gray-800 dark:hover:bg-gray-900 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition-all shadow-md hover:shadow-lg border border-gray-800 dark:border-gray-700"
493 >
494 {saveMutation.isPending ? (
495 <>
496 <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
497 Saving...
498 </>
499 ) : (
500 <>
501 <Key className="w-4 h-4" />
502 Save
503 </>
504 )}
505 </button>
506 <button
507 type="submit"
508 disabled={activateMutation.isPending}
509 className="inline-flex items-center gap-2 px-6 py-2.5 bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 disabled:bg-gray-400 dark:disabled:bg-gray-500 text-white font-medium rounded-lg transition-colors shadow-sm"
510 >
511 {activateMutation.isPending ? (
512 <>
513 <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
514 Activating...
515 </>
516 ) : (
517 <>
518 <Check className="w-4 h-4" />
519 Save & Activate
520 </>
521 )}
522 </button>
523 </div>
524 </form>
525 <p className="mt-4 text-sm text-gray-600 dark:text-gray-400">
526 Don't have a license key?{" "}
527 <a
528 href="https://store.mantrabrain.com/account/"
529 target="_blank"
530 rel="noopener noreferrer"
531 className="text-blue-600 dark:text-blue-400 hover:underline"
532 >
533 Get it from your account
534 </a>
535 </p>
536 </div>
537 )}
538
539 {/* Active License Information */}
540 {isActive && licenseInfo && (
541 <div className="space-y-6">
542 {/* License Key Display */}
543 <div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-6">
544 <div className="flex items-center justify-between mb-4">
545 <h3 className="text-lg font-semibold text-gray-900 dark:text-white">
546 {__("License Key", "yatra")}
547 </h3>
548 <button
549 onClick={() => setShowKey(!showKey)}
550 className="text-sm text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1"
551 >
552 {showKey ? (
553 <>
554 <EyeOff className="w-4 h-4" />
555 Hide
556 </>
557 ) : (
558 <>
559 <Eye className="w-4 h-4" />
560 Show
561 </>
562 )}
563 </button>
564 </div>
565 <div className="font-mono text-sm bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded px-4 py-3 text-gray-900 dark:text-white">
566 {showKey ? licenseInfo.key : maskLicenseKey(licenseInfo.key)}
567 </div>
568 </div>
569
570 {/* License Details */}
571 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
572 {licenseInfo.server_response?.customer_name && (
573 <div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
574 <div className="text-sm text-gray-600 dark:text-gray-400 mb-1">
575 {__("Licensed To", "yatra")}
576 </div>
577 <div className="font-medium text-gray-900 dark:text-white">
578 {licenseInfo.server_response.customer_name}
579 </div>
580 </div>
581 )}
582
583 {licenseInfo.server_response?.customer_email && (
584 <div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
585 <div className="text-sm text-gray-600 dark:text-gray-400 mb-1">
586 {__("Email", "yatra")}
587 </div>
588 <div className="font-medium text-gray-900 dark:text-white">
589 {licenseInfo.server_response.customer_email}
590 </div>
591 </div>
592 )}
593
594 {licenseInfo.server_response?.expires &&
595 licenseInfo.server_response.expires !== "lifetime" && (
596 <div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
597 <div className="text-sm text-gray-600 dark:text-gray-400 mb-1">
598 {__("Expires", "yatra")}
599 </div>
600 <div className="font-medium text-gray-900 dark:text-white">
601 {(() => {
602 try {
603 const date = new Date(
604 licenseInfo.server_response.expires,
605 );
606 return isNaN(date.getTime())
607 ? licenseInfo.server_response.expires
608 : date.toLocaleDateString(undefined, {
609 year: "numeric",
610 month: "long",
611 day: "numeric",
612 });
613 } catch {
614 return licenseInfo.server_response.expires;
615 }
616 })()}
617 </div>
618 </div>
619 )}
620
621 {licenseInfo.server_response?.expires === "lifetime" && (
622 <div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
623 <div className="text-sm text-gray-600 dark:text-gray-400 mb-1">
624 {__("Expires", "yatra")}
625 </div>
626 <div className="font-medium text-green-600 dark:text-green-400">
627 {__("Lifetime License", "yatra")}
628 </div>
629 </div>
630 )}
631
632 {licenseInfo.server_response?.site_count !== undefined && (
633 <div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
634 <div className="text-sm text-gray-600 dark:text-gray-400 mb-1">
635 {__("Active Sites", "yatra")}
636 </div>
637 <div className="font-medium text-gray-900 dark:text-white">
638 {licenseInfo.server_response.site_count} /{" "}
639 {licenseInfo.server_response.license_limit || "∞"}
640 </div>
641 </div>
642 )}
643 </div>
644
645 {/* Actions */}
646 <div className="flex flex-wrap gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
647 <button
648 onClick={handleCheckStatus}
649 disabled={checkMutation.isPending}
650 className="px-4 py-2 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-900 dark:text-white font-medium rounded-lg transition-colors flex items-center gap-2"
651 >
652 {checkMutation.isPending ? (
653 <>
654 <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900 dark:border-white"></div>
655 Checking...
656 </>
657 ) : (
658 <>
659 <RefreshCw className="w-4 h-4" />
660 Check Status
661 </>
662 )}
663 </button>
664
665 <button
666 onClick={handleDeactivate}
667 disabled={deactivateMutation.isPending}
668 className="px-4 py-2 bg-red-600 hover:bg-red-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors flex items-center gap-2"
669 >
670 {deactivateMutation.isPending ? (
671 <>
672 <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
673 Deactivating...
674 </>
675 ) : (
676 <>
677 <XCircle className="w-4 h-4" />
678 Deactivate License
679 </>
680 )}
681 </button>
682 </div>
683 </div>
684 )}
685
686 {/* Help Section */}
687 <div className="bg-blue-50 dark:bg-blue-900/10 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
688 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">
689 Need Help?
690 </h3>
691 <p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
692 If you're having trouble with your license, please contact our
693 support team.
694 </p>
695 <div className="flex flex-wrap gap-3">
696 <a
697 href="https://store.mantrabrain.com/account/"
698 target="_blank"
699 rel="noopener noreferrer"
700 className="text-sm text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1"
701 >
702 Manage License
703 <ExternalLink className="w-3 h-3" />
704 </a>
705 <a
706 href="https://mantrabrain.com/contact/"
707 target="_blank"
708 rel="noopener noreferrer"
709 className="text-sm text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1"
710 >
711 Contact Support
712 <ExternalLink className="w-3 h-3" />
713 </a>
714 </div>
715 </div>
716
717 {/* Debug Panel */}
718 {debugMode && (
719 <div className="bg-purple-50 dark:bg-purple-900/10 border border-purple-200 dark:border-purple-800 rounded-lg p-4">
720 <div className="flex items-center justify-between mb-3">
721 <h3 className="text-sm font-semibold text-purple-900 dark:text-purple-300 flex items-center gap-2">
722 <Bug className="w-4 h-4" />
723 Debug Information
724 </h3>
725 {debugData && (
726 <button
727 onClick={() => setDebugData(null)}
728 className="text-xs text-purple-600 dark:text-purple-400 hover:underline"
729 >
730 {__("Clear", "yatra")}
731 </button>
732 )}
733 </div>
734
735 {debugData ? (
736 <div className="space-y-3">
737 {/* Operation Type */}
738 <div>
739 <span className="text-xs font-semibold text-purple-700 dark:text-purple-300 uppercase">
740 Operation: {debugData.type}
741 </span>
742 <span className="ml-3 text-xs text-gray-500 dark:text-gray-400">
743 {debugData.timestamp}
744 </span>
745 </div>
746
747 {/* Full Response Body */}
748 <div>
749 <div className="flex items-center justify-between mb-2">
750 <div className="flex items-center gap-2">
751 <ChevronDown className="w-3 h-3 text-blue-600 dark:text-blue-400" />
752 <span className="text-xs font-semibold text-blue-700 dark:text-blue-300">
753 Full Response Body:
754 </span>
755 </div>
756 <button
757 onClick={() => {
758 const text = JSON.stringify(
759 debugData.response,
760 null,
761 2,
762 );
763 if (navigator?.clipboard?.writeText) {
764 navigator.clipboard.writeText(text);
765 showToast(
766 "Response copied to clipboard!",
767 "success",
768 );
769 } else {
770 // Fallback for older browsers
771 const textArea = document.createElement("textarea");
772 textArea.value = text;
773 document.body.appendChild(textArea);
774 textArea.select();
775 try {
776 document.execCommand("copy");
777 showToast(
778 "Response copied to clipboard!",
779 "success",
780 );
781 } catch (err) {
782 showToast("Failed to copy to clipboard", "error");
783 }
784 document.body.removeChild(textArea);
785 }
786 }}
787 className="text-xs text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1"
788 >
789 <svg
790 className="w-3 h-3"
791 fill="none"
792 stroke="currentColor"
793 viewBox="0 0 24 24"
794 >
795 <path
796 strokeLinecap="round"
797 strokeLinejoin="round"
798 strokeWidth={2}
799 d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
800 />
801 </svg>
802 Copy
803 </button>
804 </div>
805 <pre className="bg-white dark:bg-gray-900 border border-blue-200 dark:border-blue-800 rounded p-3 text-xs overflow-x-auto max-h-96 overflow-y-auto">
806 <code className="text-gray-800 dark:text-gray-200">
807 {JSON.stringify(debugData.response, null, 2)}
808 </code>
809 </pre>
810 </div>
811
812 {/* EDD API Request */}
813 {debugData.response?.edd_api_request && (
814 <div>
815 <div className="flex items-center justify-between mb-2">
816 <div className="flex items-center gap-2">
817 <ChevronDown className="w-3 h-3 text-purple-600 dark:text-purple-400" />
818 <span className="text-xs font-semibold text-purple-700 dark:text-purple-300">
819 EDD API Request (to store.mantrabrain.com):
820 </span>
821 </div>
822 <button
823 onClick={() => {
824 const text = JSON.stringify(
825 debugData.response.edd_api_request,
826 null,
827 2,
828 );
829 if (navigator?.clipboard?.writeText) {
830 navigator.clipboard.writeText(text);
831 showToast(
832 "Request copied to clipboard!",
833 "success",
834 );
835 } else {
836 // Fallback for older browsers
837 const textArea =
838 document.createElement("textarea");
839 textArea.value = text;
840 document.body.appendChild(textArea);
841 textArea.select();
842 try {
843 document.execCommand("copy");
844 showToast(
845 "Request copied to clipboard!",
846 "success",
847 );
848 } catch (err) {
849 showToast(
850 "Failed to copy to clipboard",
851 "error",
852 );
853 }
854 document.body.removeChild(textArea);
855 }
856 }}
857 className="text-xs text-purple-600 dark:text-purple-400 hover:underline flex items-center gap-1"
858 >
859 <svg
860 className="w-3 h-3"
861 fill="none"
862 stroke="currentColor"
863 viewBox="0 0 24 24"
864 >
865 <path
866 strokeLinecap="round"
867 strokeLinejoin="round"
868 strokeWidth={2}
869 d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
870 />
871 </svg>
872 Copy
873 </button>
874 </div>
875 <pre className="bg-white dark:bg-gray-900 border border-purple-200 dark:border-purple-800 rounded p-3 text-xs overflow-x-auto">
876 <code className="text-gray-800 dark:text-gray-200">
877 {JSON.stringify(
878 debugData.response.edd_api_request,
879 null,
880 2,
881 )}
882 </code>
883 </pre>
884 </div>
885 )}
886
887 {/* EDD API Response */}
888 {debugData.response?.edd_api_response && (
889 <div>
890 <div className="flex items-center justify-between mb-2">
891 <div className="flex items-center gap-2">
892 <ChevronDown className="w-3 h-3 text-green-600 dark:text-green-400" />
893 <span className="text-xs font-semibold text-green-700 dark:text-green-300">
894 EDD API Response (from store.mantrabrain.com):
895 </span>
896 </div>
897 <button
898 onClick={() => {
899 const text = JSON.stringify(
900 debugData.response.edd_api_response,
901 null,
902 2,
903 );
904 if (navigator?.clipboard?.writeText) {
905 navigator.clipboard.writeText(text);
906 showToast(
907 "Response copied to clipboard!",
908 "success",
909 );
910 } else {
911 // Fallback for older browsers
912 const textArea =
913 document.createElement("textarea");
914 textArea.value = text;
915 document.body.appendChild(textArea);
916 textArea.select();
917 try {
918 document.execCommand("copy");
919 showToast(
920 "Response copied to clipboard!",
921 "success",
922 );
923 } catch (err) {
924 showToast(
925 "Failed to copy to clipboard",
926 "error",
927 );
928 }
929 document.body.removeChild(textArea);
930 }
931 }}
932 className="text-xs text-green-600 dark:text-green-400 hover:underline flex items-center gap-1"
933 >
934 <svg
935 className="w-3 h-3"
936 fill="none"
937 stroke="currentColor"
938 viewBox="0 0 24 24"
939 >
940 <path
941 strokeLinecap="round"
942 strokeLinejoin="round"
943 strokeWidth={2}
944 d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
945 />
946 </svg>
947 Copy
948 </button>
949 </div>
950 <pre className="bg-white dark:bg-gray-900 border border-green-200 dark:border-green-800 rounded p-3 text-xs overflow-x-auto">
951 <code className="text-gray-800 dark:text-gray-200">
952 {JSON.stringify(
953 debugData.response.edd_api_response,
954 null,
955 2,
956 )}
957 </code>
958 </pre>
959 </div>
960 )}
961
962 {/* Error Data */}
963 {debugData.error && (
964 <div>
965 <div className="flex items-center justify-between mb-2">
966 <div className="flex items-center gap-2">
967 <ChevronDown className="w-3 h-3 text-red-600 dark:text-red-400" />
968 <span className="text-xs font-semibold text-red-700 dark:text-red-300">
969 Error Data:
970 </span>
971 </div>
972 <button
973 onClick={() => {
974 const text = JSON.stringify(
975 debugData.error,
976 null,
977 2,
978 );
979 if (navigator?.clipboard?.writeText) {
980 navigator.clipboard.writeText(text);
981 showToast(
982 "Error copied to clipboard!",
983 "success",
984 );
985 } else {
986 // Fallback for older browsers
987 const textArea =
988 document.createElement("textarea");
989 textArea.value = text;
990 document.body.appendChild(textArea);
991 textArea.select();
992 try {
993 document.execCommand("copy");
994 showToast(
995 "Error copied to clipboard!",
996 "success",
997 );
998 } catch (err) {
999 showToast(
1000 "Failed to copy to clipboard",
1001 "error",
1002 );
1003 }
1004 document.body.removeChild(textArea);
1005 }
1006 }}
1007 className="text-xs text-red-600 dark:text-red-400 hover:underline flex items-center gap-1"
1008 >
1009 <svg
1010 className="w-3 h-3"
1011 fill="none"
1012 stroke="currentColor"
1013 viewBox="0 0 24 24"
1014 >
1015 <path
1016 strokeLinecap="round"
1017 strokeLinejoin="round"
1018 strokeWidth={2}
1019 d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
1020 />
1021 </svg>
1022 Copy
1023 </button>
1024 </div>
1025 <pre className="bg-white dark:bg-gray-900 border border-red-200 dark:border-red-800 rounded p-3 text-xs overflow-x-auto">
1026 <code className="text-gray-800 dark:text-gray-200">
1027 {JSON.stringify(debugData.error, null, 2)}
1028 </code>
1029 </pre>
1030 </div>
1031 )}
1032 </div>
1033 ) : (
1034 <p className="text-sm text-purple-600 dark:text-purple-400">
1035 Perform a license operation (activate, deactivate, or check
1036 status) to see debug information here.
1037 </p>
1038 )}
1039 </div>
1040 )}
1041 </div>
1042 </div>
1043
1044 {/* Deactivate Confirmation Dialog */}
1045 {showDeactivateConfirm && (
1046 <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm px-4">
1047 <div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full p-6">
1048 <div className="flex items-start gap-4">
1049 <div className="flex-shrink-0">
1050 <div className="w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/20 flex items-center justify-center">
1051 <XCircle className="w-6 h-6 text-red-600 dark:text-red-400" />
1052 </div>
1053 </div>
1054 <div className="flex-1">
1055 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
1056 Deactivate License
1057 </h3>
1058 <p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
1059 Are you sure you want to deactivate this license? You will
1060 lose support and updates for Yatra Pro.
1061 </p>
1062 <div className="flex gap-3 justify-end">
1063 <button
1064 onClick={() => setShowDeactivateConfirm(false)}
1065 className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors"
1066 >
1067 Cancel
1068 </button>
1069 <button
1070 onClick={confirmDeactivate}
1071 disabled={deactivateMutation.isPending}
1072 className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 disabled:bg-gray-400 rounded-lg transition-colors flex items-center gap-2"
1073 >
1074 {deactivateMutation.isPending ? (
1075 <>
1076 <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
1077 Deactivating...
1078 </>
1079 ) : (
1080 "Deactivate"
1081 )}
1082 </button>
1083 </div>
1084 </div>
1085 </div>
1086 </div>
1087 </div>
1088 )}
1089 </div>
1090 );
1091 };
1092
1093 export default License;
1094