diff --git a/src/components/dashboard/CreditsPage.tsx b/src/components/dashboard/CreditsPage.tsx
index 9467d76..efb01b8 100644
--- a/src/components/dashboard/CreditsPage.tsx
+++ b/src/components/dashboard/CreditsPage.tsx
@@ -2,6 +2,7 @@ import { For, Show, createSignal, onMount } from "solid-js";
import { Portal } from "solid-js/web";
import { BTN_GHOST, BTN_ORANGE, BTN_PRIMARY, CARD, ORANGE, NAVY, PKG_CARD } from "~/components/DashboardShell";
import { PROFESSIONAL_ROLE_SET, ROLE_PREFIXES, type RoleKey } from "./RoleDashboardShared";
+import { openRazorpayCheckout } from "~/lib/razorpay";
const API = "/api/gateway";
@@ -102,8 +103,6 @@ export default function CreditsPage(props: Props) {
step: "form",
error: "",
});
-
- // Mock card form state
const [cardNumber, setCardNumber] = createSignal("");
const [cardExpiry, setCardExpiry] = createSignal("");
const [cardCvv, setCardCvv] = createSignal("");
@@ -356,24 +355,9 @@ export default function CreditsPage(props: Props) {
if (!pkg) return;
- // Validate card fields
- if (cardNumber().replace(/\s/g, "").length < 16) {
- setCheckout((c) => ({ ...c, error: "Please enter a valid card number" }));
- return;
- }
- if (!cardExpiry() || cardExpiry().length < 5) {
- setCheckout((c) => ({ ...c, error: "Please enter expiry date (MM/YY)" }));
- return;
- }
- if (cardCvv().length < 3) {
- setCheckout((c) => ({ ...c, error: "Please enter valid CVV" }));
- return;
- }
-
setCheckout((c) => ({ ...c, step: "processing", error: "" }));
try {
- // Step 1: Create order
const orderRes = await apiFetch("/api/payments/create-order", {
method: "POST",
body: JSON.stringify({
@@ -393,24 +377,25 @@ export default function CreditsPage(props: Props) {
return;
}
- const orderId = orderData.order_id;
+ const paymentResult = await openRazorpayCheckout({
+ amount: orderData.amount,
+ currency: orderData.currency,
+ orderId: orderData.order_id,
+ description: `${pkg.name} - ${pkg.tracecoins_amount} Tracecoins`,
+ });
- // Step 2: Simulate payment processing (mock)
- await new Promise((resolve) => setTimeout(resolve, 2000));
-
- // Step 3: Verify payment
const verifyRes = await apiFetch("/api/payments/verify", {
method: "POST",
body: JSON.stringify({
- order_id: orderId,
- payment_id: "pay_mock_" + Date.now(),
- signature: "mock_signature",
+ razorpay_order_id: paymentResult.razorpay_order_id,
+ razorpay_payment_id: paymentResult.razorpay_payment_id,
+ razorpay_signature: paymentResult.razorpay_signature,
}),
});
const verifyData = await verifyRes.json().catch(() => ({}));
if (verifyRes.ok && verifyData.verified) {
- setCheckout((c) => ({ ...c, step: "success", orderId }));
+ setCheckout((c) => ({ ...c, step: "success", orderId: paymentResult.razorpay_order_id }));
setMsg(
`Payment successful! ${pkg.tracecoins_amount} Tracecoins have been added to your wallet.`
);
@@ -655,52 +640,36 @@ export default function CreditsPage(props: Props) {
- {/* Mock Notice */}
+ {/* Razorpay Notice */}
- Mock Payment: Use any values. This is a simulation.
+ Razorpay Standard Checkout will open in a secure modal. Cancelled or failed
+ payments will not be credited.
- {/* Card Number */}
-
- {/* Expiry & CVV */}
-
- Expiry (MM/YY)
+ Card Number
setCardExpiry(formatExpiry(e.currentTarget.value))}
- placeholder="MM/YY"
- maxLength="5"
+ value={cardNumber()}
+ onInput={(e) => setCardNumber(formatCardNumber(e.currentTarget.value))}
+ placeholder="1234 5678 9012 3456"
+ maxLength="19"
style={{
width: "100%",
padding: "12px",
@@ -729,6 +698,68 @@ export default function CreditsPage(props: Props) {
}}
/>
+
+
+
+
+ Expiry (MM/YY)
+
+ setCardExpiry(formatExpiry(e.currentTarget.value))}
+ placeholder="MM/YY"
+ maxLength="5"
+ style={{
+ width: "100%",
+ padding: "12px",
+ border: "1px solid #E5E7EB",
+ "border-radius": "8px",
+ "font-size": "14px",
+ "box-sizing": "border-box",
+ }}
+ />
+
+
+
+ CVV
+
+
+ setCardCvv(e.currentTarget.value.replace(/\D/g, "").slice(0, 4))
+ }
+ placeholder="123"
+ maxLength="4"
+ style={{
+ width: "100%",
+ padding: "12px",
+ border: "1px solid #E5E7EB",
+ "border-radius": "8px",
+ "font-size": "14px",
+ "box-sizing": "border-box",
+ }}
+ />
+
+
+
- CVV
+ Cardholder Name
- setCardCvv(e.currentTarget.value.replace(/\D/g, "").slice(0, 4))
- }
- placeholder="123"
- maxLength="4"
+ value={cardName()}
+ onInput={(e) => setCardName(e.currentTarget.value)}
+ placeholder="John Doe"
style={{
width: "100%",
padding: "12px",
@@ -759,36 +787,7 @@ export default function CreditsPage(props: Props) {
}}
/>
-
-
- {/* Cardholder Name */}
-
-
- Cardholder Name
-
- setCardName(e.currentTarget.value)}
- placeholder="John Doe"
- style={{
- width: "100%",
- padding: "12px",
- border: "1px solid #E5E7EB",
- "border-radius": "8px",
- "font-size": "14px",
- "box-sizing": "border-box",
- }}
- />
-
+
{/* Pay Button */}
- Secure payment powered by Nxtgauge
+ Secure payment powered by Razorpay
diff --git a/src/global.d.ts b/src/global.d.ts
index dc0c2f9..39906cd 100644
--- a/src/global.d.ts
+++ b/src/global.d.ts
@@ -1,2 +1,39 @@
///
///
+
+declare global {
+ interface Window {
+ Razorpay?: new (options: {
+ key: string;
+ amount: number;
+ currency: string;
+ order_id: string;
+ name: string;
+ description: string;
+ handler: (response: {
+ razorpay_payment_id: string;
+ razorpay_order_id: string;
+ razorpay_signature: string;
+ }) => void;
+ modal?: {
+ ondismiss?: () => void;
+ };
+ prefill?: {
+ name?: string;
+ email?: string;
+ contact?: string;
+ };
+ theme?: {
+ color?: string;
+ };
+ }) => {
+ on: (
+ event: "payment.failed",
+ handler: (event: { error?: { description?: string } }) => void
+ ) => void;
+ open: () => void;
+ };
+ }
+}
+
+export {};
diff --git a/src/lib/api.ts b/src/lib/api.ts
index a998d4f..eddfb54 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -141,17 +141,29 @@ export async function fetchInvoices(rolePrefix: string, page = 1, limit = 20): P
return apiFetch(`/api/${rolePrefix}/wallet/invoices?page=${page}&limit=${limit}`);
}
-export async function createPaymentOrder(amount: number, packageId?: string): Promise {
+export async function createPaymentOrder(
+ amount: number,
+ packageId?: string,
+ currency = "INR"
+): Promise {
return apiFetch('/api/payments/create-order', {
method: 'POST',
- body: JSON.stringify({ amount, package_id: packageId }),
+ body: JSON.stringify({ amount, currency, package_id: packageId }),
});
}
-export async function verifyPayment(orderId: string, paymentId: string): Promise {
+export async function verifyPayment(
+ orderId: string,
+ paymentId: string,
+ signature: string
+): Promise {
return apiFetch('/api/payments/verify', {
method: 'POST',
- body: JSON.stringify({ order_id: orderId, payment_id: paymentId }),
+ body: JSON.stringify({
+ razorpay_order_id: orderId,
+ razorpay_payment_id: paymentId,
+ razorpay_signature: signature,
+ }),
});
}
diff --git a/src/lib/razorpay.ts b/src/lib/razorpay.ts
new file mode 100644
index 0000000..9d48d2f
--- /dev/null
+++ b/src/lib/razorpay.ts
@@ -0,0 +1,92 @@
+type RazorpaySuccessPayload = {
+ razorpay_payment_id: string;
+ razorpay_order_id: string;
+ razorpay_signature: string;
+};
+
+type RazorpayCheckoutOptions = {
+ amount: number;
+ currency: string;
+ orderId: string;
+ description: string;
+ prefill?: {
+ name?: string;
+ email?: string;
+ contact?: string;
+ };
+};
+
+const RAZORPAY_SCRIPT_ID = "razorpay-checkout-js";
+const RAZORPAY_SCRIPT_SRC = "https://checkout.razorpay.com/v1/checkout.js";
+
+function getRazorpayKeyId() {
+ const keyId = import.meta.env.VITE_RAZORPAY_KEY_ID;
+ if (!keyId) {
+ throw new Error("Razorpay public key is not configured.");
+ }
+ return keyId;
+}
+
+export async function loadRazorpayCheckout(): Promise {
+ if (typeof window === "undefined") {
+ throw new Error("Razorpay checkout is only available in the browser.");
+ }
+
+ if (window.Razorpay) {
+ return;
+ }
+
+ const existing = document.getElementById(RAZORPAY_SCRIPT_ID) as HTMLScriptElement | null;
+ if (existing) {
+ await new Promise((resolve, reject) => {
+ existing.addEventListener("load", () => resolve(), { once: true });
+ existing.addEventListener("error", () => reject(new Error("Failed to load Razorpay checkout.")), {
+ once: true,
+ });
+ });
+ return;
+ }
+
+ await new Promise((resolve, reject) => {
+ const script = document.createElement("script");
+ script.id = RAZORPAY_SCRIPT_ID;
+ script.src = RAZORPAY_SCRIPT_SRC;
+ script.async = true;
+ script.onload = () => resolve();
+ script.onerror = () => reject(new Error("Failed to load Razorpay checkout."));
+ document.body.appendChild(script);
+ });
+}
+
+export async function openRazorpayCheckout(
+ options: RazorpayCheckoutOptions
+): Promise {
+ await loadRazorpayCheckout();
+
+ return new Promise((resolve, reject) => {
+ const instance = new window.Razorpay({
+ key: getRazorpayKeyId(),
+ amount: options.amount,
+ currency: options.currency,
+ order_id: options.orderId,
+ name: "Nxtgauge",
+ description: options.description,
+ handler: (response: RazorpaySuccessPayload) => resolve(response),
+ modal: {
+ ondismiss: () => reject(new Error("Payment cancelled.")),
+ },
+ prefill: options.prefill,
+ theme: {
+ color: "#FF5E13",
+ },
+ });
+
+ instance.on("payment.failed", (event: { error?: { description?: string } }) => {
+ reject(new Error(event.error?.description || "Payment failed."));
+ });
+
+ instance.open();
+ });
+}
+
+export type { RazorpaySuccessPayload };
diff --git a/src/routes/dashboard/wallet/buy.tsx b/src/routes/dashboard/wallet/buy.tsx
index 48dde05..b6d2a64 100644
--- a/src/routes/dashboard/wallet/buy.tsx
+++ b/src/routes/dashboard/wallet/buy.tsx
@@ -2,6 +2,7 @@ import { createResource, createSignal, Show, For } from "solid-js";
import { useNavigate } from "@solidjs/router";
import DashboardLayout from "~/components/DashboardLayout";
import { api } from "~/lib/api";
+import { openRazorpayCheckout } from "~/lib/razorpay";
interface Package {
id: string;
@@ -55,8 +56,7 @@ export default function BuyTracecoinsPage() {
setError("");
try {
- // Step 1: Create order via Beeceptor
- const orderRes = await api.post("/payments/order", {
+ const orderRes = await api.post("/payments/create-order", {
package_id: pkg.id,
amount: pkg.price_inr,
currency: "INR",
@@ -64,29 +64,17 @@ export default function BuyTracecoinsPage() {
const { order_id, amount, currency } = orderRes.data;
- // Step 2: Simulate Razorpay payment with Beeceptor
- const paymentRes = await fetch("https://nxtgauge.free.beeceptor.com/payment", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- order_id,
- amount,
- currency,
- status: "captured",
- }),
+ const paymentData = await openRazorpayCheckout({
+ amount,
+ currency,
+ orderId: order_id,
+ description: `${pkg.name} - ${pkg.tracecoins_amount} Tracecoins`,
});
- const paymentData = await paymentRes.json();
-
- if (paymentData.status !== "captured") {
- throw new Error("Payment failed");
- }
-
- // Step 3: Verify payment with our backend
await api.post("/payments/verify", {
- order_id,
- payment_id: paymentData.payment_id || "pay_test_" + Date.now(),
- signature: "test_signature",
+ razorpay_order_id: paymentData.razorpay_order_id,
+ razorpay_payment_id: paymentData.razorpay_payment_id,
+ razorpay_signature: paymentData.razorpay_signature,
});
setSuccess(true);