import type { BillingCheckoutStatus } from "../data/repos"; type Translate = (key: string, params?: Record) => string; type ApiErrorLike = { code?: unknown; httpStatus?: unknown; }; export type CheckoutPollResult = | { outcome: "fulfilled"; checkout: BillingCheckoutStatus } | { outcome: "failed"; checkout: BillingCheckoutStatus } | { outcome: "timeout"; checkout: BillingCheckoutStatus }; export function isSafeBillingUrl(value: string, dev = import.meta.env.DEV): boolean { try { const url = new URL(value); if (url.protocol === "https:") return true; return ( dev && url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]") ); } catch { return false; } } export function billingErrorMessage( error: unknown, t: Translate, fallbackKey: string, ): string { const apiError = error && typeof error === "object" ? (error as ApiErrorLike) : null; const isApiError = apiError && ("code" in apiError || "httpStatus" in apiError); if (!isApiError) return t(fallbackKey); const code = typeof apiError?.code === "number" ? apiError.code : 0; const status = typeof apiError?.httpStatus === "number" ? apiError.httpStatus : 0; if (status === 0 && code === 0) return t("checkout.networkError"); if (status === 503 || status === 504 || code === 503010) return t("checkout.unavailable"); if (status === 401) return t("checkout.sessionExpired"); if (code === 409081) return t("checkout.portalUnavailable"); return t(fallbackKey); } export function startBillingRedirect( assign: () => void, onFailure: () => void, timeoutMs = 3_000, ): () => void { let settled = false; let timer = 0; const cleanup = () => { settled = true; window.clearTimeout(timer); window.removeEventListener("pagehide", cleanup); }; window.addEventListener("pagehide", cleanup, { once: true }); timer = window.setTimeout(() => { if (settled) return; cleanup(); onFailure(); }, timeoutMs); try { assign(); } catch { cleanup(); onFailure(); } return cleanup; } export function isTerminalCheckoutFailure(checkout: BillingCheckoutStatus): boolean { const checkoutStatus = checkout.checkout_status.toLowerCase(); const paymentStatus = checkout.payment_status.toLowerCase(); const fulfillmentStatus = checkout.fulfillment_status.toLowerCase(); return ( ["failed", "expired", "canceled", "cancelled"].includes(checkoutStatus) || ["failed", "expired", "canceled", "cancelled"].includes(paymentStatus) || ["failed", "expired", "canceled", "cancelled"].includes(fulfillmentStatus) ); } export async function pollCheckout( getCheckout: () => Promise, options: { intervalMs?: number; timeoutMs?: number; sleep?: (ms: number) => Promise; stopped?: () => boolean; } = {}, ): Promise { const intervalMs = options.intervalMs ?? 1_500; const timeoutMs = options.timeoutMs ?? 30_000; const sleep = options.sleep ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); let elapsed = 0; let checkout = await getCheckout(); while (!options.stopped?.()) { if (checkout.fulfillment_status.toLowerCase() === "fulfilled") { return { outcome: "fulfilled", checkout }; } if (isTerminalCheckoutFailure(checkout)) return { outcome: "failed", checkout }; if (elapsed >= timeoutMs) return { outcome: "timeout", checkout }; const wait = Math.min(intervalMs, timeoutMs - elapsed); await sleep(wait); elapsed += wait; if (options.stopped?.()) return null; checkout = await getCheckout(); } return null; }