feat(licenses): enhance license validation and loading state management

- Added loading state management in the EnablePaidFeatures component to improve user experience during license validation.
- Updated validateLicense function to return detailed error messages for better feedback.
- Modified user API to return validation results instead of a boolean, enhancing error handling.
- Removed unused SQL files and updated package.json scripts for better development workflow.
This commit is contained in:
Mauricio Siu
2025-03-23 12:29:55 -06:00
parent 9e30525569
commit c8e6df4c29
9 changed files with 28 additions and 224 deletions

View File

@@ -16,6 +16,7 @@ import { useState, useEffect } from "react";
export const EnablePaidFeatures = () => {
const { data, refetch } = api.user.get.useQuery();
const [isLoading, setIsLoading] = useState(false);
const { mutateAsync: validateLicense } =
api.user.validateLicense.useMutation();
const { mutateAsync: update } = api.user.update.useMutation();
@@ -32,6 +33,7 @@ export const EnablePaidFeatures = () => {
toast.error("Please enter a license key");
return;
}
setIsLoading(true);
await validateLicense({
licenseKey,
})
@@ -40,7 +42,12 @@ export const EnablePaidFeatures = () => {
})
.catch((e) => {
console.error(e);
toast.error("Error validating license");
toast.error("Error validating license", {
description: e.message,
});
})
.finally(() => {
setIsLoading(false);
});
};
@@ -102,8 +109,12 @@ export const EnablePaidFeatures = () => {
className="w-full"
/>
</div>
<Button onClick={handleValidateLicense} variant="secondary">
Validate
<Button
onClick={handleValidateLicense}
variant="secondary"
disabled={isLoading}
>
{isLoading ? "Validating..." : "Validate"}
</Button>
</div>
)}

View File

@@ -148,21 +148,21 @@ export const userRouter = createTRPCRouter({
)
.mutation(async ({ input, ctx }) => {
const owner = await findUserById(ctx.user.ownerId);
const isValid = await validateLicense(
const result = await validateLicense(
input.licenseKey,
owner?.serverIp || "",
);
if (!isValid) {
if (!result.isValid) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid license key",
message: result.error,
});
}
await updateUser(ctx.user.id, {
licenseKey: input.licenseKey,
});
return isValid;
return result;
}),
getUserByToken: publicProcedure
.input(apiFindOneToken)

View File

@@ -1,9 +1,6 @@
const licensesUrl = process.env.LICENSES_URL || "http://localhost:4002";
export const validateLicense = async (
licenseKey: string,
serverIp: string,
): Promise<boolean> => {
export const validateLicense = async (licenseKey: string, serverIp: string) => {
const response = await fetch(`${licensesUrl}/api/validate`, {
method: "POST",
headers: {
@@ -17,5 +14,5 @@ export const validateLicense = async (
console.log("Validation errors:", data.error.issues);
}
return response.ok;
return data;
};

View File

@@ -1,22 +0,0 @@
CREATE TYPE "public"."billing_type" AS ENUM('monthly', 'annual');--> statement-breakpoint
CREATE TYPE "public"."license_status" AS ENUM('active', 'expired', 'cancelled', 'payment_pending');--> statement-breakpoint
CREATE TYPE "public"."license_type" AS ENUM('basic', 'premium', 'business');--> statement-breakpoint
CREATE TABLE "licenses" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"product_id" text NOT NULL,
"license_key" text NOT NULL,
"status" "license_status" DEFAULT 'active' NOT NULL,
"type" "license_type" NOT NULL,
"billing_type" "billing_type" NOT NULL,
"server_ip" text,
"activated_at" timestamp,
"last_verified_at" timestamp,
"expires_at" timestamp NOT NULL,
"stripeCustomerId" text NOT NULL,
"stripeSubscriptionId" text NOT NULL,
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP,
"metadata" text,
"email" text NOT NULL,
CONSTRAINT "licenses_license_key_unique" UNIQUE("license_key")
);

View File

@@ -1,171 +0,0 @@
{
"id": "5a996744-b11f-4f1a-b4b0-91f6bf5c2bed",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.licenses": {
"name": "licenses",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"product_id": {
"name": "product_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"license_key": {
"name": "license_key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "license_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'active'"
},
"type": {
"name": "type",
"type": "license_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"billing_type": {
"name": "billing_type",
"type": "billing_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"server_ip": {
"name": "server_ip",
"type": "text",
"primaryKey": false,
"notNull": false
},
"activated_at": {
"name": "activated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"last_verified_at": {
"name": "last_verified_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"stripeCustomerId": {
"name": "stripeCustomerId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"stripeSubscriptionId": {
"name": "stripeSubscriptionId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "CURRENT_TIMESTAMP"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "CURRENT_TIMESTAMP"
},
"metadata": {
"name": "metadata",
"type": "text",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"licenses_license_key_unique": {
"name": "licenses_license_key_unique",
"nullsNotDistinct": false,
"columns": [
"license_key"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.billing_type": {
"name": "billing_type",
"schema": "public",
"values": [
"monthly",
"annual"
]
},
"public.license_status": {
"name": "license_status",
"schema": "public",
"values": [
"active",
"expired",
"cancelled",
"payment_pending"
]
},
"public.license_type": {
"name": "license_type",
"schema": "public",
"values": [
"basic",
"premium",
"business"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -1,13 +1,5 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1742369437742,
"tag": "0000_noisy_epoch",
"breakpoints": true
}
]
"entries": []
}

View File

@@ -9,6 +9,7 @@
"typecheck": "tsc --noEmit",
"generate": "drizzle-kit generate",
"drop": "drizzle-kit drop",
"push": "drizzle-kit push",
"migrate": "tsx ./migrate.ts",
"truncate": "tsx ./truncate.ts",
"reset:all": "tsx ./truncate.ts && tsx ./migrate.ts",

View File

@@ -58,13 +58,12 @@ router.get("/health", async (c) => {
router.post("/validate", zValidator("json", validateSchema), async (c) => {
const { licenseKey, serverIp } = c.req.valid("json");
console.log("Validating license", licenseKey, serverIp);
try {
const result = await validateLicense(licenseKey, serverIp);
console.log("Result", result);
return c.json(result);
} catch (error) {
logger.error("Error validating license:", error);
logger.error("Error validating license:", { error });
return c.json({ isValid: false, error: "Error validating license" }, 500);
}
});
@@ -125,12 +124,7 @@ router.post("/resend-license", zValidator("json", resendSchema), async (c) => {
customerName: license.email,
}),
);
// await transporter.sendMail({
// from: fromAddress,
// to: toAddresses.join(", "),
// subject,
// html: htmlContent,
// });
await transporter.sendMail({
from: process.env.SMTP_FROM_ADDRESS,
to: license.email,
@@ -199,7 +193,7 @@ router.post("/stripe/webhook", async (c) => {
billingType,
email: session.customer_details?.email!,
stripeCustomerId: customerResponse.id,
stripeSubscriptionId: session.id,
stripeSubscriptionId: session.subscription as string,
});
console.log("License created", license);

View File

@@ -60,6 +60,8 @@ export const validateLicense = async (
license.stripeSubscriptionId,
);
console.log("Suscription", suscription);
if (suscription.status !== "active") {
return {
isValid: false,