76 lines · 2.7 KB
import { useSyncExternalStore } from "react";

/** The structured part of a 402 Payment Required, as sent by the server (see paymentRequired in errors.ts). */
export type PaymentDetail = {
  message: string;
  meter: string;
  reason: "plan_limit" | "spending_limit" | "paid_feature";
  /** Whether the viewer can pay for the blocked account; false for, say, a collaborator on someone's repo. */
  manageable: boolean;
  account: { name: string; kind: string } | null;
  billingPath: string | null;
};

const HTTP_PAYMENT_REQUIRED = 402;

/** The payment detail of a thrown server error, or null when it is not an out-of-budget 402. */
export function paymentDetail(error: unknown): PaymentDetail | null {
  if (!(error instanceof Error)) return null;
  const { status, detail } = error as Error & { status?: number; detail?: Record<string, unknown> | null };
  if (status !== HTTP_PAYMENT_REQUIRED || !detail || detail.code !== "payment_required") return null;
  if (typeof detail.meter !== "string") return null;
  const reason = detail.reason === "spending_limit" || detail.reason === "paid_feature" ? detail.reason : "plan_limit";
  const account = detail.account as { name?: unknown; kind?: unknown } | undefined;
  return {
    message: error.message,
    meter: detail.meter,
    reason,
    manageable: detail.manageable === true,
    account:
      account && typeof account.name === "string" ? { name: account.name, kind: String(account.kind ?? "user") } : null,
    billingPath: typeof detail.billingPath === "string" ? detail.billingPath : null,
  };
}

/** For plain fetch routes (uploads): opens the upsell when a JSON 402 comes back. Returns whether it did. */
export function upsellFromResponse(status: number, payload: unknown): boolean {
  if (status !== HTTP_PAYMENT_REQUIRED || !payload || typeof payload !== "object") return false;
  const body = payload as Record<string, unknown>;
  const error = Object.assign(new Error(typeof body.error === "string" ? body.error : "Payment required"), {
    status,