@usethrottle/subscriptions 3.6.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/provider.tsx","../src/hooks.ts","../../core-cadence/src/cadence.ts","../../core-cadence/src/installments.ts","../../core-cadence/src/labels.ts","../src/helpers.ts","../src/components.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useMemo, useReducer, type ReactNode } from 'react';\nimport { SubscriptionsClient } from './client';\nimport type {\n SubscriptionFetcher,\n SubscriptionProviderProps,\n SubscriptionsClientContract,\n} from './types';\n\ninterface SubscriptionContextValue {\n client: SubscriptionsClientContract;\n cacheVersion: number;\n invalidate: () => void;\n}\n\nconst SubscriptionContext = createContext<SubscriptionContextValue | null>(null);\n\nexport function SubscriptionProvider({\n children,\n client,\n fetcher,\n baseUrl,\n timeoutMs,\n}: SubscriptionProviderProps) {\n const [cacheVersion, invalidate] = useReducer((value: number) => value + 1, 0);\n const resolvedClient = useMemo(() => {\n if (client) return client;\n return new SubscriptionsClient({\n fetcher: fetcher ?? createSubscriptionFetcher('/api/throttle'),\n baseUrl,\n timeoutMs,\n });\n }, [baseUrl, client, fetcher, timeoutMs]);\n\n const value = useMemo(\n () => ({ client: resolvedClient, cacheVersion, invalidate }),\n [cacheVersion, resolvedClient],\n );\n\n return <SubscriptionContext.Provider value={value}>{children}</SubscriptionContext.Provider>;\n}\n\nexport function useSubscriptionClient(): SubscriptionsClientContract {\n return useSubscriptionContext().client;\n}\n\nexport function useSubscriptionInvalidation(): () => void {\n return useSubscriptionContext().invalidate;\n}\n\nexport function useSubscriptionCacheVersion(): number {\n return useSubscriptionContext().cacheVersion;\n}\n\nexport function createSubscriptionFetcher(basePath: string): SubscriptionFetcher {\n const cleanBase = basePath.replace(/\\/+$/, '');\n return (path, init) => {\n const cleanPath = path.startsWith('/') ? path : `/${path}`;\n return fetch(`${cleanBase}${cleanPath}`, init);\n };\n}\n\nexport function useSubscriptionContext(): SubscriptionContextValue {\n const ctx = useContext(SubscriptionContext);\n if (!ctx) {\n throw new Error(\n '@usethrottle/subscriptions hooks must be used inside <SubscriptionProvider />',\n );\n }\n return ctx;\n}\n\nexport function SubscriptionProviderWithClient({\n children,\n client,\n}: {\n children: ReactNode;\n client: SubscriptionsClientContract;\n}) {\n const [cacheVersion, invalidate] = useReducer((value: number) => value + 1, 0);\n const value = useMemo(() => ({ client, cacheVersion, invalidate }), [cacheVersion, client]);\n return <SubscriptionContext.Provider value={value}>{children}</SubscriptionContext.Provider>;\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n useSubscriptionCacheVersion,\n useSubscriptionClient,\n useSubscriptionInvalidation,\n} from './provider';\nimport type {\n CancelSubscriptionInput,\n ChangePlanInput,\n CheckoutSession,\n CreateCheckoutSessionInput,\n CreateSubscriptionInput,\n Customer,\n CustomerPaymentMethod,\n ListSubscriptionsParams,\n MutationState,\n PaginatedSubscriptions,\n QueryOptions,\n QueryState,\n Subscription,\n TrialEligibilityInput,\n TrialEligibilityResult,\n UpdateSubscriptionInput,\n} from './types';\n\nexport function useSubscriptions(\n params: ListSubscriptionsParams = {},\n options: QueryOptions<PaginatedSubscriptions> = {},\n): QueryState<PaginatedSubscriptions> {\n const client = useSubscriptionClient();\n const cacheVersion = useSubscriptionCacheVersion();\n const stableParams = useStableValue(params);\n return useAsyncQuery(\n () => client.list(stableParams),\n [client, stableParams, cacheVersion],\n options,\n );\n}\n\nexport function useSubscription(\n id: string | null | undefined,\n options: QueryOptions<Subscription> = {},\n): QueryState<Subscription> {\n const client = useSubscriptionClient();\n const cacheVersion = useSubscriptionCacheVersion();\n return useAsyncQuery(\n () => (id ? client.get(id) : Promise.resolve(null)),\n [client, id, cacheVersion],\n { ...options, enabled: options.enabled ?? Boolean(id) },\n );\n}\n\nexport function useCustomerByExternalId(\n externalId: string | null | undefined,\n options: QueryOptions<Customer> = {},\n): QueryState<Customer> {\n const client = useSubscriptionClient();\n const cacheVersion = useSubscriptionCacheVersion();\n return useAsyncQuery(\n () => (externalId ? client.getCustomerByExternalId(externalId) : Promise.resolve(null)),\n [client, externalId, cacheVersion],\n { ...options, enabled: options.enabled ?? Boolean(externalId) },\n );\n}\n\nexport function useCustomerPaymentMethods(\n customerId: string | null | undefined,\n options: QueryOptions<CustomerPaymentMethod[]> = {},\n): QueryState<CustomerPaymentMethod[]> {\n const client = useSubscriptionClient();\n const cacheVersion = useSubscriptionCacheVersion();\n return useAsyncQuery(\n () => (customerId ? client.listCustomerPaymentMethods(customerId) : Promise.resolve(null)),\n [client, customerId, cacheVersion],\n { ...options, enabled: options.enabled ?? Boolean(customerId) },\n );\n}\n\nexport function useTrialEligibility(): MutationState<\n TrialEligibilityResult,\n TrialEligibilityInput\n> {\n const client = useSubscriptionClient();\n return useAsyncMutation((input) => client.checkTrialEligibility(input));\n}\n\nexport function useCreateSubscription(): MutationState<Subscription, CreateSubscriptionInput> {\n const client = useSubscriptionClient();\n const invalidate = useSubscriptionInvalidation();\n return useAsyncMutation((input) => client.create(input), invalidate);\n}\n\nexport function useUpdateSubscription(): MutationState<\n Subscription,\n { id: string; data: UpdateSubscriptionInput }\n> {\n const client = useSubscriptionClient();\n const invalidate = useSubscriptionInvalidation();\n return useAsyncMutation(({ id, data }) => client.update(id, data), invalidate);\n}\n\nexport function useChangePlan(): MutationState<Subscription, ChangePlanInput> {\n const client = useSubscriptionClient();\n const invalidate = useSubscriptionInvalidation();\n return useAsyncMutation((input) => client.changePlan(input), invalidate);\n}\n\nexport function useCancelSubscription(): MutationState<Subscription, CancelSubscriptionInput> {\n const client = useSubscriptionClient();\n const invalidate = useSubscriptionInvalidation();\n return useAsyncMutation(\n (input) => client.cancel(input.id, { atPeriodEnd: input.atPeriodEnd }),\n invalidate,\n );\n}\n\nexport function usePauseSubscription(): MutationState<Subscription, { id: string }> {\n const client = useSubscriptionClient();\n const invalidate = useSubscriptionInvalidation();\n return useAsyncMutation((input) => client.pause(input.id), invalidate);\n}\n\nexport function useResumeSubscription(): MutationState<Subscription, { id: string }> {\n const client = useSubscriptionClient();\n const invalidate = useSubscriptionInvalidation();\n return useAsyncMutation((input) => client.resume(input.id), invalidate);\n}\n\nexport function useCreateCheckoutSession(): MutationState<\n CheckoutSession,\n CreateCheckoutSessionInput\n> {\n const client = useSubscriptionClient();\n return useAsyncMutation((input) => client.createCheckoutSession(input));\n}\n\nfunction useAsyncQuery<T>(\n load: () => Promise<T | null>,\n deps: readonly unknown[],\n options: QueryOptions<T>,\n): QueryState<T> {\n const enabled = options.enabled ?? true;\n const [data, setData] = useState<T | null>(options.initialData ?? null);\n const [error, setError] = useState<unknown>(null);\n const [isLoading, setIsLoading] = useState(enabled && options.initialData === undefined);\n const [isFetching, setIsFetching] = useState(false);\n const loadRef = useRef(load);\n loadRef.current = load;\n\n const refetch = useCallback(async () => {\n if (!enabled) return data;\n setIsFetching(true);\n setError(null);\n try {\n const next = await loadRef.current();\n setData(next);\n return next;\n } catch (err) {\n setError(err);\n throw err;\n } finally {\n setIsFetching(false);\n setIsLoading(false);\n }\n }, [data, enabled]);\n\n useEffect(() => {\n if (!enabled) {\n setIsLoading(false);\n return;\n }\n let cancelled = false;\n setIsFetching(true);\n setError(null);\n load()\n .then((next) => {\n if (!cancelled) setData(next);\n })\n .catch((err) => {\n if (!cancelled) setError(err);\n })\n .finally(() => {\n if (!cancelled) {\n setIsLoading(false);\n setIsFetching(false);\n }\n });\n\n return () => {\n cancelled = true;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [enabled, ...deps]);\n\n return { data, error, isLoading, isFetching, refetch };\n}\n\nfunction useAsyncMutation<TData, TVariables>(\n action: (variables: TVariables) => Promise<TData>,\n onSuccess?: () => void,\n): MutationState<TData, TVariables> {\n const [data, setData] = useState<TData | null>(null);\n const [error, setError] = useState<unknown>(null);\n const [isPending, setIsPending] = useState(false);\n\n const mutateAsync = useCallback(\n async (variables: TVariables) => {\n setIsPending(true);\n setError(null);\n try {\n const next = await action(variables);\n setData(next);\n onSuccess?.();\n return next;\n } catch (err) {\n setError(err);\n throw err;\n } finally {\n setIsPending(false);\n }\n },\n [action, onSuccess],\n );\n\n const mutate = useCallback(\n (variables: TVariables) => {\n void mutateAsync(variables).catch(() => undefined);\n },\n [mutateAsync],\n );\n\n const reset = useCallback(() => {\n setData(null);\n setError(null);\n setIsPending(false);\n }, []);\n\n return { data, error, isPending, mutate, mutateAsync, reset };\n}\n\nfunction useStableValue<T>(value: T): T {\n const key = JSON.stringify(value);\n return useMemo(() => value, [key]);\n}\n","/**\n * A subscription's billing cadence: \"every N units\".\n *\n * Every subscription row stores a unit and a count; the five named intervals\n * are shorthand for particular combinations. This module is the only place\n * that knows the vocabulary, the bounds, and how a period boundary is\n * computed — the engine, the renewal cron, the API, the dashboard and the\n * buyer surfaces all import it. Before it existed there were three divergent\n * copies of the period math, and the renewal cron's advanced months with\n * `setMonth`, which overflows: a subscription started on Jan 31 renewed on\n * Mar 3 and never returned to the 31st.\n */\n\nexport const INTERVAL_UNITS = ['hour', 'day', 'week', 'month'] as const;\nexport type IntervalUnit = (typeof INTERVAL_UNITS)[number];\n\nexport const NAMED_INTERVALS = ['weekly', 'biweekly', 'monthly', 'quarterly', 'yearly'] as const;\nexport type NamedInterval = (typeof NAMED_INTERVALS)[number];\n\n/** What the `interval` field holds: a named interval, or `custom` for everything else. */\nexport const INTERVAL_VALUES = [...NAMED_INTERVALS, 'custom'] as const;\nexport type IntervalValue = (typeof INTERVAL_VALUES)[number];\n\nexport interface Cadence {\n interval: IntervalValue;\n unit: IntervalUnit;\n count: number;\n}\n\n/** Anything that describes a cadence the way the API and the database do. */\nexport interface CadenceLike {\n interval: string;\n intervalUnit?: string | null;\n intervalCount?: number | null;\n}\n\nexport interface CadenceInput {\n interval?: string | null;\n intervalUnit?: string | null;\n intervalCount?: number | null;\n}\n\nexport const NAMED_SHORTHAND: Readonly<Record<NamedInterval, { unit: IntervalUnit; count: number }>> = {\n weekly: { unit: 'week', count: 1 },\n biweekly: { unit: 'week', count: 2 },\n monthly: { unit: 'month', count: 1 },\n quarterly: { unit: 'month', count: 3 },\n // month × 12 rather than a year unit, so a Feb 29 anniversary clamps to Feb\n // 28 and comes back to Feb 29 in leap years like every other month cadence.\n yearly: { unit: 'month', count: 12 },\n};\n\n/** At most one year in every unit. The shortest cadence is 1 hour. */\nexport const MAX_INTERVAL_COUNT: Readonly<Record<IntervalUnit, number>> = {\n hour: 8760,\n day: 365,\n week: 52,\n month: 12,\n};\n\nexport const HOUR_MS = 3_600_000;\nexport const DAY_MS = 24 * HOUR_MS;\nexport const WEEK_MS = 7 * DAY_MS;\n\nconst FIXED_UNIT_MS = { hour: HOUR_MS, day: DAY_MS, week: WEEK_MS } as const;\n\nexport class CadenceError extends Error {\n readonly statusCode = 400;\n constructor(\n readonly code: 'invalid_interval' | 'interval_conflict',\n message: string,\n ) {\n super(message);\n this.name = 'CadenceError';\n }\n}\n\nexport function isNamedInterval(value: unknown): value is NamedInterval {\n return typeof value === 'string' && (NAMED_INTERVALS as readonly string[]).includes(value);\n}\n\nexport function isIntervalUnit(value: unknown): value is IntervalUnit {\n return typeof value === 'string' && (INTERVAL_UNITS as readonly string[]).includes(value);\n}\n\nfunction isIntervalValue(value: unknown): value is IntervalValue {\n return typeof value === 'string' && (INTERVAL_VALUES as readonly string[]).includes(value);\n}\n\nfunction reduceToLargestUnit(unit: IntervalUnit, count: number): { unit: IntervalUnit; count: number } {\n let u = unit;\n let c = count;\n if (u === 'hour' && c % 24 === 0) {\n u = 'day';\n c = c / 24;\n }\n if (u === 'day' && c % 7 === 0) {\n u = 'week';\n c = c / 7;\n }\n return { unit: u, count: c };\n}\n\nfunction namedFor(unit: IntervalUnit, count: number): NamedInterval | null {\n for (const name of NAMED_INTERVALS) {\n const s = NAMED_SHORTHAND[name];\n if (s.unit === unit && s.count === count) return name;\n }\n return null;\n}\n\n/**\n * Turn whatever a caller sent into the one stored form. Accepts a named\n * interval alone, a unit and count alone, or both — in which case they must\n * agree. The result is reduced to its largest exact unit and mapped to a named\n * interval whenever one matches, so there is exactly one representation per\n * cadence and \"every 2 weeks\" can never be filed apart from \"biweekly\".\n */\nexport function canonicalizeCadence(input: CadenceInput): Cadence {\n const interval = input.interval ?? undefined;\n if (interval !== undefined && !isIntervalValue(interval)) {\n throw new CadenceError('invalid_interval', `Unknown interval \"${interval}\"`);\n }\n const hasUnit = input.intervalUnit != null;\n const hasCount = input.intervalCount != null;\n if (hasUnit !== hasCount) {\n throw new CadenceError('invalid_interval', 'intervalUnit and intervalCount must be sent together');\n }\n if (!hasUnit) {\n if (isNamedInterval(interval)) return { interval, ...NAMED_SHORTHAND[interval] };\n throw new CadenceError(\n 'invalid_interval',\n interval === 'custom'\n ? 'A custom interval needs intervalUnit and intervalCount'\n : 'Send an interval, or intervalUnit and intervalCount',\n );\n }\n\n const unit = input.intervalUnit;\n const count = input.intervalCount as number;\n if (!isIntervalUnit(unit)) {\n throw new CadenceError('invalid_interval', 'intervalUnit must be one of hour, day, week, month');\n }\n if (!Number.isInteger(count) || count < 1) {\n throw new CadenceError('invalid_interval', 'intervalCount must be a whole number of at least 1');\n }\n if (count > MAX_INTERVAL_COUNT[unit]) {\n throw new CadenceError(\n 'invalid_interval',\n `A cadence can be at most one year: ${MAX_INTERVAL_COUNT[unit]} ${unit}s`,\n );\n }\n\n const reduced = reduceToLargestUnit(unit, count);\n const cadence: Cadence = { interval: namedFor(reduced.unit, reduced.count) ?? 'custom', ...reduced };\n if (interval !== undefined && interval !== 'custom' && interval !== cadence.interval) {\n throw new CadenceError(\n 'interval_conflict',\n `interval \"${interval}\" does not match every ${count} ${unit}${count === 1 ? '' : 's'}`,\n );\n }\n return cadence;\n}\n\n/**\n * Read the cadence off a stored subscription. A named interval always means\n * its shorthand, whatever the unit/count columns hold: a canonical row's\n * columns match the shorthand anyway, and code that predates the columns\n * (still serving between migration 0224 and the deploy) changes `interval`\n * without touching them, so for a named row they can be stale. Only a\n * `custom` row is read from its unit and count. The same rule applies to the\n * pending cadence — pass `{ interval: pendingInterval, intervalUnit:\n * pendingIntervalUnit, intervalCount: pendingIntervalCount }`.\n */\nexport function cadenceOf(row: CadenceLike): Cadence {\n if (isNamedInterval(row.interval)) return { interval: row.interval, ...NAMED_SHORTHAND[row.interval] };\n if (row.interval === 'custom' && row.intervalUnit != null && row.intervalCount != null && isIntervalUnit(row.intervalUnit)) {\n return { interval: 'custom', unit: row.intervalUnit, count: row.intervalCount };\n }\n throw new Error(`Unknown subscription interval: ${row.interval}`);\n}\n\n/** The exact period length for hour/day/week cadences; null for months, which vary. */\nexport function fixedPeriodMs(cadence: Cadence): number | null {\n return cadence.unit === 'month' ? null : cadence.count * FIXED_UNIT_MS[cadence.unit];\n}\n\n/** The shortest a period can be. A month counts as 28 days. */\nexport function minimumPeriodMs(cadence: Cadence): number {\n return fixedPeriodMs(cadence) ?? cadence.count * 28 * DAY_MS;\n}\n\n/** anchor + months, keeping the anchor's day-of-month (clamped to month end) and UTC time of day. */\nexport function addMonthsAnchored(anchor: Date, months: number): Date {\n const monthIndex = anchor.getUTCMonth() + months;\n const year = anchor.getUTCFullYear() + Math.floor(monthIndex / 12);\n const month = ((monthIndex % 12) + 12) % 12;\n const lastDay = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();\n return new Date(\n Date.UTC(\n year,\n month,\n Math.min(anchor.getUTCDate(), lastDay),\n anchor.getUTCHours(),\n anchor.getUTCMinutes(),\n anchor.getUTCSeconds(),\n anchor.getUTCMilliseconds(),\n ),\n );\n}\n\n/**\n * The first period boundary strictly after `after`, on the grid measured from\n * `anchor`. Never chained from a previous boundary — that is what made the\n * month-end clamp stick on the 28th. An `after` that is off the grid (a late\n * tick, a recovery) resolves to the next grid date.\n */\nexport function nextPeriodEnd(args: { anchor: Date; cadence: Cadence; after: Date }): Date {\n const { anchor, cadence, after } = args;\n const step = fixedPeriodMs(cadence);\n if (step !== null) {\n const k = Math.max(1, Math.floor((after.getTime() - anchor.getTime()) / step) + 1);\n return new Date(anchor.getTime() + k * step);\n }\n const monthsElapsed =\n (after.getUTCFullYear() - anchor.getUTCFullYear()) * 12 + (after.getUTCMonth() - anchor.getUTCMonth());\n let k = Math.max(1, Math.floor(monthsElapsed / cadence.count));\n let boundary = addMonthsAnchored(anchor, k * cadence.count);\n while (boundary.getTime() <= after.getTime()) {\n k += 1;\n boundary = addMonthsAnchored(anchor, k * cadence.count);\n }\n return boundary;\n}\n\n/**\n * The grid period that contains `at`: the latest boundary at or before it (never\n * before the anchor) and the one after. A subscription that fell more than a\n * period behind is billed once, for this period, and the periods it missed are\n * skipped rather than charged back to back (D38).\n */\nexport function periodContaining(args: { anchor: Date; cadence: Cadence; at: Date }): { start: Date; end: Date } {\n const { anchor, cadence, at } = args;\n const step = fixedPeriodMs(cadence);\n if (step !== null) {\n const k = Math.max(0, Math.floor((at.getTime() - anchor.getTime()) / step));\n const start = new Date(anchor.getTime() + k * step);\n return { start, end: new Date(start.getTime() + step) };\n }\n const monthsElapsed =\n (at.getUTCFullYear() - anchor.getUTCFullYear()) * 12 + (at.getUTCMonth() - anchor.getUTCMonth());\n let k = Math.max(0, Math.floor(monthsElapsed / cadence.count));\n let start = addMonthsAnchored(anchor, k * cadence.count);\n while (k > 0 && start.getTime() > at.getTime()) {\n k -= 1;\n start = addMonthsAnchored(anchor, k * cadence.count);\n }\n return { start, end: addMonthsAnchored(anchor, (k + 1) * cadence.count) };\n}\n\nconst RETRY_CAP_DAYS = [1, 3, 7] as const;\n\n/**\n * How long to wait before retry number `failureNumber` (1..3). Short cadences\n * retry proportionally faster; anything of a week or longer lands on the\n * original 1 / 3 / 7 days exactly.\n */\nexport function dunningRetryDelayMs(cadence: Cadence, failureNumber: number): number {\n const n = Math.min(Math.max(Math.trunc(failureNumber), 1), RETRY_CAP_DAYS.length);\n return Math.min(n * minimumPeriodMs(cadence), RETRY_CAP_DAYS[n - 1] * DAY_MS);\n}\n\n/** The 3-days-ahead renewal reminder only makes sense for cadences of a week or longer. */\nexport function sendsRenewalReminder(cadence: Cadence): boolean {\n return minimumPeriodMs(cadence) >= WEEK_MS;\n}\n\n/**\n * Multiply a period's charge by this to get its monthly equivalent. Chosen so\n * `weekly` is exactly 52/12 and `biweekly` exactly 26/12, the factors\n * analytics already report — no existing MRR number moves.\n */\nexport function monthlyEquivalentFactor(cadence: Cadence): number {\n if (cadence.unit === 'month') return 1 / cadence.count;\n return (52 / 12) * (WEEK_MS / (fixedPeriodMs(cadence) as number));\n}\n","/**\n * Installment plans: a subscription with a fixed number of payments that ends\n * itself — status `completed` — the moment the last one succeeds.\n */\n\nexport const MIN_TOTAL_PAYMENTS = 2;\nexport const MAX_TOTAL_PAYMENTS = 60;\n\nexport type InstallmentErrorCode =\n | 'invalid_total_payments'\n | 'invalid_combination'\n | 'total_payments_immutable'\n | 'installment_plan_locked'\n | 'installment_plan_merchant_only';\n\nexport class InstallmentError extends Error {\n constructor(\n readonly code: InstallmentErrorCode,\n message: string,\n readonly statusCode: number,\n ) {\n super(message);\n this.name = 'InstallmentError';\n }\n}\n\nexport function validateTotalPayments(value: unknown): number | null {\n if (value === undefined || value === null) return null;\n if (\n typeof value !== 'number' ||\n !Number.isInteger(value) ||\n value < MIN_TOTAL_PAYMENTS ||\n value > MAX_TOTAL_PAYMENTS\n ) {\n throw new InstallmentError(\n 'invalid_total_payments',\n `totalPayments must be a whole number from ${MIN_TOTAL_PAYMENTS} to ${MAX_TOTAL_PAYMENTS}`,\n 400,\n );\n }\n return value;\n}\n\nexport function isInstallmentPlan(sub: { totalPayments?: number | null }): boolean {\n return sub.totalPayments != null;\n}\n\n/** After one counted payment — a charge or a waive — the new count and whether the plan is paid in full. */\nexport function countInstallmentPayment(sub: {\n totalPayments?: number | null;\n paymentsMade?: number | null;\n}): { paymentsMade: number; completes: boolean } | null {\n if (sub.totalPayments == null) return null;\n const paymentsMade = (sub.paymentsMade ?? 0) + 1;\n return { paymentsMade, completes: paymentsMade >= sub.totalPayments };\n}\n\nexport interface InstallmentProgress {\n paymentsMade: number;\n totalPayments: number;\n remainingPayments: number;\n remainingCents: number;\n}\n\nexport function installmentProgress(sub: {\n totalPayments?: number | null;\n paymentsMade?: number | null;\n amount: number;\n quantity?: number | null;\n scheduleRows?: Array<{ unitAmount: number; discount: number }> | null;\n}): InstallmentProgress | null {\n if (sub.totalPayments == null) return null;\n const paymentsMade = Math.min(sub.paymentsMade ?? 0, sub.totalPayments);\n const remainingPayments = sub.totalPayments - paymentsMade;\n const quantity = sub.quantity ?? 1;\n const remainingCents =\n sub.scheduleRows && sub.scheduleRows.length > 0\n ? sub.scheduleRows.slice(paymentsMade).reduce((s, r) => s + r.unitAmount * quantity - r.discount, 0)\n : remainingPayments * sub.amount * quantity;\n return { paymentsMade, totalPayments: sub.totalPayments, remainingPayments, remainingCents };\n}\n\nexport function installmentLocked(): InstallmentError {\n return new InstallmentError(\n 'installment_plan_locked',\n \"An installment plan's amount, cadence, quantity and plan are fixed when it is created. Cancel it and create a new one to change the terms.\",\n 409,\n );\n}\n\nexport function merchantOnly(): InstallmentError {\n return new InstallmentError(\n 'installment_plan_merchant_only',\n 'Installment plans can only be paused, resumed or cancelled by the merchant',\n 403,\n );\n}\n\nexport function totalPaymentsImmutable(): InstallmentError {\n return new InstallmentError(\n 'total_payments_immutable',\n 'totalPayments is set when a subscription is created and cannot be changed',\n 400,\n );\n}\n","import { isIntervalUnit, isNamedInterval, type CadenceLike, type NamedInterval } from './cadence';\nimport { isVariedSchedule, scheduleUnitTotal } from './schedule';\n\nconst NAMED_NOUN: Record<NamedInterval, string> = {\n weekly: 'week',\n biweekly: '2 weeks',\n monthly: 'month',\n quarterly: 'quarter',\n yearly: 'year',\n};\n\nconst NAMED_LABEL: Record<NamedInterval, string> = {\n weekly: 'Weekly',\n biweekly: 'Biweekly',\n monthly: 'Monthly',\n quarterly: 'Quarterly',\n yearly: 'Yearly',\n};\n\n/**\n * The period as a noun — what reads correctly after a slash: \"$49.00 / 10\n * days\". This replaces the per-file `INTERVAL_WORD` maps whose drift shipped\n * \"$49.00 / biweekly\" once already. Unknown input falls through unchanged so a\n * value this build doesn't know still renders instead of vanishing.\n */\nexport function cadenceNoun(cadence: CadenceLike): string {\n if (isNamedInterval(cadence.interval)) return NAMED_NOUN[cadence.interval];\n if (isIntervalUnit(cadence.intervalUnit) && cadence.intervalCount != null) {\n return cadence.intervalCount === 1 ? cadence.intervalUnit : `${cadence.intervalCount} ${cadence.intervalUnit}s`;\n }\n return cadence.interval;\n}\n\n/** The cadence as a standalone label, e.g. an Interval column: \"Monthly\", \"Every 10 days\". */\nexport function cadenceLabel(cadence: CadenceLike): string {\n if (isNamedInterval(cadence.interval)) return NAMED_LABEL[cadence.interval];\n if (isIntervalUnit(cadence.intervalUnit) && cadence.intervalCount != null) return `Every ${cadenceNoun(cadence)}`;\n return cadence.interval;\n}\n\n/** Sub-daily cadences show the time of the next charge, not just its date. */\nexport function isSubDaily(cadence: CadenceLike): boolean {\n return cadence.intervalUnit === 'hour';\n}\n\n/** A subscription line's terms, the fields a label needs. */\nexport interface RecurringTermsLike extends CadenceLike {\n totalPayments?: number | null;\n trialDays?: number | null;\n schedule?: Array<{ amount: number }> | null;\n}\n\n/**\n * One short label for a subscription line's terms, for carts and quotes:\n * \"$20.00 / month\", \"$20.00 / 10 days · 3 payments\", \"Free for 14 days, then\n * $20.00 / month\", \"$100.00 total · 4 payments\". The label computes the money\n * itself from the line's unit price and seats — for a schedule, the deal total\n * (every payment × seats), never the line's unit price (payment 1) — and hands\n * it to `money`, so each surface keeps its own money style.\n */\nexport function recurringTermsLabel(\n terms: RecurringTermsLike,\n price: { unitPrice: number; quantity?: number | null },\n money: (minor: number) => string,\n): string {\n const quantity = price.quantity ?? 1;\n if (terms.schedule && terms.schedule.length > 0) {\n return `${money(scheduleUnitTotal(terms.schedule) * quantity)} total · ${terms.schedule.length} payments`;\n }\n const each = money(price.unitPrice * quantity);\n const noun = cadenceNoun(terms);\n const trialDays = terms.trialDays ?? 0;\n if (trialDays > 0) return `Free for ${trialDays} day${trialDays === 1 ? '' : 's'}, then ${each} / ${noun}`;\n return `${each} / ${noun}` + (terms.totalPayments != null ? ` · ${terms.totalPayments} payments` : '');\n}\n\n/** A subscription's public schedule fields, as the API returns them. */\nexport interface ScheduleDealLike {\n paymentSchedule?: Array<{ amount: number; discount: number; intervalUnit: string | null; intervalCount: number | null }> | null;\n scheduleTotal?: number | null;\n}\n\n/**\n * A varied schedule's deal — its total (every row's net: seats in, discounts\n * out) and its payment count — or null for an equal plan, whose synthesized\n * schedule keeps the per-period label, and for an ongoing plan. A varied\n * schedule has no per-period price: `amount` and the cadence columns hold only\n * the next row's (ruling R-FR4). Every surface gates both its price and its\n * cadence cells on this one answer.\n */\nexport function scheduleDeal(sub: ScheduleDealLike): { total: number; payments: number } | null {\n if (!sub.paymentSchedule || sub.scheduleTotal == null || !isVariedSchedule(sub.paymentSchedule)) return null;\n return { total: sub.scheduleTotal, payments: sub.paymentSchedule.length };\n}\n\n/** \"$100.00 total · 4 payments\" for a varied schedule, else null. `money` keeps each surface's money style. */\nexport function scheduleDealLabel(sub: ScheduleDealLike, money: (minor: number) => string): string | null {\n const deal = scheduleDeal(sub);\n return deal ? `${money(deal.total)} total · ${deal.payments} payments` : null;\n}\n","import {\n cadenceNoun as coreCadenceNoun,\n cadenceLabel as coreCadenceLabel,\n installmentProgress as coreInstallmentProgress,\n} from '@core/cadence';\nimport type { Subscription, SubscriptionPaymentScheduleRow, SubscriptionStatus } from './types';\n\n/** `cancelled` and `completed` (an installment plan paid in full) never bill again. */\nexport function isTerminalSubscription(status: SubscriptionStatus | Subscription): boolean {\n const s = getStatus(status);\n return s === 'cancelled' || s === 'completed';\n}\n\nexport function canPauseSubscription(status: SubscriptionStatus | Subscription): boolean {\n return getStatus(status) === 'active';\n}\n\nexport function canResumeSubscription(status: SubscriptionStatus | Subscription): boolean {\n return getStatus(status) === 'paused';\n}\n\nexport function canCancelSubscription(status: SubscriptionStatus | Subscription): boolean {\n return !isTerminalSubscription(status);\n}\n\nexport function isSubscriptionPastDue(status: SubscriptionStatus | Subscription): boolean {\n return getStatus(status) === 'past_due';\n}\n\nexport function isSubscriptionTrialing(status: SubscriptionStatus | Subscription): boolean {\n return getStatus(status) === 'trialing';\n}\n\nexport function daysUntil(date: string | Date, now = new Date()): number {\n const end = date instanceof Date ? date : new Date(date);\n const ms = end.getTime() - now.getTime();\n if (!Number.isFinite(ms)) return 0;\n return Math.max(0, Math.ceil(ms / 86_400_000));\n}\n\nfunction getStatus(input: SubscriptionStatus | Subscription): SubscriptionStatus {\n return typeof input === 'string' ? input : input.status;\n}\n\n/**\n * Anything that describes a cadence the way the API and the database do.\n * Structurally mirrors the private cadence engine's own shape — kept local\n * so the published `.d.ts` never names that unpublished package.\n */\nexport interface CadenceLike {\n interval: string;\n intervalUnit?: string | null;\n intervalCount?: number | null;\n}\n\nexport interface InstallmentProgress {\n paymentsMade: number;\n totalPayments: number;\n remainingPayments: number;\n remainingCents: number;\n}\n\n/** \"10 days\", \"2 weeks\", \"month\" — the period after a slash: `$49.00 / ${cadenceNoun(sub)}`. */\nexport function cadenceNoun(cadence: CadenceLike): string {\n return coreCadenceNoun(cadence);\n}\n\n/** The cadence as a standalone label, e.g. an Interval column: \"Monthly\", \"Every 10 days\". */\nexport function cadenceLabel(cadence: CadenceLike): string {\n return coreCadenceLabel(cadence);\n}\n\n/**\n * Progress of an installment plan. Pass the subscription itself: when it carries\n * `paymentSchedule` (a custom schedule), `remainingCents` is the sum of the\n * not-yet-paid rows' `net` (already net of discount, quantity applied, before\n * tax). Otherwise it is `remainingPayments × amount × quantity`.\n */\nexport function installmentProgress(sub: {\n totalPayments?: number | null;\n paymentsMade?: number | null;\n amount: number;\n quantity?: number | null;\n paymentSchedule?: Array<Pick<SubscriptionPaymentScheduleRow, 'net' | 'status'>> | null;\n}): InstallmentProgress | null {\n const progress = coreInstallmentProgress(sub);\n if (!progress || !sub.paymentSchedule || sub.paymentSchedule.length === 0) return progress;\n const remainingCents = sub.paymentSchedule.filter((r) => r.status !== 'paid').reduce((sum, r) => sum + r.net, 0);\n return { ...progress, remainingCents };\n}\n","import type { CSSProperties } from 'react';\nimport { daysUntil } from './helpers';\nimport type {\n DunningBannerProps,\n SubscriptionStatus,\n SubscriptionStatusBadgeProps,\n TrialCountdownProps,\n} from './types';\n\nconst STATUS_LABELS: Record<SubscriptionStatus, string> = {\n active: 'Active',\n paused: 'Paused',\n cancelled: 'Cancelled',\n past_due: 'Past due',\n trialing: 'Trialing',\n unpaid: 'Unpaid',\n completed: 'Paid in full',\n};\n\nconst STATUS_STYLES: Record<SubscriptionStatus, CSSProperties> = {\n active: { color: '#035c4f', background: '#dff8ef', borderColor: '#a8e8d2' },\n paused: { color: '#735c00', background: '#fff4ca', borderColor: '#f3d46b' },\n cancelled: { color: '#6c2632', background: '#ffe1e7', borderColor: '#f4a9b8' },\n past_due: { color: '#7a2d00', background: '#ffe8d6', borderColor: '#f5b47f' },\n trialing: { color: '#124c73', background: '#dff1ff', borderColor: '#9ed3fb' },\n unpaid: { color: '#7a2d00', background: '#ffe8d6', borderColor: '#f5b47f' },\n completed: { color: '#035c4f', background: '#dff8ef', borderColor: '#a8e8d2' },\n};\n\nexport function SubscriptionStatusBadge({\n status,\n className,\n style,\n labels,\n}: SubscriptionStatusBadgeProps) {\n return (\n <span\n className={className}\n style={{\n display: 'inline-flex',\n alignItems: 'center',\n border: '1px solid',\n borderRadius: 999,\n padding: '2px 8px',\n fontSize: 12,\n fontWeight: 700,\n lineHeight: 1.6,\n whiteSpace: 'nowrap',\n ...STATUS_STYLES[status],\n ...style,\n }}\n >\n {labels?.[status] ?? STATUS_LABELS[status]}\n </span>\n );\n}\n\nexport function TrialCountdown({\n endDate,\n now,\n className,\n style,\n expiredLabel = 'Trial ended',\n prefix = 'Trial ends in',\n}: TrialCountdownProps) {\n const days = daysUntil(endDate, now);\n const label = days <= 0 ? expiredLabel : `${prefix} ${days} ${days === 1 ? 'day' : 'days'}`;\n\n return (\n <span className={className} style={style}>\n {label}\n </span>\n );\n}\n\nexport function DunningBanner({\n subscription,\n onUpdateCard,\n title = 'Payment needs attention',\n message,\n actionLabel = 'Update card',\n className,\n style,\n}: DunningBannerProps) {\n if (subscription.status !== 'past_due') return null;\n const fallbackMessage =\n subscription.failureCount > 0\n ? `We could not renew this subscription after ${subscription.failureCount} failed payment attempt${\n subscription.failureCount === 1 ? '' : 's'\n }.`\n : 'We could not renew this subscription with the saved payment method.';\n\n return (\n <div\n role=\"status\"\n className={className}\n style={{\n border: '1px solid #f5b47f',\n borderRadius: 8,\n background: '#fff7ed',\n color: '#7a2d00',\n padding: 16,\n display: 'grid',\n gap: 10,\n ...style,\n }}\n >\n <strong>{title}</strong>\n <span>{message ?? fallbackMessage}</span>\n {onUpdateCard ? (\n <button\n type=\"button\"\n onClick={onUpdateCard}\n style={{\n width: 'fit-content',\n border: '1px solid #7a2d00',\n borderRadius: 6,\n background: '#7a2d00',\n color: '#fff',\n padding: '8px 12px',\n fontWeight: 700,\n cursor: 'pointer',\n }}\n >\n {actionLabel}\n </button>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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","names":["value","useCallback","useMemo","useCallback","useMemo","cadenceNoun","cadenceLabel","installmentProgress","jsx"]}
1
+ {"version":3,"sources":["../src/provider.tsx","../src/hooks.ts","../../core-cadence/src/cadence.ts","../../core-cadence/src/installments.ts","../../core-cadence/src/labels.ts","../src/helpers.ts","../src/components.tsx","../src/index.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useMemo, useReducer, type ReactNode } from 'react';\nimport { SubscriptionsClient } from './client';\nimport type {\n SubscriptionFetcher,\n SubscriptionProviderProps,\n SubscriptionsClientContract,\n} from './types';\n\ninterface SubscriptionContextValue {\n client: SubscriptionsClientContract;\n cacheVersion: number;\n invalidate: () => void;\n}\n\nconst SubscriptionContext = createContext<SubscriptionContextValue | null>(null);\n\nexport function SubscriptionProvider({\n children,\n client,\n fetcher,\n baseUrl,\n timeoutMs,\n}: SubscriptionProviderProps) {\n const [cacheVersion, invalidate] = useReducer((value: number) => value + 1, 0);\n const resolvedClient = useMemo(() => {\n if (client) return client;\n return new SubscriptionsClient({\n fetcher: fetcher ?? createSubscriptionFetcher('/api/throttle'),\n baseUrl,\n timeoutMs,\n });\n }, [baseUrl, client, fetcher, timeoutMs]);\n\n const value = useMemo(\n () => ({ client: resolvedClient, cacheVersion, invalidate }),\n [cacheVersion, resolvedClient],\n );\n\n return <SubscriptionContext.Provider value={value}>{children}</SubscriptionContext.Provider>;\n}\n\nexport function useSubscriptionClient(): SubscriptionsClientContract {\n return useSubscriptionContext().client;\n}\n\nexport function useSubscriptionInvalidation(): () => void {\n return useSubscriptionContext().invalidate;\n}\n\nexport function useSubscriptionCacheVersion(): number {\n return useSubscriptionContext().cacheVersion;\n}\n\nexport function createSubscriptionFetcher(basePath: string): SubscriptionFetcher {\n const cleanBase = basePath.replace(/\\/+$/, '');\n return (path, init) => {\n const cleanPath = path.startsWith('/') ? path : `/${path}`;\n return fetch(`${cleanBase}${cleanPath}`, init);\n };\n}\n\nexport function useSubscriptionContext(): SubscriptionContextValue {\n const ctx = useContext(SubscriptionContext);\n if (!ctx) {\n throw new Error(\n '@usethrottle/subscriptions hooks must be used inside <SubscriptionProvider />',\n );\n }\n return ctx;\n}\n\nexport function SubscriptionProviderWithClient({\n children,\n client,\n}: {\n children: ReactNode;\n client: SubscriptionsClientContract;\n}) {\n const [cacheVersion, invalidate] = useReducer((value: number) => value + 1, 0);\n const value = useMemo(() => ({ client, cacheVersion, invalidate }), [cacheVersion, client]);\n return <SubscriptionContext.Provider value={value}>{children}</SubscriptionContext.Provider>;\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n useSubscriptionCacheVersion,\n useSubscriptionClient,\n useSubscriptionInvalidation,\n} from './provider';\nimport type {\n CancelSubscriptionInput,\n ChangePlanInput,\n CheckoutSession,\n CreateCheckoutSessionInput,\n CreateSubscriptionInput,\n Customer,\n CustomerPaymentMethod,\n ListSubscriptionsParams,\n MutationState,\n PaginatedSubscriptions,\n QueryOptions,\n QueryState,\n Subscription,\n TrialEligibilityInput,\n TrialEligibilityResult,\n UpdateSubscriptionInput,\n} from './types';\n\nexport function useSubscriptions(\n params: ListSubscriptionsParams = {},\n options: QueryOptions<PaginatedSubscriptions> = {},\n): QueryState<PaginatedSubscriptions> {\n const client = useSubscriptionClient();\n const cacheVersion = useSubscriptionCacheVersion();\n const stableParams = useStableValue(params);\n return useAsyncQuery(\n () => client.list(stableParams),\n [client, stableParams, cacheVersion],\n options,\n );\n}\n\nexport function useSubscription(\n id: string | null | undefined,\n options: QueryOptions<Subscription> = {},\n): QueryState<Subscription> {\n const client = useSubscriptionClient();\n const cacheVersion = useSubscriptionCacheVersion();\n return useAsyncQuery(\n () => (id ? client.get(id) : Promise.resolve(null)),\n [client, id, cacheVersion],\n { ...options, enabled: options.enabled ?? Boolean(id) },\n );\n}\n\nexport function useCustomerByExternalId(\n externalId: string | null | undefined,\n options: QueryOptions<Customer> = {},\n): QueryState<Customer> {\n const client = useSubscriptionClient();\n const cacheVersion = useSubscriptionCacheVersion();\n return useAsyncQuery(\n () => (externalId ? client.getCustomerByExternalId(externalId) : Promise.resolve(null)),\n [client, externalId, cacheVersion],\n { ...options, enabled: options.enabled ?? Boolean(externalId) },\n );\n}\n\nexport function useCustomerPaymentMethods(\n customerId: string | null | undefined,\n options: QueryOptions<CustomerPaymentMethod[]> = {},\n): QueryState<CustomerPaymentMethod[]> {\n const client = useSubscriptionClient();\n const cacheVersion = useSubscriptionCacheVersion();\n return useAsyncQuery(\n () => (customerId ? client.listCustomerPaymentMethods(customerId) : Promise.resolve(null)),\n [client, customerId, cacheVersion],\n { ...options, enabled: options.enabled ?? Boolean(customerId) },\n );\n}\n\nexport function useTrialEligibility(): MutationState<\n TrialEligibilityResult,\n TrialEligibilityInput\n> {\n const client = useSubscriptionClient();\n return useAsyncMutation((input) => client.checkTrialEligibility(input));\n}\n\nexport function useCreateSubscription(): MutationState<Subscription, CreateSubscriptionInput> {\n const client = useSubscriptionClient();\n const invalidate = useSubscriptionInvalidation();\n return useAsyncMutation((input) => client.create(input), invalidate);\n}\n\nexport function useUpdateSubscription(): MutationState<\n Subscription,\n { id: string; data: UpdateSubscriptionInput }\n> {\n const client = useSubscriptionClient();\n const invalidate = useSubscriptionInvalidation();\n return useAsyncMutation(({ id, data }) => client.update(id, data), invalidate);\n}\n\nexport function useChangePlan(): MutationState<Subscription, ChangePlanInput> {\n const client = useSubscriptionClient();\n const invalidate = useSubscriptionInvalidation();\n return useAsyncMutation((input) => client.changePlan(input), invalidate);\n}\n\nexport function useCancelSubscription(): MutationState<Subscription, CancelSubscriptionInput> {\n const client = useSubscriptionClient();\n const invalidate = useSubscriptionInvalidation();\n return useAsyncMutation(\n (input) => client.cancel(input.id, { atPeriodEnd: input.atPeriodEnd }),\n invalidate,\n );\n}\n\nexport function usePauseSubscription(): MutationState<Subscription, { id: string }> {\n const client = useSubscriptionClient();\n const invalidate = useSubscriptionInvalidation();\n return useAsyncMutation((input) => client.pause(input.id), invalidate);\n}\n\nexport function useResumeSubscription(): MutationState<Subscription, { id: string }> {\n const client = useSubscriptionClient();\n const invalidate = useSubscriptionInvalidation();\n return useAsyncMutation((input) => client.resume(input.id), invalidate);\n}\n\nexport function useCreateCheckoutSession(): MutationState<\n CheckoutSession,\n CreateCheckoutSessionInput\n> {\n const client = useSubscriptionClient();\n return useAsyncMutation((input) => client.createCheckoutSession(input));\n}\n\nfunction useAsyncQuery<T>(\n load: () => Promise<T | null>,\n deps: readonly unknown[],\n options: QueryOptions<T>,\n): QueryState<T> {\n const enabled = options.enabled ?? true;\n const [data, setData] = useState<T | null>(options.initialData ?? null);\n const [error, setError] = useState<unknown>(null);\n const [isLoading, setIsLoading] = useState(enabled && options.initialData === undefined);\n const [isFetching, setIsFetching] = useState(false);\n const loadRef = useRef(load);\n loadRef.current = load;\n\n const refetch = useCallback(async () => {\n if (!enabled) return data;\n setIsFetching(true);\n setError(null);\n try {\n const next = await loadRef.current();\n setData(next);\n return next;\n } catch (err) {\n setError(err);\n throw err;\n } finally {\n setIsFetching(false);\n setIsLoading(false);\n }\n }, [data, enabled]);\n\n useEffect(() => {\n if (!enabled) {\n setIsLoading(false);\n return;\n }\n let cancelled = false;\n setIsFetching(true);\n setError(null);\n load()\n .then((next) => {\n if (!cancelled) setData(next);\n })\n .catch((err) => {\n if (!cancelled) setError(err);\n })\n .finally(() => {\n if (!cancelled) {\n setIsLoading(false);\n setIsFetching(false);\n }\n });\n\n return () => {\n cancelled = true;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [enabled, ...deps]);\n\n return { data, error, isLoading, isFetching, refetch };\n}\n\nfunction useAsyncMutation<TData, TVariables>(\n action: (variables: TVariables) => Promise<TData>,\n onSuccess?: () => void,\n): MutationState<TData, TVariables> {\n const [data, setData] = useState<TData | null>(null);\n const [error, setError] = useState<unknown>(null);\n const [isPending, setIsPending] = useState(false);\n\n const mutateAsync = useCallback(\n async (variables: TVariables) => {\n setIsPending(true);\n setError(null);\n try {\n const next = await action(variables);\n setData(next);\n onSuccess?.();\n return next;\n } catch (err) {\n setError(err);\n throw err;\n } finally {\n setIsPending(false);\n }\n },\n [action, onSuccess],\n );\n\n const mutate = useCallback(\n (variables: TVariables) => {\n void mutateAsync(variables).catch(() => undefined);\n },\n [mutateAsync],\n );\n\n const reset = useCallback(() => {\n setData(null);\n setError(null);\n setIsPending(false);\n }, []);\n\n return { data, error, isPending, mutate, mutateAsync, reset };\n}\n\nfunction useStableValue<T>(value: T): T {\n const key = JSON.stringify(value);\n return useMemo(() => value, [key]);\n}\n","/**\n * A subscription's billing cadence: \"every N units\".\n *\n * Every subscription row stores a unit and a count; the five named intervals\n * are shorthand for particular combinations. This module is the only place\n * that knows the vocabulary, the bounds, and how a period boundary is\n * computed — the engine, the renewal cron, the API, the dashboard and the\n * buyer surfaces all import it. Before it existed there were three divergent\n * copies of the period math, and the renewal cron's advanced months with\n * `setMonth`, which overflows: a subscription started on Jan 31 renewed on\n * Mar 3 and never returned to the 31st.\n */\n\nexport const INTERVAL_UNITS = ['hour', 'day', 'week', 'month'] as const;\nexport type IntervalUnit = (typeof INTERVAL_UNITS)[number];\n\nexport const NAMED_INTERVALS = ['weekly', 'biweekly', 'monthly', 'quarterly', 'yearly'] as const;\nexport type NamedInterval = (typeof NAMED_INTERVALS)[number];\n\n/** What the `interval` field holds: a named interval, or `custom` for everything else. */\nexport const INTERVAL_VALUES = [...NAMED_INTERVALS, 'custom'] as const;\nexport type IntervalValue = (typeof INTERVAL_VALUES)[number];\n\nexport interface Cadence {\n interval: IntervalValue;\n unit: IntervalUnit;\n count: number;\n}\n\n/** Anything that describes a cadence the way the API and the database do. */\nexport interface CadenceLike {\n interval: string;\n intervalUnit?: string | null;\n intervalCount?: number | null;\n}\n\nexport interface CadenceInput {\n interval?: string | null;\n intervalUnit?: string | null;\n intervalCount?: number | null;\n}\n\nexport const NAMED_SHORTHAND: Readonly<Record<NamedInterval, { unit: IntervalUnit; count: number }>> = {\n weekly: { unit: 'week', count: 1 },\n biweekly: { unit: 'week', count: 2 },\n monthly: { unit: 'month', count: 1 },\n quarterly: { unit: 'month', count: 3 },\n // month × 12 rather than a year unit, so a Feb 29 anniversary clamps to Feb\n // 28 and comes back to Feb 29 in leap years like every other month cadence.\n yearly: { unit: 'month', count: 12 },\n};\n\n/** At most one year in every unit. The shortest cadence is 1 hour. */\nexport const MAX_INTERVAL_COUNT: Readonly<Record<IntervalUnit, number>> = {\n hour: 8760,\n day: 365,\n week: 52,\n month: 12,\n};\n\nexport const HOUR_MS = 3_600_000;\nexport const DAY_MS = 24 * HOUR_MS;\nexport const WEEK_MS = 7 * DAY_MS;\n\nconst FIXED_UNIT_MS = { hour: HOUR_MS, day: DAY_MS, week: WEEK_MS } as const;\n\nexport class CadenceError extends Error {\n readonly statusCode = 400;\n constructor(\n readonly code: 'invalid_interval' | 'interval_conflict',\n message: string,\n ) {\n super(message);\n this.name = 'CadenceError';\n }\n}\n\nexport function isNamedInterval(value: unknown): value is NamedInterval {\n return typeof value === 'string' && (NAMED_INTERVALS as readonly string[]).includes(value);\n}\n\nexport function isIntervalUnit(value: unknown): value is IntervalUnit {\n return typeof value === 'string' && (INTERVAL_UNITS as readonly string[]).includes(value);\n}\n\nfunction isIntervalValue(value: unknown): value is IntervalValue {\n return typeof value === 'string' && (INTERVAL_VALUES as readonly string[]).includes(value);\n}\n\nfunction reduceToLargestUnit(unit: IntervalUnit, count: number): { unit: IntervalUnit; count: number } {\n let u = unit;\n let c = count;\n if (u === 'hour' && c % 24 === 0) {\n u = 'day';\n c = c / 24;\n }\n if (u === 'day' && c % 7 === 0) {\n u = 'week';\n c = c / 7;\n }\n return { unit: u, count: c };\n}\n\nfunction namedFor(unit: IntervalUnit, count: number): NamedInterval | null {\n for (const name of NAMED_INTERVALS) {\n const s = NAMED_SHORTHAND[name];\n if (s.unit === unit && s.count === count) return name;\n }\n return null;\n}\n\n/**\n * Turn whatever a caller sent into the one stored form. Accepts a named\n * interval alone, a unit and count alone, or both — in which case they must\n * agree. The result is reduced to its largest exact unit and mapped to a named\n * interval whenever one matches, so there is exactly one representation per\n * cadence and \"every 2 weeks\" can never be filed apart from \"biweekly\".\n */\nexport function canonicalizeCadence(input: CadenceInput): Cadence {\n const interval = input.interval ?? undefined;\n if (interval !== undefined && !isIntervalValue(interval)) {\n throw new CadenceError('invalid_interval', `Unknown interval \"${interval}\"`);\n }\n const hasUnit = input.intervalUnit != null;\n const hasCount = input.intervalCount != null;\n if (hasUnit !== hasCount) {\n throw new CadenceError('invalid_interval', 'intervalUnit and intervalCount must be sent together');\n }\n if (!hasUnit) {\n if (isNamedInterval(interval)) return { interval, ...NAMED_SHORTHAND[interval] };\n throw new CadenceError(\n 'invalid_interval',\n interval === 'custom'\n ? 'A custom interval needs intervalUnit and intervalCount'\n : 'Send an interval, or intervalUnit and intervalCount',\n );\n }\n\n const unit = input.intervalUnit;\n const count = input.intervalCount as number;\n if (!isIntervalUnit(unit)) {\n throw new CadenceError('invalid_interval', 'intervalUnit must be one of hour, day, week, month');\n }\n if (!Number.isInteger(count) || count < 1) {\n throw new CadenceError('invalid_interval', 'intervalCount must be a whole number of at least 1');\n }\n if (count > MAX_INTERVAL_COUNT[unit]) {\n throw new CadenceError(\n 'invalid_interval',\n `A cadence can be at most one year: ${MAX_INTERVAL_COUNT[unit]} ${unit}s`,\n );\n }\n\n const reduced = reduceToLargestUnit(unit, count);\n const cadence: Cadence = { interval: namedFor(reduced.unit, reduced.count) ?? 'custom', ...reduced };\n if (interval !== undefined && interval !== 'custom' && interval !== cadence.interval) {\n throw new CadenceError(\n 'interval_conflict',\n `interval \"${interval}\" does not match every ${count} ${unit}${count === 1 ? '' : 's'}`,\n );\n }\n return cadence;\n}\n\n/**\n * Read the cadence off a stored subscription. A named interval always means\n * its shorthand, whatever the unit/count columns hold: a canonical row's\n * columns match the shorthand anyway, and code that predates the columns\n * (still serving between migration 0224 and the deploy) changes `interval`\n * without touching them, so for a named row they can be stale. Only a\n * `custom` row is read from its unit and count. The same rule applies to the\n * pending cadence — pass `{ interval: pendingInterval, intervalUnit:\n * pendingIntervalUnit, intervalCount: pendingIntervalCount }`.\n */\nexport function cadenceOf(row: CadenceLike): Cadence {\n if (isNamedInterval(row.interval)) return { interval: row.interval, ...NAMED_SHORTHAND[row.interval] };\n if (row.interval === 'custom' && row.intervalUnit != null && row.intervalCount != null && isIntervalUnit(row.intervalUnit)) {\n return { interval: 'custom', unit: row.intervalUnit, count: row.intervalCount };\n }\n throw new Error(`Unknown subscription interval: ${row.interval}`);\n}\n\n/** The exact period length for hour/day/week cadences; null for months, which vary. */\nexport function fixedPeriodMs(cadence: Cadence): number | null {\n return cadence.unit === 'month' ? null : cadence.count * FIXED_UNIT_MS[cadence.unit];\n}\n\n/** The shortest a period can be. A month counts as 28 days. */\nexport function minimumPeriodMs(cadence: Cadence): number {\n return fixedPeriodMs(cadence) ?? cadence.count * 28 * DAY_MS;\n}\n\n/** anchor + months, keeping the anchor's day-of-month (clamped to month end) and UTC time of day. */\nexport function addMonthsAnchored(anchor: Date, months: number): Date {\n const monthIndex = anchor.getUTCMonth() + months;\n const year = anchor.getUTCFullYear() + Math.floor(monthIndex / 12);\n const month = ((monthIndex % 12) + 12) % 12;\n const lastDay = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();\n return new Date(\n Date.UTC(\n year,\n month,\n Math.min(anchor.getUTCDate(), lastDay),\n anchor.getUTCHours(),\n anchor.getUTCMinutes(),\n anchor.getUTCSeconds(),\n anchor.getUTCMilliseconds(),\n ),\n );\n}\n\n/**\n * The first period boundary strictly after `after`, on the grid measured from\n * `anchor`. Never chained from a previous boundary — that is what made the\n * month-end clamp stick on the 28th. An `after` that is off the grid (a late\n * tick, a recovery) resolves to the next grid date.\n */\nexport function nextPeriodEnd(args: { anchor: Date; cadence: Cadence; after: Date }): Date {\n const { anchor, cadence, after } = args;\n const step = fixedPeriodMs(cadence);\n if (step !== null) {\n const k = Math.max(1, Math.floor((after.getTime() - anchor.getTime()) / step) + 1);\n return new Date(anchor.getTime() + k * step);\n }\n const monthsElapsed =\n (after.getUTCFullYear() - anchor.getUTCFullYear()) * 12 + (after.getUTCMonth() - anchor.getUTCMonth());\n let k = Math.max(1, Math.floor(monthsElapsed / cadence.count));\n let boundary = addMonthsAnchored(anchor, k * cadence.count);\n while (boundary.getTime() <= after.getTime()) {\n k += 1;\n boundary = addMonthsAnchored(anchor, k * cadence.count);\n }\n return boundary;\n}\n\n/**\n * The grid period that contains `at`: the latest boundary at or before it (never\n * before the anchor) and the one after. A subscription that fell more than a\n * period behind is billed once, for this period, and the periods it missed are\n * skipped rather than charged back to back (D38).\n */\nexport function periodContaining(args: { anchor: Date; cadence: Cadence; at: Date }): { start: Date; end: Date } {\n const { anchor, cadence, at } = args;\n const step = fixedPeriodMs(cadence);\n if (step !== null) {\n const k = Math.max(0, Math.floor((at.getTime() - anchor.getTime()) / step));\n const start = new Date(anchor.getTime() + k * step);\n return { start, end: new Date(start.getTime() + step) };\n }\n const monthsElapsed =\n (at.getUTCFullYear() - anchor.getUTCFullYear()) * 12 + (at.getUTCMonth() - anchor.getUTCMonth());\n let k = Math.max(0, Math.floor(monthsElapsed / cadence.count));\n let start = addMonthsAnchored(anchor, k * cadence.count);\n while (k > 0 && start.getTime() > at.getTime()) {\n k -= 1;\n start = addMonthsAnchored(anchor, k * cadence.count);\n }\n return { start, end: addMonthsAnchored(anchor, (k + 1) * cadence.count) };\n}\n\nconst RETRY_CAP_DAYS = [1, 3, 7] as const;\n\n/**\n * How long to wait before retry number `failureNumber` (1..3). Short cadences\n * retry proportionally faster; anything of a week or longer lands on the\n * original 1 / 3 / 7 days exactly.\n */\nexport function dunningRetryDelayMs(cadence: Cadence, failureNumber: number): number {\n const n = Math.min(Math.max(Math.trunc(failureNumber), 1), RETRY_CAP_DAYS.length);\n return Math.min(n * minimumPeriodMs(cadence), RETRY_CAP_DAYS[n - 1] * DAY_MS);\n}\n\n/** The 3-days-ahead renewal reminder only makes sense for cadences of a week or longer. */\nexport function sendsRenewalReminder(cadence: Cadence): boolean {\n return minimumPeriodMs(cadence) >= WEEK_MS;\n}\n\n/**\n * Multiply a period's charge by this to get its monthly equivalent. Chosen so\n * `weekly` is exactly 52/12 and `biweekly` exactly 26/12, the factors\n * analytics already report — no existing MRR number moves.\n */\nexport function monthlyEquivalentFactor(cadence: Cadence): number {\n if (cadence.unit === 'month') return 1 / cadence.count;\n return (52 / 12) * (WEEK_MS / (fixedPeriodMs(cadence) as number));\n}\n","/**\n * Installment plans: a subscription with a fixed number of payments that ends\n * itself — status `completed` — the moment the last one succeeds.\n */\n\nexport const MIN_TOTAL_PAYMENTS = 2;\nexport const MAX_TOTAL_PAYMENTS = 60;\n\nexport type InstallmentErrorCode =\n | 'invalid_total_payments'\n | 'invalid_combination'\n | 'total_payments_immutable'\n | 'installment_plan_locked'\n | 'installment_plan_merchant_only';\n\nexport class InstallmentError extends Error {\n constructor(\n readonly code: InstallmentErrorCode,\n message: string,\n readonly statusCode: number,\n ) {\n super(message);\n this.name = 'InstallmentError';\n }\n}\n\nexport function validateTotalPayments(value: unknown): number | null {\n if (value === undefined || value === null) return null;\n if (\n typeof value !== 'number' ||\n !Number.isInteger(value) ||\n value < MIN_TOTAL_PAYMENTS ||\n value > MAX_TOTAL_PAYMENTS\n ) {\n throw new InstallmentError(\n 'invalid_total_payments',\n `totalPayments must be a whole number from ${MIN_TOTAL_PAYMENTS} to ${MAX_TOTAL_PAYMENTS}`,\n 400,\n );\n }\n return value;\n}\n\nexport function isInstallmentPlan(sub: { totalPayments?: number | null }): boolean {\n return sub.totalPayments != null;\n}\n\n/** After one counted payment — a charge or a waive — the new count and whether the plan is paid in full. */\nexport function countInstallmentPayment(sub: {\n totalPayments?: number | null;\n paymentsMade?: number | null;\n}): { paymentsMade: number; completes: boolean } | null {\n if (sub.totalPayments == null) return null;\n const paymentsMade = (sub.paymentsMade ?? 0) + 1;\n return { paymentsMade, completes: paymentsMade >= sub.totalPayments };\n}\n\nexport interface InstallmentProgress {\n paymentsMade: number;\n totalPayments: number;\n remainingPayments: number;\n remainingCents: number;\n}\n\nexport function installmentProgress(sub: {\n totalPayments?: number | null;\n paymentsMade?: number | null;\n amount: number;\n quantity?: number | null;\n scheduleRows?: Array<{ unitAmount: number; discount: number }> | null;\n}): InstallmentProgress | null {\n if (sub.totalPayments == null) return null;\n const paymentsMade = Math.min(sub.paymentsMade ?? 0, sub.totalPayments);\n const remainingPayments = sub.totalPayments - paymentsMade;\n const quantity = sub.quantity ?? 1;\n const remainingCents =\n sub.scheduleRows && sub.scheduleRows.length > 0\n ? sub.scheduleRows.slice(paymentsMade).reduce((s, r) => s + r.unitAmount * quantity - r.discount, 0)\n : remainingPayments * sub.amount * quantity;\n return { paymentsMade, totalPayments: sub.totalPayments, remainingPayments, remainingCents };\n}\n\nexport function installmentLocked(): InstallmentError {\n return new InstallmentError(\n 'installment_plan_locked',\n \"An installment plan's amount, cadence, quantity and plan are fixed when it is created. Cancel it and create a new one to change the terms.\",\n 409,\n );\n}\n\nexport function merchantOnly(): InstallmentError {\n return new InstallmentError(\n 'installment_plan_merchant_only',\n 'Installment plans can only be paused, resumed or cancelled by the merchant',\n 403,\n );\n}\n\nexport function totalPaymentsImmutable(): InstallmentError {\n return new InstallmentError(\n 'total_payments_immutable',\n 'totalPayments is set when a subscription is created and cannot be changed',\n 400,\n );\n}\n","import { isIntervalUnit, isNamedInterval, type CadenceLike, type NamedInterval } from './cadence';\nimport { isVariedSchedule, scheduleUnitTotal } from './schedule';\n\nconst NAMED_NOUN: Record<NamedInterval, string> = {\n weekly: 'week',\n biweekly: '2 weeks',\n monthly: 'month',\n quarterly: 'quarter',\n yearly: 'year',\n};\n\nconst NAMED_LABEL: Record<NamedInterval, string> = {\n weekly: 'Weekly',\n biweekly: 'Biweekly',\n monthly: 'Monthly',\n quarterly: 'Quarterly',\n yearly: 'Yearly',\n};\n\n/**\n * The period as a noun — what reads correctly after a slash: \"$49.00 / 10\n * days\". This replaces the per-file `INTERVAL_WORD` maps whose drift shipped\n * \"$49.00 / biweekly\" once already. Unknown input falls through unchanged so a\n * value this build doesn't know still renders instead of vanishing.\n */\nexport function cadenceNoun(cadence: CadenceLike): string {\n if (isNamedInterval(cadence.interval)) return NAMED_NOUN[cadence.interval];\n if (isIntervalUnit(cadence.intervalUnit) && cadence.intervalCount != null) {\n return cadence.intervalCount === 1 ? cadence.intervalUnit : `${cadence.intervalCount} ${cadence.intervalUnit}s`;\n }\n return cadence.interval;\n}\n\n/** The cadence as a standalone label, e.g. an Interval column: \"Monthly\", \"Every 10 days\". */\nexport function cadenceLabel(cadence: CadenceLike): string {\n if (isNamedInterval(cadence.interval)) return NAMED_LABEL[cadence.interval];\n if (isIntervalUnit(cadence.intervalUnit) && cadence.intervalCount != null) return `Every ${cadenceNoun(cadence)}`;\n return cadence.interval;\n}\n\n/** Sub-daily cadences show the time of the next charge, not just its date. */\nexport function isSubDaily(cadence: CadenceLike): boolean {\n return cadence.intervalUnit === 'hour';\n}\n\n/** A subscription line's terms, the fields a label needs. */\nexport interface RecurringTermsLike extends CadenceLike {\n totalPayments?: number | null;\n trialDays?: number | null;\n schedule?: Array<{ amount: number }> | null;\n}\n\n/**\n * One short label for a subscription line's terms, for carts and quotes:\n * \"$20.00 / month\", \"$20.00 / 10 days · 3 payments\", \"Free for 14 days, then\n * $20.00 / month\", \"$100.00 total · 4 payments\". The label computes the money\n * itself from the line's unit price and seats — for a schedule, the deal total\n * (every payment × seats), never the line's unit price (payment 1) — and hands\n * it to `money`, so each surface keeps its own money style.\n */\nexport function recurringTermsLabel(\n terms: RecurringTermsLike,\n price: { unitPrice: number; quantity?: number | null },\n money: (minor: number) => string,\n): string {\n const quantity = price.quantity ?? 1;\n if (terms.schedule && terms.schedule.length > 0) {\n return `${money(scheduleUnitTotal(terms.schedule) * quantity)} total · ${terms.schedule.length} payments`;\n }\n const each = money(price.unitPrice * quantity);\n const noun = cadenceNoun(terms);\n const trialDays = terms.trialDays ?? 0;\n if (trialDays > 0) return `Free for ${trialDays} day${trialDays === 1 ? '' : 's'}, then ${each} / ${noun}`;\n return `${each} / ${noun}` + (terms.totalPayments != null ? ` · ${terms.totalPayments} payments` : '');\n}\n\n/** A subscription's public schedule fields, as the API returns them. */\nexport interface ScheduleDealLike {\n paymentSchedule?: Array<{ amount: number; discount: number; intervalUnit: string | null; intervalCount: number | null }> | null;\n scheduleTotal?: number | null;\n}\n\n/**\n * A varied schedule's deal — its total (every row's net: seats in, discounts\n * out) and its payment count — or null for an equal plan, whose synthesized\n * schedule keeps the per-period label, and for an ongoing plan. A varied\n * schedule has no per-period price: `amount` and the cadence columns hold only\n * the next row's (ruling R-FR4). Every surface gates both its price and its\n * cadence cells on this one answer.\n */\nexport function scheduleDeal(sub: ScheduleDealLike): { total: number; payments: number } | null {\n if (!sub.paymentSchedule || sub.scheduleTotal == null || !isVariedSchedule(sub.paymentSchedule)) return null;\n return { total: sub.scheduleTotal, payments: sub.paymentSchedule.length };\n}\n\n/** \"$100.00 total · 4 payments\" for a varied schedule, else null. `money` keeps each surface's money style. */\nexport function scheduleDealLabel(sub: ScheduleDealLike, money: (minor: number) => string): string | null {\n const deal = scheduleDeal(sub);\n return deal ? `${money(deal.total)} total · ${deal.payments} payments` : null;\n}\n","import {\n cadenceNoun as coreCadenceNoun,\n cadenceLabel as coreCadenceLabel,\n installmentProgress as coreInstallmentProgress,\n} from '@core/cadence';\nimport type { Subscription, SubscriptionPaymentScheduleRow, SubscriptionStatus } from './types';\n\n/** `cancelled` and `completed` (an installment plan paid in full) never bill again. */\nexport function isTerminalSubscription(status: SubscriptionStatus | Subscription): boolean {\n const s = getStatus(status);\n return s === 'cancelled' || s === 'completed';\n}\n\nexport function canPauseSubscription(status: SubscriptionStatus | Subscription): boolean {\n return getStatus(status) === 'active';\n}\n\nexport function canResumeSubscription(status: SubscriptionStatus | Subscription): boolean {\n return getStatus(status) === 'paused';\n}\n\nexport function canCancelSubscription(status: SubscriptionStatus | Subscription): boolean {\n return !isTerminalSubscription(status);\n}\n\nexport function isSubscriptionPastDue(status: SubscriptionStatus | Subscription): boolean {\n return getStatus(status) === 'past_due';\n}\n\nexport function isSubscriptionTrialing(status: SubscriptionStatus | Subscription): boolean {\n return getStatus(status) === 'trialing';\n}\n\nexport function daysUntil(date: string | Date, now = new Date()): number {\n const end = date instanceof Date ? date : new Date(date);\n const ms = end.getTime() - now.getTime();\n if (!Number.isFinite(ms)) return 0;\n return Math.max(0, Math.ceil(ms / 86_400_000));\n}\n\nfunction getStatus(input: SubscriptionStatus | Subscription): SubscriptionStatus {\n return typeof input === 'string' ? input : input.status;\n}\n\n/**\n * Anything that describes a cadence the way the API and the database do.\n * Structurally mirrors the private cadence engine's own shape — kept local\n * so the published `.d.ts` never names that unpublished package.\n */\nexport interface CadenceLike {\n interval: string;\n intervalUnit?: string | null;\n intervalCount?: number | null;\n}\n\nexport interface InstallmentProgress {\n paymentsMade: number;\n totalPayments: number;\n remainingPayments: number;\n remainingCents: number;\n}\n\n/** \"10 days\", \"2 weeks\", \"month\" — the period after a slash: `$49.00 / ${cadenceNoun(sub)}`. */\nexport function cadenceNoun(cadence: CadenceLike): string {\n return coreCadenceNoun(cadence);\n}\n\n/** The cadence as a standalone label, e.g. an Interval column: \"Monthly\", \"Every 10 days\". */\nexport function cadenceLabel(cadence: CadenceLike): string {\n return coreCadenceLabel(cadence);\n}\n\n/**\n * Progress of an installment plan. Pass the subscription itself: when it carries\n * `paymentSchedule` (a custom schedule), `remainingCents` is the sum of the\n * not-yet-paid rows' `net` (already net of discount, quantity applied, before\n * tax). Otherwise it is `remainingPayments × amount × quantity`.\n */\nexport function installmentProgress(sub: {\n totalPayments?: number | null;\n paymentsMade?: number | null;\n amount: number;\n quantity?: number | null;\n paymentSchedule?: Array<Pick<SubscriptionPaymentScheduleRow, 'net' | 'status'>> | null;\n}): InstallmentProgress | null {\n const progress = coreInstallmentProgress(sub);\n if (!progress || !sub.paymentSchedule || sub.paymentSchedule.length === 0) return progress;\n const remainingCents = sub.paymentSchedule.filter((r) => r.status !== 'paid').reduce((sum, r) => sum + r.net, 0);\n return { ...progress, remainingCents };\n}\n","import type { CSSProperties } from 'react';\nimport { daysUntil } from './helpers';\nimport type {\n DunningBannerProps,\n SubscriptionStatus,\n SubscriptionStatusBadgeProps,\n TrialCountdownProps,\n} from './types';\n\nconst STATUS_LABELS: Record<SubscriptionStatus, string> = {\n active: 'Active',\n paused: 'Paused',\n cancelled: 'Cancelled',\n past_due: 'Past due',\n trialing: 'Trialing',\n unpaid: 'Unpaid',\n completed: 'Paid in full',\n};\n\nconst STATUS_STYLES: Record<SubscriptionStatus, CSSProperties> = {\n active: { color: '#035c4f', background: '#dff8ef', borderColor: '#a8e8d2' },\n paused: { color: '#735c00', background: '#fff4ca', borderColor: '#f3d46b' },\n cancelled: { color: '#6c2632', background: '#ffe1e7', borderColor: '#f4a9b8' },\n past_due: { color: '#7a2d00', background: '#ffe8d6', borderColor: '#f5b47f' },\n trialing: { color: '#124c73', background: '#dff1ff', borderColor: '#9ed3fb' },\n unpaid: { color: '#7a2d00', background: '#ffe8d6', borderColor: '#f5b47f' },\n completed: { color: '#035c4f', background: '#dff8ef', borderColor: '#a8e8d2' },\n};\n\nexport function SubscriptionStatusBadge({\n status,\n className,\n style,\n labels,\n}: SubscriptionStatusBadgeProps) {\n return (\n <span\n className={className}\n style={{\n display: 'inline-flex',\n alignItems: 'center',\n border: '1px solid',\n borderRadius: 999,\n padding: '2px 8px',\n fontSize: 12,\n fontWeight: 700,\n lineHeight: 1.6,\n whiteSpace: 'nowrap',\n ...STATUS_STYLES[status],\n ...style,\n }}\n >\n {labels?.[status] ?? STATUS_LABELS[status]}\n </span>\n );\n}\n\nexport function TrialCountdown({\n endDate,\n now,\n className,\n style,\n expiredLabel = 'Trial ended',\n prefix = 'Trial ends in',\n}: TrialCountdownProps) {\n const days = daysUntil(endDate, now);\n const label = days <= 0 ? expiredLabel : `${prefix} ${days} ${days === 1 ? 'day' : 'days'}`;\n\n return (\n <span className={className} style={style}>\n {label}\n </span>\n );\n}\n\nexport function DunningBanner({\n subscription,\n onUpdateCard,\n title = 'Payment needs attention',\n message,\n actionLabel = 'Update card',\n className,\n style,\n}: DunningBannerProps) {\n if (subscription.status !== 'past_due') return null;\n const fallbackMessage =\n subscription.failureCount > 0\n ? `We could not renew this subscription after ${subscription.failureCount} failed payment attempt${\n subscription.failureCount === 1 ? '' : 's'\n }.`\n : 'We could not renew this subscription with the saved payment method.';\n\n return (\n <div\n role=\"status\"\n className={className}\n style={{\n border: '1px solid #f5b47f',\n borderRadius: 8,\n background: '#fff7ed',\n color: '#7a2d00',\n padding: 16,\n display: 'grid',\n gap: 10,\n ...style,\n }}\n >\n <strong>{title}</strong>\n <span>{message ?? fallbackMessage}</span>\n {onUpdateCard ? (\n <button\n type=\"button\"\n onClick={onUpdateCard}\n style={{\n width: 'fit-content',\n border: '1px solid #7a2d00',\n borderRadius: 6,\n background: '#7a2d00',\n color: '#fff',\n padding: '8px 12px',\n fontWeight: 700,\n cursor: 'pointer',\n }}\n >\n {actionLabel}\n </button>\n ) : null}\n </div>\n );\n}\n","export { SubscriptionsClient, createSubscriptionsClient } from './client';\nexport { ThrottleSubscriptionsError, ThrottleError, isThrottleError } from './errors';\nexport {\n SubscriptionProvider,\n SubscriptionProviderWithClient,\n createSubscriptionFetcher,\n useSubscriptionClient,\n useSubscriptionInvalidation,\n} from './provider';\nexport {\n useCancelSubscription,\n useChangePlan,\n useCreateCheckoutSession,\n useCreateSubscription,\n useCustomerByExternalId,\n useCustomerPaymentMethods,\n usePauseSubscription,\n useResumeSubscription,\n useSubscription,\n useSubscriptions,\n useTrialEligibility,\n useUpdateSubscription,\n} from './hooks';\nexport { DunningBanner, SubscriptionStatusBadge, TrialCountdown } from './components';\nexport {\n cadenceLabel,\n cadenceNoun,\n canCancelSubscription,\n canPauseSubscription,\n canResumeSubscription,\n daysUntil,\n installmentProgress,\n isSubscriptionPastDue,\n isSubscriptionTrialing,\n isTerminalSubscription,\n type CadenceLike,\n type InstallmentProgress,\n} from './helpers';\nexport type * from './types';\n// Money helpers (MC2-R34): amounts are integer minor units of their `currency`.\nexport * as money from '@usethrottle/money';\nexport {\n formatMoney,\n currencySymbol,\n minorUnitsOf,\n toDecimalString,\n toMinor,\n parseMoney,\n moneyValue,\n CURRENCIES,\n isKnownCurrency,\n UnknownCurrencyError,\n} from '@usethrottle/money';\nexport type { CurrencyCode, MoneyValue } from '@usethrottle/money';\n"],"mappings":"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","names":["value","useCallback","useMemo","useCallback","useMemo","cadenceNoun","cadenceLabel","installmentProgress","jsx"]}
package/dist/server.cjs CHANGED
@@ -59,7 +59,8 @@ function normalizeSubscription(raw) {
59
59
  nextPaymentAmount: raw?.nextPaymentAmount == null ? null : Number(raw.nextPaymentAmount),
60
60
  amount: Number(raw?.amount ?? 0),
61
61
  quantity: Number(raw?.quantity ?? 1),
62
- currency: optionalString(raw?.currency) ?? "USD",
62
+ // No USD fallback (MC2-R35): a subscription without a currency is a broken response.
63
+ currency: requiredString(raw?.currency, "subscription.currency"),
63
64
  currentPeriodStart: requiredDateString(
64
65
  raw?.currentPeriodStart,
65
66
  "subscription.currentPeriodStart"
@@ -76,7 +77,10 @@ function normalizeSubscription(raw) {
76
77
  cancelledAt: optionalDateString(raw?.cancelledAt),
77
78
  metadata: objectOrEmpty(raw?.metadata),
78
79
  paymentMethodId: optionalString(raw?.paymentMethodId),
79
- renewalIssue: normalizeRenewalIssue(raw?.renewalIssue),
80
+ deliveryMethod: raw?.deliveryMethod === "shipment" ? "shipment" : "access_grant",
81
+ shippingAddress: raw?.shippingAddress && typeof raw.shippingAddress === "object" ? raw.shippingAddress : null,
82
+ // `currency` above has already been checked.
83
+ renewalIssue: normalizeRenewalIssue(raw?.renewalIssue, raw.currency),
80
84
  proration: raw?.proration ? {
81
85
  creditCents: raw.proration.creditCents,
82
86
  chargedCents: raw.proration.chargedCents,
@@ -99,7 +103,7 @@ function normalizePaymentSchedule(raw) {
99
103
  dueAt: optionalDateString(r?.dueAt)
100
104
  }));
101
105
  }
102
- function normalizeRenewalIssue(raw) {
106
+ function normalizeRenewalIssue(raw, subscriptionCurrency) {
103
107
  if (!raw || typeof raw !== "object") return null;
104
108
  return {
105
109
  kind: raw.kind,
@@ -112,7 +116,7 @@ function normalizeRenewalIssue(raw) {
112
116
  charge: raw.charge ? {
113
117
  transactionId: optionalString(raw.charge.transactionId),
114
118
  amount: Number(raw.charge.amount ?? 0),
115
- currency: optionalString(raw.charge.currency) ?? "USD",
119
+ currency: optionalString(raw.charge.currency) || subscriptionCurrency,
116
120
  periodStart: optionalString(raw.charge.periodStart) ?? void 0
117
121
  } : null
118
122
  };
@@ -542,6 +546,13 @@ function setQuery(query, key, value) {
542
546
  }
543
547
 
544
548
  // src/server.ts
549
+ var BUYER_BODY_FIELDS = {
550
+ pause: [],
551
+ resume: [],
552
+ cancel: ["atPeriodEnd"],
553
+ "payment-method": ["paymentMethodId"],
554
+ update: ["shippingAddress"]
555
+ };
545
556
  var ALLOWED_PATHS = [
546
557
  /^\/api\/v1\/subscriptions(?:\/[\w.-]+)?(?:\/(?:pause|resume|cancel|payment-method))?$/,
547
558
  /^\/api\/v1\/subscriptions\/eligibility-check$/,
@@ -571,6 +582,32 @@ async function handleSubscriptionProxyRequest(request, pathParts, options) {
571
582
  baseUrl: options.baseUrl,
572
583
  fetch: options.fetch
573
584
  });
585
+ const raw = method === "GET" ? "" : await request.text();
586
+ let body = {};
587
+ if (raw) {
588
+ try {
589
+ const parsed = JSON.parse(raw);
590
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("not an object");
591
+ body = parsed;
592
+ } catch {
593
+ return jsonError(400, "invalid_body", "The request body must be a JSON object");
594
+ }
595
+ }
596
+ if (method === "POST" && path === "/api/v1/subscriptions") {
597
+ const allowed = options.authorizeCreate ? await options.authorizeCreate({ request, body, externalCustomerId }) : false;
598
+ if (!allowed) {
599
+ return jsonError(403, "create_not_allowed", "Creating a subscription from the buyer portal is not allowed");
600
+ }
601
+ }
602
+ if (method === "GET" && subscriptionIdFromPath(path) && path !== "/api/v1/subscriptions/eligibility-check") {
603
+ const subscription = await client.get(subscriptionIdFromPath(path)).catch((err) => {
604
+ if (err instanceof ThrottleSubscriptionsError && err.statusCode === 404) return null;
605
+ throw err;
606
+ });
607
+ if (!subscription || !await defaultMutationAuthorization(client, subscription, externalCustomerId)) {
608
+ return jsonError(404, "not_found", "Subscription not found");
609
+ }
610
+ }
574
611
  if (isSubscriptionMutation(path, method)) {
575
612
  const subId = subscriptionIdFromPath(path);
576
613
  if (subId) {
@@ -585,16 +622,28 @@ async function handleSubscriptionProxyRequest(request, pathParts, options) {
585
622
  if (!await options.authorizeMutation({ request, subscription, externalCustomerId })) {
586
623
  return jsonError(403, "forbidden", "Subscription does not belong to this buyer");
587
624
  }
588
- } else if (subscription.totalPayments != null && !isPaymentMethodPath(path)) {
625
+ } else if (subscription.totalPayments != null && !isPaymentMethodPath(path) && !isShippingAddressOnly(path, method, body)) {
589
626
  return jsonError(
590
627
  403,
591
628
  "installment_plan_merchant_only",
592
629
  "Installment plans can only be paused, resumed or cancelled by the merchant"
593
630
  );
594
631
  }
632
+ const action = mutationAction(path, method);
633
+ const extra = Object.keys(body).filter((key) => !(BUYER_BODY_FIELDS[action] ?? []).includes(key));
634
+ if (extra.length > 0) {
635
+ const merchantSays = action === "update" && options.authorizeUpdate ? await options.authorizeUpdate({ request, subscription, body, externalCustomerId }) : false;
636
+ if (!merchantSays) {
637
+ return jsonError(
638
+ 403,
639
+ "field_not_allowed",
640
+ `The buyer portal cannot change ${extra.join(", ")} on a subscription`
641
+ );
642
+ }
643
+ }
595
644
  }
596
645
  }
597
- const targetPath = await buildPinnedPathAndBody(request, path, externalCustomerId, client);
646
+ const targetPath = await buildPinnedPathAndBody(request, path, externalCustomerId, client, raw);
598
647
  if (targetPath instanceof Response) return targetPath;
599
648
  const upstream = await fetchWithApiKey(request, targetPath.path, {
600
649
  apiKey: options.apiKey,
@@ -607,7 +656,7 @@ async function handleSubscriptionProxyRequest(request, pathParts, options) {
607
656
  headers: filterHeaders(upstream.headers)
608
657
  });
609
658
  }
610
- async function buildPinnedPathAndBody(request, path, externalCustomerId, client) {
659
+ async function buildPinnedPathAndBody(request, path, externalCustomerId, client, raw) {
611
660
  const url = new URL(request.url);
612
661
  const query = new URLSearchParams(url.search);
613
662
  if (request.method.toUpperCase() === "GET" && path === "/api/v1/subscriptions") {
@@ -633,7 +682,6 @@ async function buildPinnedPathAndBody(request, path, externalCustomerId, client)
633
682
  };
634
683
  }
635
684
  if (request.method.toUpperCase() !== "GET") {
636
- const raw = await request.text();
637
685
  if (path === "/api/v1/subscriptions" && request.method.toUpperCase() === "POST") {
638
686
  const customer = await client.getCustomerByExternalId(externalCustomerId);
639
687
  if (!customer) return jsonError(404, "not_found", "Customer not found");
@@ -688,6 +736,14 @@ function withQuery(path, query) {
688
736
  const qs = query.toString();
689
737
  return qs ? `${path}?${qs}` : path;
690
738
  }
739
+ function mutationAction(path, method) {
740
+ const suffix = /^\/api\/v1\/subscriptions\/[\w.-]+\/([\w-]+)$/.exec(path)?.[1];
741
+ if (suffix) return suffix;
742
+ return method === "PATCH" ? "update" : "unknown";
743
+ }
744
+ function isShippingAddressOnly(path, method, body) {
745
+ return mutationAction(path, method) === "update" && Object.keys(body).length > 0 && Object.keys(body).every((key) => key === "shippingAddress");
746
+ }
691
747
  function isSubscriptionMutation(path, method) {
692
748
  if (path === "/api/v1/subscriptions/eligibility-check") return false;
693
749
  return method !== "GET" && /^\/api\/v1\/subscriptions\/[\w.-]+/.test(path);