Integrate Razorpay checkout flow

This commit is contained in:
Tracewebstudio Dev 2026-06-26 20:47:31 +02:00
parent 79ac24217a
commit ab054884eb
5 changed files with 257 additions and 129 deletions

View file

@ -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) {
</div>
</Show>
{/* Mock Notice */}
{/* Razorpay Notice */}
<div
style={{
background: "#FEF3C7",
border: "1px solid #FCD34D",
background: "#FFF7ED",
border: "1px solid #FDBA74",
"border-radius": "8px",
padding: "10px",
"font-size": "12px",
color: "#92400E",
padding: "12px",
"font-size": "13px",
color: "#9A3412",
}}
>
<strong>Mock Payment:</strong> Use any values. This is a simulation.
Razorpay Standard Checkout will open in a secure modal. Cancelled or failed
payments will not be credited.
</div>
{/* Card Number */}
<div>
<label
<Show when={Boolean(aiPkg)}>
<div
style={{
display: "block",
background: "#FEF3C7",
border: "1px solid #FCD34D",
"border-radius": "8px",
padding: "10px",
"font-size": "12px",
"font-weight": "600",
color: "#374151",
"margin-bottom": "6px",
color: "#92400E",
}}
>
Card Number
</label>
<input
type="text"
value={cardNumber()}
onInput={(e) => setCardNumber(formatCardNumber(e.currentTarget.value))}
placeholder="1234 5678 9012 3456"
maxLength="19"
style={{
width: "100%",
padding: "12px",
border: "1px solid #E5E7EB",
"border-radius": "8px",
"font-size": "14px",
"box-sizing": "border-box",
}}
/>
</div>
AI credit purchases still use the existing internal test payment flow in this
screen.
</div>
{/* Expiry & CVV */}
<div style={{ display: "grid", "grid-template-columns": "1fr 1fr", gap: "12px" }}>
<div>
<label
style={{
@ -711,14 +680,14 @@ export default function CreditsPage(props: Props) {
"margin-bottom": "6px",
}}
>
Expiry (MM/YY)
Card Number
</label>
<input
type="text"
value={cardExpiry()}
onInput={(e) => 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) {
}}
/>
</div>
<div style={{ display: "grid", "grid-template-columns": "1fr 1fr", gap: "12px" }}>
<div>
<label
style={{
display: "block",
"font-size": "12px",
"font-weight": "600",
color: "#374151",
"margin-bottom": "6px",
}}
>
Expiry (MM/YY)
</label>
<input
type="text"
value={cardExpiry()}
onInput={(e) => 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",
}}
/>
</div>
<div>
<label
style={{
display: "block",
"font-size": "12px",
"font-weight": "600",
color: "#374151",
"margin-bottom": "6px",
}}
>
CVV
</label>
<input
type="text"
value={cardCvv()}
onInput={(e) =>
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",
}}
/>
</div>
</div>
<div>
<label
style={{
@ -739,16 +770,13 @@ export default function CreditsPage(props: Props) {
"margin-bottom": "6px",
}}
>
CVV
Cardholder Name
</label>
<input
type="text"
value={cardCvv()}
onInput={(e) =>
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) {
}}
/>
</div>
</div>
{/* Cardholder Name */}
<div>
<label
style={{
display: "block",
"font-size": "12px",
"font-weight": "600",
color: "#374151",
"margin-bottom": "6px",
}}
>
Cardholder Name
</label>
<input
type="text"
value={cardName()}
onInput={(e) => 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",
}}
/>
</div>
</Show>
{/* Pay Button */}
<button
@ -812,7 +811,7 @@ export default function CreditsPage(props: Props) {
"text-align": "center",
}}
>
Secure payment powered by Nxtgauge
Secure payment powered by Razorpay
</p>
</div>
</Show>

37
src/global.d.ts vendored
View file

@ -1,2 +1,39 @@
/// <reference types="vinxi/types/client" />
/// <reference types="vinxi/types/server" />
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 {};

View file

@ -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<any> {
export async function createPaymentOrder(
amount: number,
packageId?: string,
currency = "INR"
): Promise<any> {
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<any> {
export async function verifyPayment(
orderId: string,
paymentId: string,
signature: string
): Promise<any> {
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,
}),
});
}

92
src/lib/razorpay.ts Normal file
View file

@ -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<void> {
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<void>((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<void>((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<RazorpaySuccessPayload> {
await loadRazorpayCheckout();
return new Promise<RazorpaySuccessPayload>((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 };

View file

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