From 77febdaeb43a22e2b2d9a0f2655de2d5e73c6484 Mon Sep 17 00:00:00 2001 From: Tracewebstudio Dev Date: Fri, 14 Aug 2026 11:19:07 +0200 Subject: [PATCH] feat(credits): add coupon code input to AI credit checkout Adds an optional coupon code field (uppercase, with Apply button and confirmation tick) to the checkout modal, visible only for AI credit package purchases. The code is passed as coupon_code to POST /api/ai-credits/order; the backend applies the discount and the discounted amount flows through to PayU. CheckoutState gains couponCode + couponApplied fields. All open/close reset paths clear them. Retry from error preserves the entered code. Co-Authored-By: Claude Sonnet 4.6 --- src/components/dashboard/CreditsPage.tsx | 70 ++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/src/components/dashboard/CreditsPage.tsx b/src/components/dashboard/CreditsPage.tsx index 7ffe847..f35e19a 100644 --- a/src/components/dashboard/CreditsPage.tsx +++ b/src/components/dashboard/CreditsPage.tsx @@ -72,6 +72,8 @@ type CheckoutState = { orderId: string | null; step: "form" | "processing" | "success" | "error"; error: string; + couponCode: string; + couponApplied: boolean; }; async function apiFetch(path: string, opts?: RequestInit) { @@ -116,6 +118,8 @@ export default function CreditsPage(props: Props) { orderId: null, step: "form", error: "", + couponCode: "", + couponApplied: false, }); const isProfessional = () => PROFESSIONAL_ROLE_SET.has(props.roleKey); const prefix = () => ROLE_PREFIXES[props.roleKey]; @@ -298,24 +302,28 @@ export default function CreditsPage(props: Props) { onMount(loadAllData); const openCheckout = (pkg: Package) => { - setCheckout({ package: pkg, aiCreditPackage: null, orderId: null, step: "form", error: "" }); + setCheckout({ package: pkg, aiCreditPackage: null, orderId: null, step: "form", error: "", couponCode: "", couponApplied: false }); }; const openAiCreditCheckout = (pkg: AiCreditPackage) => { - setCheckout({ package: null, aiCreditPackage: pkg, orderId: null, step: "form", error: "" }); + setCheckout({ package: null, aiCreditPackage: pkg, orderId: null, step: "form", error: "", couponCode: "", couponApplied: false }); }; const closeCheckout = () => { - setCheckout({ package: null, aiCreditPackage: null, orderId: null, step: "form", error: "" }); + setCheckout({ package: null, aiCreditPackage: null, orderId: null, step: "form", error: "", couponCode: "", couponApplied: false }); }; const processAiCreditPayment = async (aiPkg: AiCreditPackage) => { setCheckout((c) => ({ ...c, step: "processing", error: "" })); try { + const coupon = checkout().couponCode.trim(); const orderRes = await apiFetch("/api/ai-credits/order", { method: "POST", - body: JSON.stringify({ package_id: aiPkg.id }), + body: JSON.stringify({ + package_id: aiPkg.id, + ...(coupon ? { coupon_code: coupon } : {}), + }), }); const orderData = await orderRes.json().catch(() => ({})); @@ -682,6 +690,59 @@ export default function CreditsPage(props: Props) { + {/* Coupon code — only for AI credit purchases */} + +
+ +
+ setCheckout((c) => ({ ...c, couponCode: e.currentTarget.value.toUpperCase(), couponApplied: false }))} + style={{ + flex: "1", + padding: "9px 12px", + "border-radius": "8px", + border: checkout().couponApplied ? "1.5px solid #16A34A" : "1.5px solid #D1D5DB", + "font-size": "13px", + outline: "none", + "letter-spacing": "0.05em", + }} + /> + +
+ +

+ ✓ Coupon will be applied at checkout +

+
+
+
+ {/* PayU Notice */}
Pay {formatCurrency(checkout().package ? checkout().package!.price : checkout().aiCreditPackage!.price_inr)} + {checkout().couponApplied && checkout().couponCode.trim() ? " (coupon applied)" : ""}