/** * SvelteKit-specific Autumn client with SSR support. * * Extends the vanilla Svelte client with SvelteKit features. */ import { getContext, setContext } from "svelte"; import { useConvexClient } from "convex-svelte"; import type { AutumnConvexApi, Customer, Product, CheckParams, CheckResult, CheckoutParams, CheckoutResult, TrackParams, TrackResult, AttachParams, AttachResult, CancelParams, BillingPortalParams, BillingPortalResult, CreateEntityParams, GetEntityParams, SetupPaymentParams, SetupPaymentResult, CreateReferralCodeParams, CreateReferralCodeResult, RedeemReferralCodeParams, RedeemReferralCodeResult, SetUsageParams, SetUsageResult, QueryParams, QueryResult, EventListParams, EventListResult, EventAggregateParams, Entity, LocalCheckResult, RefetchOptions, } from "../svelte/types.js"; import { unwrapAutumnResponse } from "../svelte/types.js"; import { isBrowser } from "../svelte/utils.js"; const AUTUMN_CONTEXT_KEY = "$$_autumn_sveltekit"; /** * Server state for SSR hydration. */ export interface AutumnServerState { customer: Customer | null; _timeFetched: number; } /** * SvelteKit's invalidate function type. * * Import from '$app/navigation' in your consuming application and pass it to the client. */ export type InvalidateFunction = (url: string | URL) => Promise; /** * Create an Autumn client for SvelteKit with SSR support. * * @param params - Configuration options * @param params.convexApi - The Autumn Convex API object * @param params.getServerState - Optional function to retrieve server state * @param params.invalidate - Optional SvelteKit invalidate function for data refetching * @returns The Autumn client API with reactive state and methods */ export function createAutumnClientSvelteKit({ convexApi, getServerState, invalidate, }: { convexApi: AutumnConvexApi; getServerState?: () => AutumnServerState; invalidate?: InvalidateFunction; }) { const client = useConvexClient(); // Wrap customer in container to enable Svelte reactivity tracking through property mutation. const _state = $state<{ customer: Customer | null }>({ customer: getServerState?.()?.customer ?? null }); // Sync customer data reactively from server state after invalidation. $effect(() => { const serverState = getServerState?.(); if (serverState?.customer) { _state.customer = serverState.customer; } else { _state.customer = null; } }); /** * Performs client-side feature access check. * * @param params - Check parameters including featureId and requiredBalance * @returns Local check result indicating whether access is allowed */ const allowed = (params: CheckParams): LocalCheckResult => { const customer = _state.customer; if (!customer) { return { allowed: false, reason: "No customer data" }; } const { featureId, requiredBalance = 1 } = params; const feature = customer.features?.[featureId]; if (!feature) { return { allowed: false, reason: "Feature not found" }; } if (feature.balance !== undefined && feature.balance < requiredBalance) { return { allowed: false, reason: `Insufficient balance: ${feature.balance} < ${requiredBalance}`, }; } return { allowed: true }; }; /** * Check feature access with server-side validation and usage tracking. * * @param params - Check parameters including featureId and requiredBalance * @param options - Options including whether to refetch customer data * @returns Check result from the server */ const check = async ( params: CheckParams, options: RefetchOptions = {}, ): Promise => { const { refetch = true } = options; const result = await client.action(convexApi.check, params); const response = unwrapAutumnResponse(result); if (refetch && invalidate) { await invalidate('autumn:customer'); } return response; }; /** * Initiate checkout flow. * * @param params - Checkout parameters including productId and optional dialog * @param options - Options including whether to refetch customer data * @returns The checkout result: a hosted Stripe `url`, or a purchase * preview to confirm with `attach` when `url` is absent */ const checkout = async ( params: CheckoutParams, options: RefetchOptions = {}, ): Promise => { const { refetch = true } = options; const result = await client.action(convexApi.checkout, params); const data = unwrapAutumnResponse(result); if (params.dialog && data.url) { if (isBrowser) { params.dialog(data.url); } } if (refetch && invalidate) { await invalidate('autumn:customer'); } return data; }; /** * Track usage of a feature. * * @param params - Track parameters including featureId and amount * @param options - Options including whether to refetch customer data * @returns Track result from the server */ const track = async ( params: TrackParams, options: RefetchOptions = {}, ): Promise => { const { refetch = true } = options; const result = await client.action(convexApi.track, params); const data = unwrapAutumnResponse(result); if (refetch && invalidate) { await invalidate('autumn:customer'); } return data; }; /** * Attach a product to the customer. * * @param params - Attach parameters including productId * @param options - Options including whether to refetch customer data * @returns The attach result, including `checkout_url` when the stored * payment method could not be charged */ const attach = async ( params: AttachParams, options: RefetchOptions = {}, ): Promise => { const { refetch = true } = options; const result = await client.action(convexApi.attach, params); const data = unwrapAutumnResponse(result); if (refetch && invalidate) { await invalidate('autumn:customer'); } return data; }; /** * Cancel a product subscription. * * @param params - Cancel parameters including productId * @param options - Options including whether to refetch customer data */ const cancel = async ( params: CancelParams, options: RefetchOptions = {}, ): Promise => { const { refetch = true } = options; const result = await client.action(convexApi.cancel, params); unwrapAutumnResponse(result); if (refetch && invalidate) { await invalidate('autumn:customer'); } }; /** * Open the billing portal. * * @param params - Optional billing portal parameters * @returns Object containing the billing portal URL */ const openBillingPortal = async ( params: BillingPortalParams = {}, ): Promise => { const result = await client.action(convexApi.billingPortal, params); const data = unwrapAutumnResponse(result); if (isBrowser && data.url) { window.open(data.url, "_blank"); } return data; }; /** * Create a new entity. * * @param params - Entity creation parameters including name and type * @param options - Options including whether to refetch customer data * @returns The created entity */ const createEntity = async ( params: CreateEntityParams, options: RefetchOptions = {}, ): Promise => { const { refetch = true } = options; const result = await client.action(convexApi.createEntity, params); const entity = unwrapAutumnResponse(result); if (refetch && invalidate) { await invalidate('autumn:customer'); } return entity; }; /** * Get an entity by ID. * * @param params - Parameters including the entity ID * @returns The requested entity */ const getEntity = async (params: GetEntityParams): Promise => { const result = await client.action(convexApi.getEntity, params); return unwrapAutumnResponse(result); }; /** * Setup payment method without immediate charge. * * @param params - Optional setup payment parameters * @param options - Options including whether to refetch customer data * @returns Object containing the setup payment URL */ const setupPayment = async ( params: SetupPaymentParams = {}, options: RefetchOptions = {}, ): Promise => { const { refetch = true } = options; const result = await client.action(convexApi.setupPayment, params); const data = unwrapAutumnResponse(result); if (isBrowser && data.url) { window.location.href = data.url; } if (refetch && invalidate) { await invalidate('autumn:customer'); } return data; }; /** * Create a referral code. * * @param params - Referral code creation parameters * @param options - Options including whether to refetch customer data * @returns Object containing the created referral code */ const createReferralCode = async ( params: CreateReferralCodeParams, options: RefetchOptions = {}, ): Promise => { const { refetch = true } = options; const result = await client.action(convexApi.createReferralCode, params); const data = unwrapAutumnResponse(result); if (refetch && invalidate) { await invalidate('autumn:customer'); } return data; }; /** * Redeem a referral code. * * @param params - Referral code redemption parameters including the code * @param options - Options including whether to refetch customer data * @returns Object containing the redemption result */ const redeemReferralCode = async ( params: RedeemReferralCodeParams, options: RefetchOptions = {}, ): Promise => { const { refetch = true } = options; const result = await client.action(convexApi.redeemReferralCode, params); const data = unwrapAutumnResponse(result); if (refetch && invalidate) { await invalidate('autumn:customer'); } return data; }; /** * List all available products. * * @returns Array of available products */ const listProducts = async (): Promise => { const result = await client.action(convexApi.listProducts, {}); const data = unwrapAutumnResponse<{ list: Product[] }>(result); return data.list; }; /** * Set usage to an absolute value. * * Use this to sync external usage data or reset usage to a specific value. * * @param params - Usage parameters including featureId and value * @returns Result of the usage update */ const usage = async (params: SetUsageParams): Promise => { const result = await client.action(convexApi.usage, params); return unwrapAutumnResponse(result); }; /** * Query customer data with custom parameters. * * @param params - Query parameters * @returns Query result data */ const query = async (params: QueryParams): Promise => { const result = await client.action(convexApi.query, params); return unwrapAutumnResponse(result); }; /** * List raw Autumn events without invalidating customer state. * * @param params - Event query parameters * @returns Paginated event list */ const listEvents = async (params: EventListParams): Promise => { const result = await client.action(convexApi.listEvents, params); return unwrapAutumnResponse(result); }; /** * Aggregate Autumn events without invalidating customer state. * * @param params - Event aggregation parameters * @returns Aggregate analytics payload */ const aggregateEvents = async ( params: EventAggregateParams, ): Promise => { const result = await client.action(convexApi.aggregateEvents, params); return unwrapAutumnResponse(result); }; /** * Manually refetch customer data. * * Uses SvelteKit's targeted invalidation for efficient refresh. * Only works if invalidate function was provided during client creation. */ const refetch = async (): Promise => { if (invalidate) { await invalidate('autumn:customer'); } }; const autumnApi = { // Use getter to enable Svelte reactivity tracking when reading customer state. get customer(): Customer | null { return _state.customer; }, allowed, check, checkout, track, attach, cancel, openBillingPortal, createEntity, getEntity, setupPayment, createReferralCode, redeemReferralCode, listProducts, usage, query, listEvents, aggregateEvents, refetch, }; return autumnApi; } /** * Set the Autumn client in the context. * * @param autumnClient - The Autumn client instance * @returns The Autumn client instance */ export function setAutumnContext( autumnClient: ReturnType, ) { setContext(AUTUMN_CONTEXT_KEY, autumnClient); return autumnClient; } /** * Get the Autumn client from the context. * * @returns The Autumn client instance from context */ export function getAutumnContext() { return getContext>( AUTUMN_CONTEXT_KEY, ); } /** * Check if Autumn has been set up. * * @returns True if the Autumn context exists */ export function hasAutumnContext(): boolean { try { return !!getContext(AUTUMN_CONTEXT_KEY); } catch { return false; } } /** * Creates reactive state for an Autumn operation with loading, error, and result tracking. * * This helper reduces boilerplate for managing loading states, error handling, and results * when calling Autumn billing operations. Perfect for operations triggered by user actions * like checkout, track, attach, etc. * * @template TParams - The parameters type for the operation * @template TResult - The result type returned by the operation * @param operation - The Autumn operation function to wrap (e.g., autumn.checkout) * @param defaultOptions - Default RefetchOptions to use for all executions * @returns Object with execute function and reactive state (isLoading, error, result, reset) * * @example * ```svelte * * * * * {#if upgrade.error} *

{upgrade.error.message}

* {/if} * ``` * * @example * ```svelte * * * {#if checkout.error} *
* {checkout.error.message} * *
* {/if} * * * ``` * * @example * ```svelte * * * ``` */ export function useAutumnOperation( operation: (params: TParams, options?: RefetchOptions) => Promise, defaultOptions?: RefetchOptions, ) { let isLoading = $state(false); let error = $state(null); let result = $state(null); async function execute( params: TParams, executeOptions?: RefetchOptions, ): Promise { isLoading = true; error = null; try { // Merge execute options with default options (execute options take precedence) const mergedOptions = { ...defaultOptions, ...executeOptions }; const operationResult = await operation(params, mergedOptions); result = operationResult; return operationResult; } catch (err) { error = err instanceof Error ? err : new Error(String(err)); result = null; return null; } finally { isLoading = false; } } function reset() { isLoading = false; error = null; result = null; } return { execute, get isLoading() { return isLoading; }, get error() { return error; }, get result() { return result; }, reset, }; }