PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
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 2.0.10 All 83 releases
yatra / resources / js / pages / License.tsx

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

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