"use client"; import { useQuery as useZeroQuery } from "@rocicorp/zero/react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertTriangle, CreditCard, RefreshCw } from "lucide-react"; import { useParams, usePathname, useRouter, useSearchParams } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Spinner } from "@/components/ui/spinner"; import { Switch } from "@/components/ui/switch"; import { stripeApiService } from "@/lib/apis/stripe-api.service"; import { AppError } from "@/lib/error"; import { queries } from "@/zero/queries"; const microsToDollars = (micros: number | null | undefined): string => { if (micros == null) return ""; return (micros / 1_000_000).toString(); }; const dollarsToMicros = (value: string): number | null => { const trimmed = value.trim(); if (trimmed === "") return null; const dollars = Number(trimmed); if (!Number.isFinite(dollars) || dollars < 0) return null; return Math.round(dollars * 1_000_000); }; const formatUsd = (micros: number) => `$${(Math.max(0, micros) / 1_000_000).toFixed(2)}`; export function AutoReloadSettings() { const params = useParams(); const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const queryClient = useQueryClient(); const searchSpaceId = Number(params?.search_space_id); const [enabled, setEnabled] = useState(false); const [thresholdInput, setThresholdInput] = useState(""); const [amountInput, setAmountInput] = useState(""); const seededRef = useRef(false); const [me] = useZeroQuery(queries.user.me({})); const balanceMicros = me?.creditMicrosBalance ?? 0; const { data: settings, isLoading } = useQuery({ queryKey: ["auto-reload-settings"], queryFn: () => stripeApiService.getAutoReloadSettings(), }); // Seed the form once from the server, then let the user own the inputs. useEffect(() => { if (settings && !seededRef.current) { seededRef.current = true; setEnabled(settings.enabled); setThresholdInput(microsToDollars(settings.threshold_micros)); setAmountInput(microsToDollars(settings.amount_micros)); } }, [settings]); // Surface the result of the Stripe card-setup redirect. useEffect(() => { const setupResult = searchParams.get("auto_reload_setup"); if (!setupResult) return; if (setupResult === "success") { toast.success("Card saved. You can now enable auto-reload."); queryClient.invalidateQueries({ queryKey: ["auto-reload-settings"] }); } else if (setupResult === "cancel") { toast.info("Card setup canceled."); } // Strip the query param so refreshes don't re-toast. router.replace(pathname); }, [searchParams, router, pathname, queryClient]); const setupMutation = useMutation({ mutationFn: () => stripeApiService.createAutoReloadSetupSession({ search_space_id: searchSpaceId }), onSuccess: (response) => { window.location.assign(response.checkout_url); }, onError: () => { toast.error("Couldn't start card setup. Please try again."); }, }); const saveMutation = useMutation({ mutationFn: stripeApiService.updateAutoReloadSettings, onSuccess: (updated) => { queryClient.setQueryData(["auto-reload-settings"], updated); toast.success(updated.enabled ? "Auto-reload is on." : "Auto-reload settings saved."); }, onError: (error) => { if (error instanceof AppError && error.message) { toast.error(error.message); return; } toast.error("Couldn't save auto-reload settings. Please try again."); }, }); // Render nothing while loading (avoids a spinner flash on pages where the // feature flag turns out to be off) and when auto-reload is disabled // server-side. if (isLoading || !settings || !settings.feature_enabled) { return null; } const minAmountDollars = (settings.min_amount_micros / 1_000_000).toFixed(2); const hasCard = settings.has_payment_method; const handleSave = () => { if (!enabled) { saveMutation.mutate({ enabled: false, threshold_micros: dollarsToMicros(thresholdInput), amount_micros: dollarsToMicros(amountInput), }); return; } const thresholdMicros = dollarsToMicros(thresholdInput); const amountMicros = dollarsToMicros(amountInput); if (!thresholdMicros || thresholdMicros <= 0) { toast.error("Enter a low-balance threshold greater than $0."); return; } if (amountMicros == null || amountMicros < settings.min_amount_micros) { toast.error(`Reload amount must be at least $${minAmountDollars}.`); return; } saveMutation.mutate({ enabled: true, threshold_micros: thresholdMicros, amount_micros: amountMicros, }); }; return ( Auto-reload Automatically top up your credit balance when it drops below a threshold, using a saved card. Current balance:{" "} {formatUsd(balanceMicros)}. {settings.failed_at && ( Last auto-reload failed Your saved card was declined and auto-reload was turned off. Update your card and re-enable it below to keep topping up automatically. )} {!hasCard ? (
Add a card to enable automatic top-ups.
) : ( <>

Charge your saved card when the balance gets low.

$ setThresholdInput(e.target.value)} disabled={!enabled} placeholder="5" />
$ setAmountInput(e.target.value)} disabled={!enabled} placeholder="10" />

Minimum ${minAmountDollars}.

)}
); }