mirror of
https://github.com/Dokploy/website.git
synced 2026-06-18 13:45:25 +02:00
feat: update LicenseSuccess and ResetLicense pages with improved API endpoints
This commit is contained in:
@@ -41,12 +41,15 @@ export default function LicenseSuccess() {
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
|
||||
fetch(`${SERVER_LICENSE_URL}/license/session?sessionId=${sessionId}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
fetch(
|
||||
`${SERVER_LICENSE_URL}/stripe/get-license-from-session?sessionId=${sessionId}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
|
||||
162
apps/website/app/[locale]/license/view/page.tsx
Normal file
162
apps/website/app/[locale]/license/view/page.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
export const SERVER_LICENSE_URL =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:4002/api"
|
||||
: "https://licenses.dokploy.com";
|
||||
|
||||
const LicenseCard = ({ license, stripeSuscription }: any) => {
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-lg shadow-xl p-6 max-w-2xl mx-auto border border-gray-700">
|
||||
<div className="space-y-6">
|
||||
{/* License Information Section */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white mb-4">
|
||||
License Information
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-400">License ID</p>
|
||||
<p className="text-gray-200">{license.id}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-400">License Key</p>
|
||||
<p className="text-gray-200 break-all">{license.licenseKey}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-400">
|
||||
Activation Status
|
||||
</p>
|
||||
<p className="text-gray-200">
|
||||
{license.activatedAt ? "Activated" : "Not Activated"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-400">
|
||||
Last Verification
|
||||
</p>
|
||||
<p className="text-gray-200">
|
||||
{license.lastVerifiedAt || "Not Verified"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-400">Created At</p>
|
||||
<p className="text-gray-200">
|
||||
{new Date(license.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-400">
|
||||
Last Updated
|
||||
</p>
|
||||
<p className="text-gray-200">
|
||||
{new Date(license.updatedAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subscription Information Section */}
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-bold text-white">
|
||||
Subscription Information
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors duration-200 flex items-center space-x-2"
|
||||
// onClick={() => {
|
||||
// // Handle subscription management here
|
||||
// }}
|
||||
>
|
||||
<span>Manage Subscription</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-400">
|
||||
Subscription ID
|
||||
</p>
|
||||
<p className="text-gray-200">{stripeSuscription.id}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-400">Quantity</p>
|
||||
<p className="text-gray-200">
|
||||
{stripeSuscription.quantity} licenses
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-400">
|
||||
Billing Type
|
||||
</p>
|
||||
<p className="text-gray-200 capitalize">
|
||||
{stripeSuscription.billingType}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-400">
|
||||
Stripe Customer ID
|
||||
</p>
|
||||
<p className="text-gray-200">{license.stripeCustomerId}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ViewLicensePage = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ temporalId: string }>;
|
||||
}) => {
|
||||
const newParams = await searchParams;
|
||||
const temporalId = newParams.temporalId;
|
||||
const licences = await fetch(
|
||||
`${SERVER_LICENSE_URL}/license/all?temporalId=${temporalId}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
const data = await licences.json();
|
||||
|
||||
if (!data.success) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 items-center justify-center mx-auto py-8 px-4">
|
||||
<h1 className="text-3xl font-bold text-white mb-8 text-center">
|
||||
License Details
|
||||
</h1>
|
||||
<span className="text-red-500 text-center flex flex-col gap-2">
|
||||
{data.error}, Please try again in
|
||||
<Link href="/reset-license" className="text-white">
|
||||
Reset License
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
console.log(data);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 items-center justify-center mx-auto py-8 px-4">
|
||||
<h1 className="text-3xl font-bold text-white mb-8 text-center">
|
||||
License Details
|
||||
</h1>
|
||||
{data?.licenses?.map((item: any) => (
|
||||
<LicenseCard
|
||||
key={item.license.id}
|
||||
license={item.license}
|
||||
stripeSuscription={item.stripeSuscription}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewLicensePage;
|
||||
@@ -4,19 +4,38 @@ import { Container } from "@/components/Container";
|
||||
import { SERVER_LICENSE_URL } from "@/components/pricing";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from "@/components/ui/input-otp";
|
||||
import { ArrowLeft, ExternalLink } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface License {
|
||||
email: string;
|
||||
serverIp?: string[];
|
||||
licenseKey: string;
|
||||
productName: string;
|
||||
createdAt: string;
|
||||
lastVerifiedAt: string;
|
||||
billingType: string;
|
||||
activatedAt: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export default function ResetLicensePage() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState("");
|
||||
const [showOtp, setShowOtp] = useState(false);
|
||||
const [showRender, setShowRender] = useState<"otp" | "email">("email");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [otp, setOtp] = useState("");
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
const sendEmail = async (email: string) => {
|
||||
try {
|
||||
const result = await fetch(`${SERVER_LICENSE_URL}/license/verification`, {
|
||||
const result = await fetch(`${SERVER_LICENSE_URL}/license/send-otp`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -25,7 +44,6 @@ export default function ResetLicensePage() {
|
||||
});
|
||||
|
||||
const data = await result.json();
|
||||
console.log(data);
|
||||
|
||||
if (data.error) {
|
||||
toast.error(
|
||||
@@ -38,7 +56,52 @@ export default function ResetLicensePage() {
|
||||
toast.success(
|
||||
"We've sent you a code to verify your email. Please check your email for the code.",
|
||||
);
|
||||
setShowOtp(true);
|
||||
setShowRender("otp");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Something went wrong. Please try again later.", {
|
||||
duration: 15000,
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
await sendEmail(email);
|
||||
};
|
||||
|
||||
const handleVerifyOtp = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
if (otp.length !== 6) {
|
||||
toast.error("Please enter a valid 6-digit code.");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await fetch(`${SERVER_LICENSE_URL}/license/verify-otp`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email, otpCode: otp }),
|
||||
});
|
||||
|
||||
const data = await result.json();
|
||||
if (data.error) {
|
||||
toast.error("Error verifying code. Please try again.", {
|
||||
description: data.error,
|
||||
});
|
||||
} else {
|
||||
const temporalId = data.temporalId;
|
||||
console.log(temporalId);
|
||||
router.push(`/license/view?temporalId=${temporalId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Something went wrong. Please try again later.", {
|
||||
@@ -57,33 +120,85 @@ export default function ResetLicensePage() {
|
||||
Reset Your License
|
||||
</h1>
|
||||
<p className="mt-4 text-lg text-muted-foreground">
|
||||
Enter your email address and we'll send you instructions to reset your
|
||||
license.
|
||||
{showRender === "otp"
|
||||
? "Enter the verification code sent to your email."
|
||||
: "Enter your email address and we'll send you instructions to reset your license."}
|
||||
</p>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="mt-10 flex flex-col items-center gap-4"
|
||||
>
|
||||
<div className="w-full max-w-sm">
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="w-full"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full max-w-sm"
|
||||
disabled={isLoading}
|
||||
{showRender === "email" ? (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="mt-10 flex flex-col items-center gap-4"
|
||||
>
|
||||
{isLoading ? "Sending..." : "Reset License"}
|
||||
</Button>
|
||||
</form>
|
||||
<div className="w-full max-w-sm">
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="w-full"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full max-w-sm"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Sending..." : "Reset License"}
|
||||
</Button>
|
||||
</form>
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleVerifyOtp}
|
||||
className="mt-10 flex flex-col items-center gap-4"
|
||||
>
|
||||
<div className="w-full max-w-sm flex justify-center gap-2">
|
||||
<InputOTP
|
||||
value={otp}
|
||||
onChange={setOtp}
|
||||
maxLength={6}
|
||||
disabled={isLoading}
|
||||
required
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
sendEmail(email);
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full max-w-sm"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Verifying..." : "Verify Code"}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setShowRender("email")}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Back to Email
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -147,7 +147,7 @@ export function Pricing() {
|
||||
|
||||
// Create checkout session
|
||||
const response = await fetch(
|
||||
`${SERVER_LICENSE_URL!}/create-checkout-session`,
|
||||
`${SERVER_LICENSE_URL!}/stripe/create-checkout-session`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
||||
Reference in New Issue
Block a user