Ctrl K
ComunidadGuías…

lib/billing-api.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"use client";

import { fetchClientRead } from "./client-read";

import type {
  BillingStatusResponse,
  UsageHistoryResponse,
  UsageSummaryResponse,
  UsageAnalyticsResponse,
} from "@/lib/billing-types";
import type {
  BillingInterval,
  BillingPlanId,
  UsageSegmentId,
} from "@/lib/billing-plans";
import { isDesktop } from "@/lib/desktop/bridge";
import { trackEvent } from "./analytics";

/** Client fetchers for billing status, usage, checkout, and the customer portal. */

async function parseJson<T>(response: Response): Promise<T> {
  const text = await response.text();
  let data: unknown = null;
  try {
    data = text ? JSON.parse(text) : null;
  } catch {
    data = null;
  }
  if (!response.ok) {
    const message =
      (data as { error?: string } | null)?.error ||
      text.trim() ||
      "Request failed";
    throw new Error(message);
  }
  return data as T;
}

export async function fetchBillingStatusApi(): Promise<BillingStatusResponse> {
  return parseJson(await fetchClientRead("/api/billing", { cache: "no-store" }));
}

export async function fetchBillingUsageApi(): Promise<UsageSummaryResponse> {
  return parseJson(await fetchClientRead("/api/billing/usage", { cache: "no-store" }));
}

export async function fetchUsageAnalyticsApi(): Promise<UsageAnalyticsResponse> {
  return parseJson(
    await fetchClientRead("/api/billing/usage-analytics", { cache: "no-store" }),
  );
}

export async function fetchUsageHistoryApi(params: {
  segment?: UsageSegmentId | null;
  offset?: number;
}): Promise<UsageHistoryResponse> {
  const search = new URLSearchParams();
  if (params.segment) search.set("segment", params.segment);
  if (params.offset) search.set("offset", String(params.offset));
  const qs = search.toString();
  return parseJson(
    await fetchClientRead(`/api/billing/usage-history${qs ? `?${qs}` : ""}`, {
      cache: "no-store",
    }),
  );
}

/** Starts a Stripe checkout → Redirect URL. */
export async function createCheckoutApi(
  planId: BillingPlanId,
  interval: BillingInterval = "month",
): Promise<string> {
  trackEvent("checkout_started", { plan_id: planId, interval });
  const { url } = await parseJson<{ url: string | null }>(
    await fetch("/api/billing/checkout", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      // `desktop`: Stripe opens in the system browser in both cases,
      // but it must RETURN in the app when it is from it that we left
      // (MIN-293). Only the page can see this — the server would only see a
      // user agent, and a user agent doesn't decide anything here.
      body: JSON.stringify({ planId, interval, desktop: isDesktop() }),
    }),
  );
  if (!url) throw new Error("Missing checkout URL");
  return url;
}

/**
 * Cancels the subscription at the end of the period, or resumes it (MIN-296) — without
 * leaving the app: this is what puts termination at the same number of gestures as
 * subscription.
 */
export async function setCancelAtPeriodEndApi(
  cancel: boolean,
): Promise<boolean> {
  trackEvent(cancel ? "subscription_canceled" : "subscription_resumed", {});
  const { cancelAtPeriodEnd } = await parseJson<{ cancelAtPeriodEnd: boolean }>(
    await fetch("/api/billing/cancel", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ resume: !cancel }),
    }),
  );
  return cancelAtPeriodEnd;
}

/** Opens the Stripe portal (manage / change / cancel) → Redirect URL. */
export async function createPortalApi(): Promise<string> {
  trackEvent("billing_portal_opened", { current_plan_id: "unknown" });
  const { url } = await parseJson<{ url: string }>(
    await fetch("/api/billing/portal", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      // The “Return” button of the Stripe portal, same story as the checkout.
      body: JSON.stringify({ desktop: isDesktop() }),
    }),
  );
  return url;
}