import { useCreditExposure } from "../lib/use-analytics-exposure"; import type { OpenGeniBrowserClient } from "@opengeni/sdk/browser"; import { Link, usePage } from "../lib/navigation"; import { CreditCardIcon, Loader2Icon, SparklesIcon } from "lucide-react"; import type { BillingCheckoutStatus, WorkspaceModelCatalogModel } from "@opengeni/sdk"; import { useEffect, useState } from "react"; import { toast } from "../lib/toast"; import { CouponRedeem } from "./credits/coupon-redeem"; import { CreditsCelebrationDialog } from "./credits/checkout-credits-celebration"; import { Button } from "../../ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../../ui/dialog"; import { CreditAmountPicker } from "./credit-amount-picker"; import { Notice } from "../../ui/notice"; import { useAppContext } from "../app-context"; import { userErrorText } from "../../ui/api-error"; import { validTopupAmount } from "../lib/format"; import { analyticsAction } from "../../ui/analytics-actions"; import { billingCheckoutReturnUrl, checkoutTabReturnUrl } from "../lib/credit-checkout"; import { subscriptionAction } from "../lib/subscription-plans"; const DEFAULT_TOPUP = "25.00"; type CreditRequiredPromptProps = { open: boolean; workspaceId: string; accountId: string | null; canBuyCredits: boolean; purpose?: "required" | "topup" | undefined; onOpenChange: (open: boolean) => void; }; export function CreditRequiredPrompt(props: CreditRequiredPromptProps) { return ; } export function CreditRequiredPromptView({ client, open, workspaceId, accountId, canBuyCredits, purpose = "required", onOpenChange, }: CreditRequiredPromptProps & { client: OpenGeniBrowserClient }) { const page = usePage(); const [topupAmount, setTopupAmount] = useState(DEFAULT_TOPUP); const [busy, setBusy] = useState(false); // A redeemed coupon, celebrated in this dialog with its real amount. const [granted, setGranted] = useState(null); useEffect(() => { if (!open) setGranted(null); }, [open]); const [stripeEnabled, setStripeEnabled] = useState(false); const [upgradeAccountId, setUpgradeAccountId] = useState(null); const canUpgrade = open && canBuyCredits && accountId !== null && upgradeAccountId === accountId; useEffect(() => { setUpgradeAccountId(null); if (!open || !accountId || !canBuyCredits) { setStripeEnabled(false); return; } let active = true; setStripeEnabled(false); void client .getBilling({ accountId }) .then(async (billing) => { if (active) setStripeEnabled(billing.mode === "stripe"); if (!active || !billing.balance.plan || billing.mode !== "stripe") return; const catalog = await client.getBillingSubscriptionPlans({ accountId }).catch(() => null); if ( active && catalog?.plan && catalog.plans.some((target) => { const action = subscriptionAction(billing.balance.plan!, catalog.plans, target); return action === "checkout" || action === "upgrade"; }) ) setUpgradeAccountId(accountId); }) .catch(() => { if (active) setStripeEnabled(false); }); return () => { active = false; }; }, [accountId, canBuyCredits, client, open]); async function buyCredits(): Promise { if (!accountId || !validTopupAmount(topupAmount) || busy) return; setBusy(true); try { const session = await client.createBillingCheckout({ amountUsd: Number(topupAmount), accountId, // Beside an embed, checkout ends on a page that asks to go back to it. successUrl: page.opensBeside ? checkoutTabReturnUrl(page.origin(), "success") : billingCheckoutReturnUrl(page.origin(), workspaceId, "success"), cancelUrl: page.opensBeside ? checkoutTabReturnUrl(page.origin(), "cancelled") : billingCheckoutReturnUrl(page.origin(), workspaceId, "cancelled"), }); page.assign(session.url); // The web app has left for checkout; an embed stays and can buy again. if (page.opensBeside) setBusy(false); } catch (error) { toast.error("Checkout failed", { description: userErrorText(error) }); setBusy(false); } } if (granted) { return ; } return ( {purpose === "topup" ? "Add Opengeni credits" : "Add Opengeni credits to continue"} {purpose === "topup" ? "Choose an amount and pay in Stripe Checkout, or redeem a coupon code." : "Add credits to use this model, or choose another model."} {canBuyCredits && stripeEnabled ? (
{accountId ? (
) : null}
) : !canBuyCredits ? (

Ask an organization owner to add credits, or an admin to connect a model.

) : null} {purpose === "required" ? ( ) : null}
); } export function EmptyCreditsNotice({ workspaceId, accountId, canBuyCredits, canReadBilling, creditFunding, }: { workspaceId: string; accountId: string | null; canBuyCredits: boolean; canReadBilling: boolean; creditFunding?: WorkspaceModelCatalogModel["creditFunding"] | undefined; }) { const client = useAppContext().client; const [legacyEmpty, setEmpty] = useState(false); const [stripeEnabled, setStripeEnabled] = useState(false); useEffect(() => { setEmpty(false); setStripeEnabled(false); if (!accountId || !canReadBilling) return; let active = true; void client .getBilling({ accountId }) .then((summary) => { if (!active) return; setEmpty(summary.mode !== "disabled" && summary.balance.balanceMicros <= 0); setStripeEnabled(summary.mode === "stripe"); }) .catch(() => { if (!active) return; setEmpty(false); setStripeEnabled(false); }); return () => { active = false; }; }, [accountId, canReadBilling, client]); const empty = stripeEnabled && (creditFunding === undefined ? legacyEmpty : creditFunding === "unavailable"); useCreditExposure(empty, workspaceId); if (!empty) return null; return ( No credits are available for this model. Choose another model or add credits.
{canBuyCredits && stripeEnabled ? ( ) : null}
); }