feat(credits): add coupon code input to AI credit checkout
All checks were successful
build-and-release / build (push) Successful in 1m41s
All checks were successful
build-and-release / build (push) Successful in 1m41s
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 <noreply@anthropic.com>
This commit is contained in:
parent
74f321099f
commit
77febdaeb4
1 changed files with 66 additions and 4 deletions
|
|
@ -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) {
|
|||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Coupon code — only for AI credit purchases */}
|
||||
<Show when={checkout().aiCreditPackage}>
|
||||
<div style={{ display: "flex", "flex-direction": "column", gap: "6px" }}>
|
||||
<label style={{ "font-size": "13px", "font-weight": "600", color: "#374151" }}>
|
||||
Coupon Code <span style={{ "font-weight": "400", color: "#9CA3AF" }}>(optional)</span>
|
||||
</label>
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter coupon code"
|
||||
value={checkout().couponCode}
|
||||
onInput={(e) => 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",
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const code = checkout().couponCode.trim();
|
||||
if (code) {
|
||||
setCheckout((c) => ({ ...c, couponApplied: true }));
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
padding: "9px 14px",
|
||||
"border-radius": "8px",
|
||||
border: "1.5px solid #D1D5DB",
|
||||
background: "#F9FAFB",
|
||||
"font-size": "13px",
|
||||
"font-weight": "600",
|
||||
cursor: "pointer",
|
||||
color: "#374151",
|
||||
"white-space": "nowrap",
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
<Show when={checkout().couponApplied && checkout().couponCode.trim()}>
|
||||
<p style={{ margin: "0", "font-size": "12px", color: "#16A34A" }}>
|
||||
✓ Coupon will be applied at checkout
|
||||
</p>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* PayU Notice */}
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -709,6 +770,7 @@ export default function CreditsPage(props: Props) {
|
|||
}}
|
||||
>
|
||||
Pay {formatCurrency(checkout().package ? checkout().package!.price : checkout().aiCreditPackage!.price_inr)}
|
||||
{checkout().couponApplied && checkout().couponCode.trim() ? " (coupon applied)" : ""}
|
||||
</button>
|
||||
|
||||
<p
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue