PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5.1
Yatra – Travel Booking & Tour Operator Software v3.0.5.1
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.5.1, at resources/js/pages/License.tsx

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