mirror of
https://github.com/Dokploy/website
synced 2025-06-26 18:16:01 +00: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(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
fetch(`${SERVER_LICENSE_URL}/license/session?sessionId=${sessionId}`, {
|
fetch(
|
||||||
method: "GET",
|
`${SERVER_LICENSE_URL}/stripe/get-license-from-session?sessionId=${sessionId}`,
|
||||||
headers: {
|
{
|
||||||
"Content-Type": "application/json",
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.error) {
|
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 { SERVER_LICENSE_URL } from "@/components/pricing";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
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 { useState } from "react";
|
||||||
import { toast } from "sonner";
|
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() {
|
export default function ResetLicensePage() {
|
||||||
|
const router = useRouter();
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [showOtp, setShowOtp] = useState(false);
|
const [showRender, setShowRender] = useState<"otp" | "email">("email");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [otp, setOtp] = useState("");
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const sendEmail = async (email: string) => {
|
||||||
e.preventDefault();
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await fetch(`${SERVER_LICENSE_URL}/license/verification`, {
|
const result = await fetch(`${SERVER_LICENSE_URL}/license/send-otp`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -25,7 +44,6 @@ export default function ResetLicensePage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = await result.json();
|
const data = await result.json();
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
toast.error(
|
toast.error(
|
||||||
@@ -38,7 +56,52 @@ export default function ResetLicensePage() {
|
|||||||
toast.success(
|
toast.success(
|
||||||
"We've sent you a code to verify your email. Please check your email for the code.",
|
"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) {
|
} catch (error) {
|
||||||
toast.error("Something went wrong. Please try again later.", {
|
toast.error("Something went wrong. Please try again later.", {
|
||||||
@@ -57,33 +120,85 @@ export default function ResetLicensePage() {
|
|||||||
Reset Your License
|
Reset Your License
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-4 text-lg text-muted-foreground">
|
<p className="mt-4 text-lg text-muted-foreground">
|
||||||
Enter your email address and we'll send you instructions to reset your
|
{showRender === "otp"
|
||||||
license.
|
? "Enter the verification code sent to your email."
|
||||||
|
: "Enter your email address and we'll send you instructions to reset your license."}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form
|
{showRender === "email" ? (
|
||||||
onSubmit={handleSubmit}
|
<form
|
||||||
className="mt-10 flex flex-col items-center gap-4"
|
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}
|
|
||||||
>
|
>
|
||||||
{isLoading ? "Sending..." : "Reset License"}
|
<div className="w-full max-w-sm">
|
||||||
</Button>
|
<Input
|
||||||
</form>
|
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>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export function Pricing() {
|
|||||||
|
|
||||||
// Create checkout session
|
// Create checkout session
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${SERVER_LICENSE_URL!}/create-checkout-session`,
|
`${SERVER_LICENSE_URL!}/stripe/create-checkout-session`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
Reference in New Issue
Block a user