import { getFormProps, useForm, type SubmissionResult } from "@conform-to/react"; import { parseWithZod } from "@conform-to/zod"; import { Form, useActionData, useNavigation } from "@remix-run/react"; import { useEffect, useMemo, useRef, useState } from "react"; import { z } from "zod"; import { Button } from "~/components/primitives/Buttons"; import { Callout } from "~/components/primitives/Callout"; import { Fieldset } from "~/components/primitives/Fieldset"; import { FormButtons } from "~/components/primitives/FormButtons"; import { FormError } from "~/components/primitives/FormError"; import { Header2 } from "~/components/primitives/Headers"; import { Input } from "~/components/primitives/Input"; import { InputGroup } from "~/components/primitives/InputGroup"; import { Paragraph } from "~/components/primitives/Paragraph"; import { DateTime } from "~/components/primitives/DateTime"; import { RadioGroup, RadioGroupItem } from "~/components/primitives/RadioButton"; import type { BillingLimitResult } from "~/services/billingLimit.schemas"; import { formatCurrency } from "~/utils/numberFormatter"; export const billingLimitRecoveryFormSchema = z .object({ action: z.enum(["increase", "remove"]), newAmount: z.coerce .number({ invalid_type_error: "Not a valid amount" }) .positive("Amount must be greater than 0") .optional(), resumeMode: z.enum(["queue", "new_only"]), }) .superRefine((value, ctx) => { if (value.action === "increase" && (value.newAmount === undefined || value.newAmount <= 0)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Amount must be greater than 0", path: ["newAmount"], }); } }); type BillingLimitRecoveryActionData = { formIntent: "billing-limit-resolve"; submission: SubmissionResult; }; export function BillingLimitRecoveryPanel({ billingLimit, currentSpendCents, queuedRunCount, suggestedNewLimitDollars, }: { billingLimit: BillingLimitResult & { isConfigured: true }; currentSpendCents: number; queuedRunCount: number; suggestedNewLimitDollars: number; }) { const { limitState } = billingLimit; const isGrace = limitState.status === "grace"; const isRejected = limitState.status === "rejected"; const [action, setAction] = useState<"increase" | "remove">("increase"); const [newAmount, setNewAmount] = useState(String(suggestedNewLimitDollars)); const [resumeMode, setResumeMode] = useState<"queue" | "new_only">("queue"); const newAmountInputRef = useRef(null); const formRef = useRef(null); useEffect(() => { setNewAmount(String(suggestedNewLimitDollars)); }, [suggestedNewLimitDollars]); function handleActionChange(value: string) { const nextAction = value as typeof action; setAction(nextAction); if (nextAction === "increase") { window.setTimeout(() => newAmountInputRef.current?.focus(), 0); } } const actionData = useActionData(); const recoverySubmission = actionData?.formIntent === "billing-limit-resolve" ? actionData.submission : undefined; const [form, fields] = useForm({ id: "billing-limit-resolve", lastResult: recoverySubmission as any, shouldRevalidate: "onInput", onValidate({ formData }) { return parseWithZod(formData, { schema: billingLimitRecoveryFormSchema }); }, defaultValue: { action: "increase", newAmount: suggestedNewLimitDollars, resumeMode: "queue", }, }); useEffect(() => { formRef.current?.dispatchEvent(new Event("input", { bubbles: true })); }, [action, newAmount, resumeMode]); const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting"; const queuedRunsLabel = useMemo(() => { if (queuedRunCount === 0) { return null; } return `~${queuedRunCount.toLocaleString()} run${ queuedRunCount === 1 ? "" : "s" } waiting in queue`; }, [queuedRunCount]); return (
Action required {isGrace ? ( <> Your organization has reached its billing limit. Processing is paused and new runs are queuing. Without action, new triggers block on{" "} . ) : ( "Your organization has exceeded its billing limit. New triggers are blocked until you resolve this." )}
Current usage: {formatCurrency(currentSpendCents / 100, false)} {billingLimit.effectiveAmountCents !== null && ( Current limit: {formatCurrency(billingLimit.effectiveAmountCents / 100, false)} )} {queuedRunsLabel && {queuedRunsLabel}} {isRejected && queuedRunsLabel && ( New triggers are currently blocked. )}
How would you like to resolve this?
{action === "increase" && ( setNewAmount(e.target.value)} placeholder="New limit amount" icon={ $ } /> )} {action === "increase" && fields.newAmount?.errors && ( {fields.newAmount.errors} )}
What should happen to queued runs? setResumeMode(value as typeof resumeMode)} className="flex flex-col gap-2" >
{isSubmitting ? "Resolving…" : "Resolve"} } />
); }