feat: update LicenseSuccess and ResetLicense pages with improved API endpoints

This commit is contained in:
Mauricio Siu
2025-03-23 18:11:47 -06:00
parent 054627e80d
commit cd0c731f0d
4 changed files with 318 additions and 38 deletions

View File

@@ -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) {

View 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;

View File

@@ -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>
);

View File

@@ -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: {