import type { BillingCheckoutStatus } from "@opengeni/sdk"; import { notifyCreditBalanceChanged } from "../../lib/credit-balance-events"; import { ArrowUpRightIcon, Loader2Icon, TicketIcon } from "lucide-react"; import { useEffect, useId, useRef, useState } from "react"; import { Button } from "../../../ui/button"; import { Input } from "../../../ui/input"; import { userErrorTextWithoutReference } from "../../../ui/api-error"; import { openCheckoutTab, startCreditCheckout, waitForCheckoutCredits, type CreditCheckoutClient, } from "../../lib/credit-checkout"; import { usePage } from "../../lib/navigation"; type Phase = | { kind: "idle" } | { kind: "starting" } | { kind: "waiting"; checkoutSessionId: string; url: string } | { kind: "failed"; message: string }; /** * "Have a code?": the person types a code, Stripe Checkout opens in a * new tab with the code already applied (a fixed-amount code buys exactly its * amount, so a $100 code is $100 of credits for $0), and this page waits for * the credits to land before `onGranted` celebrates them. When the browser * blocks the tab, checkout takes over the page and organization billing * celebrates on return instead. */ type CouponRedeemProps = { client?: CreditCheckoutClient | undefined; accountId: string; /** Where a same-tab checkout returns (that workspace's organization billing). */ workspaceId: string; onGranted: (status: BillingCheckoutStatus) => void; defaultOpen?: boolean | undefined; disabled?: boolean | undefined; /** "inline": just the field and Redeem, for a settings row; the label is for screen readers. */ variant?: "stacked" | "inline" | undefined; }; export function CouponRedeem(props: CouponRedeemProps) { return ; } function CouponRedeemForm({ client, accountId, workspaceId, onGranted, defaultOpen = false, disabled = false, variant = "stacked", }: CouponRedeemProps) { const page = usePage(); const inline = variant === "inline"; const stayOpen = defaultOpen || inline; const [open, setOpen] = useState(defaultOpen || variant === "inline"); const [code, setCode] = useState(""); const [phase, setPhase] = useState({ kind: "idle" }); const inputId = useId(); const inputRef = useRef(null); const previousPhase = useRef(phase.kind); const checkoutRequest = useRef(null); useEffect(() => () => checkoutRequest.current?.abort(), []); useEffect(() => { if (phase.kind === "failed" || (phase.kind === "idle" && previousPhase.current === "waiting")) inputRef.current?.focus(); previousPhase.current = phase.kind; }, [phase.kind]); const onGrantedRef = useRef(onGranted); onGrantedRef.current = onGranted; useEffect(() => { if (phase.kind !== "waiting" || !client) return; const controller = new AbortController(); void waitForCheckoutCredits(client, { accountId, checkoutSessionId: phase.checkoutSessionId, signal: controller.signal, }).then( (status) => { if (controller.signal.aborted) return; if (status.credit.state === "granted") { setPhase({ kind: "idle" }); setCode(""); setOpen(stayOpen); notifyCreditBalanceChanged(accountId); onGrantedRef.current(status); } else { setPhase({ kind: "failed", message: "That checkout expired before it finished. Try the code again.", }); } }, () => undefined, ); return () => controller.abort(); }, [accountId, client, phase, stayOpen]); async function redeem(): Promise { const promotionCode = code.trim(); if (!client || !promotionCode || disabled || checkoutRequest.current) return; const controller = new AbortController(); checkoutRequest.current = controller; const tab = openCheckoutTab(page); setPhase({ kind: "starting" }); try { const session = await startCreditCheckout(client, { accountId, workspaceId, promotionCode, tab, signal: controller.signal, origin: page.origin(), navigate: page.assign, navigateOpensBeside: page.opensBeside, }); if (session.inNewTab) setPhase({ kind: "waiting", checkoutSessionId: session.checkoutSessionId, url: session.url, }); } catch (error) { if (!controller.signal.aborted) setPhase({ kind: "failed", message: userErrorTextWithoutReference(error) }); } finally { if (checkoutRequest.current === controller) checkoutRequest.current = null; } } if (!open) { return ( setOpen(true)} > Have a code? ); } if (phase.kind === "waiting") { return ( Finish in Stripe to add your credits. Open checkout again setPhase({ kind: "idle" })} > Use another code ); } const busy = phase.kind === "starting"; return ( { event.preventDefault(); void redeem(); }} > Promo code { setCode(event.target.value); if (phase.kind === "failed") setPhase({ kind: "idle" }); }} autoComplete="off" autoCapitalize="characters" spellCheck={false} maxLength={64} className="min-w-0 flex-1 uppercase placeholder:normal-case pointer-coarse:h-11" placeholder="Enter your code" aria-invalid={phase.kind === "failed" || undefined} aria-describedby={`${inputId}-help`} autoFocus={!defaultOpen && !inline} /> {busy ? : null} Redeem {phase.kind === "failed" ? phase.message : "Opens Stripe to confirm your code."} ); }
Finish in Stripe to add your credits.
{phase.kind === "failed" ? phase.message : "Opens Stripe to confirm your code."}