import { useEffect, useState } from "react"; import { Link, useNavigate, useSearchParams } from "react-router-dom"; import { PageHeader } from "../components/layout/PageHeader"; import { Button, Card } from "../components/ui"; import { useData, useRepos } from "../data/DataContext"; import type { BillingCheckoutStatus } from "../data/repos"; import { useI18n } from "../i18n/I18nContext"; import { billingErrorMessage, isSafeBillingUrl, pollCheckout, startBillingRedirect, type CheckoutPollResult, } from "../lib/billingCheckout"; import { getPlanRights, planCtaLabel } from "../lib/planRights"; import { PLANS, type PlanId } from "../lib/usageMeter"; const PENDING_CHECKOUT_KEY = "harbor.billing.pending-checkout"; function isPlanId(value: string | null): value is PlanId { return value === "free" || value === "starter" || value === "pro"; } function checkoutIdFromReturn(value: string | null): string { if (value && !/[{}<>]/.test(value) && value !== "checkout_id") return value; return sessionStorage.getItem(PENDING_CHECKOUT_KEY) ?? ""; } export function PlanCheckoutPage() { const repos = useRepos(); const { refresh } = useData(); const { t, formatPlanPrice } = useI18n(); const navigate = useNavigate(); const [params] = useSearchParams(); const result = params.get("result"); const planParam = params.get("plan"); const planId = isPlanId(planParam) ? planParam : null; const plan = planId ? PLANS[planId] : null; const rights = planId ? getPlanRights(planId, t, formatPlanPrice) : null; const [currentId, setCurrentId] = useState(null); const [loadingError, setLoadingError] = useState(""); const [busy, setBusy] = useState(false); const [redirecting, setRedirecting] = useState(false); const [error, setError] = useState(""); const [pollResult, setPollResult] = useState(null); const [lastCheckout, setLastCheckout] = useState(null); const [pollAttempt, setPollAttempt] = useState(0); const checkoutId = result === "success" ? checkoutIdFromReturn(params.get("checkout_id")) : ""; useEffect(() => { if (result) return; void repos.usage .getBillingSubscription() .then((subscription) => { setCurrentId(subscription.plan_id); setLoadingError(""); }) .catch((e: unknown) => setLoadingError(billingErrorMessage(e, t, "plans.loadFail"))); }, [repos.usage, result, t]); useEffect(() => { if (result !== "success" || !checkoutId) return; let stopped = false; setBusy(true); setError(""); setPollResult(null); void pollCheckout(() => repos.usage.getCheckoutSession(checkoutId), { stopped: () => stopped, }) .then((outcome) => { if (!outcome || stopped) return; setLastCheckout(outcome.checkout); setPollResult(outcome); if (outcome.outcome === "fulfilled") { sessionStorage.removeItem(PENDING_CHECKOUT_KEY); refresh(); navigate("/app/usage", { replace: true, state: { purchaseOk: outcome.checkout.plan_id }, }); } }) .catch((e: unknown) => { if (!stopped) setError(billingErrorMessage(e, t, "checkout.pollFail")); }) .finally(() => { if (!stopped) setBusy(false); }); return () => { stopped = true; }; }, [checkoutId, navigate, pollAttempt, refresh, repos.usage, result, t]); async function startCheckout() { if (!planId || !currentId || (planId === "free" && currentId === "free")) return; setBusy(true); setRedirecting(false); setError(""); try { if (planId === "free" || planId === currentId) { const portal = await repos.usage.createBillingPortalSession(); if (!isSafeBillingUrl(portal.url)) { setError(t("checkout.invalidUrl")); setBusy(false); return; } beginRedirect(portal.url); return; } const checkout = await repos.usage.createCheckoutSession(planId, crypto.randomUUID()); if (!checkout.id || !isSafeBillingUrl(checkout.url)) { setError(t("checkout.invalidUrl")); setBusy(false); return; } sessionStorage.setItem(PENDING_CHECKOUT_KEY, checkout.id); beginRedirect(checkout.url); } catch (e) { setError(billingErrorMessage(e, t, "checkout.fail")); setBusy(false); } } function beginRedirect(url: string) { setRedirecting(true); startBillingRedirect( () => window.location.assign(url), () => { setRedirecting(false); setBusy(false); setError(t("checkout.redirectFailed")); }, ); } function retryPoll() { setPollAttempt((attempt) => attempt + 1); } if (result === "cancel") { return ( <>

{t("checkout.canceledTitle")}

{t("checkout.canceledBody")}

{t("checkout.viewPlans")}
); } if (result === "success") { const status = lastCheckout ? `${lastCheckout.checkout_status} / ${lastCheckout.payment_status} / ${lastCheckout.fulfillment_status}` : ""; return ( <> {!checkoutId ?

{t("checkout.missingId")}

: null} {busy ?

{t("checkout.verifying")}

: null} {error ?

{error}

: null} {pollResult?.outcome === "failed" ? (

{t("checkout.terminalFail", { status })}

) : null} {pollResult?.outcome === "timeout" ? (

{t("checkout.timeout")}

) : null} {(error || pollResult) && checkoutId ? ( ) : null}
{t("plans.usageLink")}
); } if (!plan || !planId || !rights) { return ( <>

{t("checkout.pickFirst")}

{t("checkout.viewPlans")}
); } if (!currentId) { return ( <> {loadingError ?

{loadingError}

:

{t("common.loading")}

} ); } const already = currentId === planId; const currentFree = planId === "free" && currentId === "free"; const portalAction = (planId === "free" && currentId !== "free") || (already && currentId !== "free"); const actionLabel = portalAction ? t("plans.manage") : t("checkout.payAndAction", { action: planCtaLabel(currentId, planId, t), price: formatPlanPrice(plan.price_twd), }); return ( <> {error ?

{error}

: null} {redirecting ?

{t("checkout.redirecting")}

: null}

{t("checkout.subscribe")}

{plan.name}

{rights.headline}

{formatPlanPrice(plan.price_twd)}{t("checkout.perMonth")}

{t("checkout.monthlyCredits", { n: plan.monthly_credits })}

{t("checkout.youGet")}

    {rights.rights.map((line) =>
  • {line}
  • )}

{t("checkout.quota")}

    {rights.quota.map((line) =>
  • {line}
  • )}

{t("checkout.notes")}

    {rights.notes.map((line) =>
  • {line}
  • )}
); }