@usethrottle/quotes 0.4.1 → 0.5.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/README.md CHANGED
@@ -206,6 +206,24 @@ A quote carries one of three terms, on `currentRevision.paymentTerms`:
206
206
  when the buyer edits quantities. Selections that would drop the total to or
207
207
  below the deposit are rejected with `invalid_item_selection`.
208
208
 
209
+ ## Subscription lines
210
+
211
+ A quote line may carry `recurring` (the same terms as a cart line). Paying the
212
+ accepted quote creates one subscription per line, with the buyer's quantity as
213
+ seats. Optional subscription lines the buyer unticks create nothing.
214
+
215
+ ```ts
216
+ const item = quote.currentRevision!.items[0];
217
+ if (item.recurring) {
218
+ // unitPrice is the per-period price per seat; a free trial charges 0 now.
219
+ const dueNow = lineDueAtAcceptance(item, item.quantity);
220
+ }
221
+ ```
222
+
223
+ `computeSelectionTotal` and `lineDueAtAcceptance` count a free-trial line as 0,
224
+ the rule the server charges by. Subscription quotes must be pay-in-full: a
225
+ rep cannot issue one on net terms or with a deposit.
226
+
209
227
  ## License
210
228
 
211
229
  MIT
package/dist/index.cjs CHANGED
@@ -28,6 +28,8 @@ __export(index_exports, {
28
28
  createQuoteCart: () => createQuoteCart,
29
29
  createQuotesClient: () => createQuotesClient,
30
30
  formatMoney: () => formatMoney,
31
+ isFreeTrialLine: () => isFreeTrialLine,
32
+ lineDueAtAcceptance: () => lineDueAtAcceptance,
31
33
  scaleLineCharges: () => scaleLineCharges,
32
34
  useQuote: () => useQuote,
33
35
  useQuoteCart: () => useQuoteCart,
@@ -201,14 +203,20 @@ function scaleLineCharges(item, quantity) {
201
203
  discountAmount: Math.round(item.discountAmount * ratio)
202
204
  };
203
205
  }
206
+ function isFreeTrialLine(item) {
207
+ return (item.recurring?.trialDays ?? 0) > 0;
208
+ }
209
+ function lineDueAtAcceptance(item, quantity) {
210
+ const { taxAmount, discountAmount } = scaleLineCharges(item, quantity);
211
+ return (isFreeTrialLine(item) ? 0 : item.unitPrice * quantity) + taxAmount - discountAmount;
212
+ }
204
213
  function computeSelectionTotal(items, selections, shippingTotal = 0) {
205
214
  let total = shippingTotal;
206
215
  for (const item of items) {
207
216
  const state = selections[item.id];
208
217
  if (item.optional && state && !state.selected) continue;
209
218
  const qty = state?.quantity ?? item.quantity;
210
- const { taxAmount, discountAmount } = scaleLineCharges(item, qty);
211
- total += item.unitPrice * qty + taxAmount - discountAmount;
219
+ total += lineDueAtAcceptance(item, qty);
212
220
  }
213
221
  return total;
214
222
  }
@@ -559,6 +567,44 @@ function useQuoteCart(options = {}) {
559
567
 
560
568
  // src/QuoteView.tsx
561
569
  var import_react2 = require("react");
570
+
571
+ // ../core-cadence/src/cadence.ts
572
+ var INTERVAL_UNITS = ["hour", "day", "week", "month"];
573
+ var NAMED_INTERVALS = ["weekly", "biweekly", "monthly", "quarterly", "yearly"];
574
+ var INTERVAL_VALUES = [...NAMED_INTERVALS, "custom"];
575
+ var HOUR_MS = 36e5;
576
+ var DAY_MS = 24 * HOUR_MS;
577
+ var WEEK_MS = 7 * DAY_MS;
578
+ function isNamedInterval(value) {
579
+ return typeof value === "string" && NAMED_INTERVALS.includes(value);
580
+ }
581
+ function isIntervalUnit(value) {
582
+ return typeof value === "string" && INTERVAL_UNITS.includes(value);
583
+ }
584
+
585
+ // ../core-cadence/src/labels.ts
586
+ var NAMED_NOUN = {
587
+ weekly: "week",
588
+ biweekly: "2 weeks",
589
+ monthly: "month",
590
+ quarterly: "quarter",
591
+ yearly: "year"
592
+ };
593
+ function cadenceNoun(cadence) {
594
+ if (isNamedInterval(cadence.interval)) return NAMED_NOUN[cadence.interval];
595
+ if (isIntervalUnit(cadence.intervalUnit) && cadence.intervalCount != null) {
596
+ return cadence.intervalCount === 1 ? cadence.intervalUnit : `${cadence.intervalCount} ${cadence.intervalUnit}s`;
597
+ }
598
+ return cadence.interval;
599
+ }
600
+ function recurringTermsLabel(terms, each) {
601
+ const noun = cadenceNoun(terms);
602
+ const trialDays = terms.trialDays ?? 0;
603
+ if (trialDays > 0) return `Free for ${trialDays} day${trialDays === 1 ? "" : "s"}, then ${each} / ${noun}`;
604
+ return `${each} / ${noun}` + (terms.totalPayments != null ? ` \xB7 ${terms.totalPayments} payments` : "");
605
+ }
606
+
607
+ // src/QuoteView.tsx
562
608
  var import_jsx_runtime = require("react/jsx-runtime");
563
609
  var row = { display: "flex", justifyContent: "space-between", gap: 16, padding: "8px 0" };
564
610
  var muted = { color: "#666", fontSize: 13 };
@@ -578,6 +624,7 @@ function QuoteView({ token, options, onCheckout, loadingFallback, className }) {
578
624
  const ordered = quote.status === "ordered";
579
625
  const accepted = quote.status === "accepted";
580
626
  const canAct = quote.status === "ready";
627
+ const recurringKept = rev ? rev.items.filter((i) => i.recurring && (!i.optional || (q.selections[i.id]?.selected ?? true))) : [];
581
628
  const handleResume = async () => {
582
629
  const { checkoutUrl } = await q.resumeCheckout();
583
630
  if (onCheckout) onCheckout(checkoutUrl);
@@ -636,7 +683,11 @@ function QuoteView({ token, options, onCheckout, loadingFallback, className }) {
636
683
  " ",
637
684
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: muted, children: "optional" })
638
685
  ] }) : item.name,
639
- item.description ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: muted, children: item.description }) : null
686
+ item.description ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: muted, children: item.description }) : null,
687
+ item.recurring ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: muted, children: [
688
+ "Subscription \xB7 ",
689
+ recurringTermsLabel(item.recurring, formatMoney(item.unitPrice * state.quantity, currency))
690
+ ] }) : null
640
691
  ] }),
641
692
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { align: "right", children: formatMoney(item.unitPrice, currency) }),
642
693
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { align: "right", children: item.qtyEditable && canAct ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -651,18 +702,23 @@ function QuoteView({ token, options, onCheckout, loadingFallback, className }) {
651
702
  style: { width: 64 }
652
703
  }
653
704
  ) : state.quantity }),
654
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { align: "right", children: included ? formatMoney(
655
- item.unitPrice * state.quantity + item.taxAmount - item.discountAmount,
656
- currency
657
- ) : formatMoney(0, currency) })
705
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { align: "right", children: formatMoney(included ? lineDueAtAcceptance(item, state.quantity) : 0, currency) })
658
706
  ] }, item.id);
659
707
  }) })
660
708
  ] }) : null,
661
709
  rev ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
662
710
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: row, children: [
663
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: "Total" }),
711
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: recurringKept.length > 0 ? "Due at acceptance" : "Total" }),
664
712
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: formatMoney(q.total, currency) })
665
713
  ] }),
714
+ recurringKept.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
715
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: muted, children: "Recurring after" }),
716
+ recurringKept.map((i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: muted, children: [
717
+ i.name,
718
+ ": ",
719
+ recurringTermsLabel(i.recurring, formatMoney(i.unitPrice * (q.selections[i.id]?.quantity ?? i.quantity), currency))
720
+ ] }, i.id))
721
+ ] }) : null,
666
722
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: muted, children: termsLabel(rev.paymentTerms, rev.depositAmount, currency) }),
667
723
  rev.leadTime ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: muted, children: [
668
724
  "Lead time: ",
@@ -936,6 +992,8 @@ function QuoteRequestForm({
936
992
  createQuoteCart,
937
993
  createQuotesClient,
938
994
  formatMoney,
995
+ isFreeTrialLine,
996
+ lineDueAtAcceptance,
939
997
  scaleLineCharges,
940
998
  useQuote,
941
999
  useQuoteCart,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.tsx","../src/errors.ts","../src/client.ts","../src/hooks.ts","../src/quote-cart.ts","../src/QuoteView.tsx","../src/QuoteRequestForm.tsx"],"sourcesContent":["export { QuotesClient, createQuotesClient, formatMoney, scaleLineCharges, computeSelectionTotal } from './client';\nexport { ThrottleQuotesError } from './errors';\nexport { useQuote, useQuoteCart, useQuoteRequestForm } from './hooks';\nexport type { UseQuoteResult, UseQuoteCartResult, UseQuoteRequestFormResult } from './hooks';\nexport { createQuoteCart } from './quote-cart';\nexport type { QuoteCart, QuoteCartLine, QuoteCartOptions, QuoteCartStorage } from './quote-cart';\nexport { QuoteView } from './QuoteView';\nexport type { QuoteViewProps } from './QuoteView';\nexport { QuoteRequestForm } from './QuoteRequestForm';\nexport type { QuoteRequestFormProps } from './QuoteRequestForm';\nexport type {\n AcceptQuoteInput,\n AcceptQuoteResult,\n BuyerQuote,\n BuyerQuoteStatus,\n ItemSelection,\n PaymentTerms,\n QuoteBranding,\n QuoteComment,\n QuoteItem,\n QuoteRequestItem,\n QuoteRevision,\n QuotesClientOptions,\n RequestFormConfig,\n SubmitQuoteRequestInput,\n ThrottleEnvelope,\n} from './types';\n","import { ThrottleError, type ThrottleErrorArgs } from '@usethrottle/errors';\n\n/**\n * Every failure from the quote-link routes surfaces as this. The `code` is the\n * API's own error code, so callers can branch on the ones that matter to a\n * buyer flow rather than parsing messages:\n *\n * revision_superseded — a newer revision was issued; `details.currentRevisionId`\n * tells you what to reload\n * quote_expired — the offer lapsed (HTTP 410)\n * invalid_item_selection — locked-line edit, non-optional deselect, or unknown item\n * accept_in_progress — a concurrent accept won; retry shortly\n * not_found — unknown/rotated token, or a draft/archived quote\n */\nexport class ThrottleQuotesError extends ThrottleError {\n constructor(args: {\n code: string;\n message: string;\n statusCode: number;\n details?: unknown;\n responseHeaders?: ThrottleErrorArgs['responseHeaders'];\n }) {\n super(args);\n this.name = 'ThrottleQuotesError';\n }\n\n /** The offer moved on — reload and show the buyer the current revision. */\n get isSuperseded(): boolean {\n return this.code === 'revision_superseded';\n }\n\n get isExpired(): boolean {\n return this.code === 'quote_expired';\n }\n\n /** Revision the server considers current, when it told us (spec §8.1). */\n get currentRevisionId(): string | undefined {\n const d = this.details as { currentRevisionId?: string } | undefined;\n return d?.currentRevisionId;\n }\n}\n","import { ThrottleQuotesError } from './errors';\nimport type {\n AcceptQuoteInput,\n AcceptQuoteResult,\n BuyerQuote,\n QuoteComment,\n QuotesClientOptions,\n RequestFormConfig,\n SubmitQuoteRequestInput,\n ThrottleEnvelope,\n} from './types';\n\nconst DEFAULT_BASE = 'https://api.usethrottle.dev';\n\n/**\n * Headless client for the public quote surfaces. Everything is authorized by\n * the capability token in the URL — there is no API key here, and there must\n * not be: this runs in the buyer's browser.\n */\nexport class QuotesClient {\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof globalThis.fetch;\n private readonly timeoutMs: number;\n\n constructor(opts: QuotesClientOptions = {}) {\n const fetchImpl = opts.fetch ?? globalThis.fetch;\n if (!fetchImpl) throw new Error('QuotesClient: fetch is required');\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE).replace(/\\/+$/, '');\n this.fetchImpl = fetchImpl;\n this.timeoutMs = opts.timeoutMs ?? 30_000;\n }\n\n private async request<T>(method: string, path: string, body?: unknown): Promise<T> {\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);\n try {\n const res = await this.fetchImpl(`${this.baseUrl}${path}`, {\n method,\n headers: body === undefined ? {} : { 'content-type': 'application/json' },\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n signal: ctrl.signal,\n });\n const json = (await res.json().catch(() => ({}))) as ThrottleEnvelope<T>;\n if (!res.ok) {\n throw new ThrottleQuotesError({\n code: json.error?.code ?? 'unknown_error',\n message: json.error?.message ?? `HTTP ${res.status}`,\n statusCode: res.status,\n details: json.error?.details,\n responseHeaders: res.headers,\n });\n }\n return json.data as T;\n } finally {\n clearTimeout(timer);\n }\n }\n\n /** Fetch the quote a buyer link points at. 404s for unknown/rotated tokens. */\n getQuote(token: string): Promise<BuyerQuote> {\n return this.request<BuyerQuote>('GET', `/api/v1/quote-links/${encodeURIComponent(token)}`);\n }\n\n /**\n * Tell the merchant the buyer opened the quote. Fire-and-forget: the server\n * throttles the resulting webhook to one per revision per 6h, and a failed\n * beacon must never break the page.\n */\n async recordView(token: string): Promise<void> {\n try {\n await this.request('POST', `/api/v1/quote-links/${encodeURIComponent(token)}/view`, {});\n } catch {\n /* telemetry only */\n }\n }\n\n addComment(token: string, input: { body: string; authorName?: string; authorEmail?: string }) {\n return this.request<QuoteComment>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/comments`,\n input,\n );\n }\n\n requestRevision(token: string, input: { message?: string; authorName?: string } = {}) {\n return this.request<BuyerQuote>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/request-revision`,\n input,\n );\n }\n\n decline(token: string, input: { reason?: string; authorName?: string } = {}) {\n return this.request<BuyerQuote>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/decline`,\n input,\n );\n }\n\n /**\n * Accept and get the checkout URL to send the buyer to.\n *\n * Re-accepting the same revision is idempotent and replays the same URL.\n * Accepting a superseded revision throws `revision_superseded` carrying\n * `currentRevisionId`; an expired offer throws `quote_expired`.\n */\n accept(token: string, input: AcceptQuoteInput): Promise<AcceptQuoteResult> {\n return this.request<AcceptQuoteResult>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/accept`,\n input,\n );\n }\n\n /**\n * Send a buyer who already accepted back to a LIVE checkout.\n *\n * Checkout sessions expire after 30 minutes, so the URL returned by\n * `accept` is usually dead by the time the buyer returns to the link. This\n * re-opens a session against the same cart when it is (no re-pricing).\n * Throws `checkout_not_resumable` if the quote was never accepted, and\n * `checkout_completed` once it has been paid.\n */\n resumeCheckout(token: string): Promise<{ checkoutUrl: string }> {\n return this.request<{ checkoutUrl: string }>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/checkout`,\n {},\n );\n }\n\n /** URL of the PDF for the revision the buyer is looking at (server 302s to signed storage). */\n pdfUrl(token: string): string {\n return `${this.baseUrl}/api/v1/quote-links/${encodeURIComponent(token)}/pdf`;\n }\n\n // ── RFQ intake ─────────────────────────────────────────────────────────────\n\n getRequestForm(formToken: string): Promise<RequestFormConfig> {\n return this.request<RequestFormConfig>(\n 'GET',\n `/api/v1/quote-request-forms/${encodeURIComponent(formToken)}/public`,\n );\n }\n\n /**\n * Submit an RFQ. The response is deliberately thin (`{ submitted: true }`) —\n * anonymous callers learn no internal identifiers. A submission caught by\n * the honeypot returns the same shape without creating anything, so a bot\n * cannot tell it was detected; treat success as \"accepted\", not \"created\".\n */\n submitRequest(formToken: string, input: SubmitQuoteRequestInput): Promise<{ submitted: boolean }> {\n return this.request<{ submitted: boolean }>('POST', '/api/v1/quote-requests', {\n formToken,\n ...input,\n });\n }\n}\n\nexport function createQuotesClient(opts: QuotesClientOptions = {}): QuotesClient {\n return new QuotesClient(opts);\n}\n\n// ── money formatting ─────────────────────────────────────────────────────────\n\n/** Minor units → a display string, e.g. (104000, 'USD') → \"$1,040.00\". */\nexport function formatMoney(minorUnits: number, currency: string, locale?: string): string {\n return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(minorUnits / 100);\n}\n\n/**\n * Scale a line's quoted tax/discount to an edited quantity.\n *\n * MUST match `scaleLineCharges` in @commerce/quotes — the server recomputes\n * the accepted total with this rule, and any divergence shows the buyer one\n * total and charges another.\n */\nexport function scaleLineCharges(\n item: { quantity: number; taxAmount: number; discountAmount: number },\n quantity: number,\n): { taxAmount: number; discountAmount: number } {\n const base = item.quantity;\n if (!base || base <= 0 || quantity === base) {\n return { taxAmount: item.taxAmount, discountAmount: item.discountAmount };\n }\n const ratio = quantity / base;\n return {\n taxAmount: Math.round(item.taxAmount * ratio),\n discountAmount: Math.round(item.discountAmount * ratio),\n };\n}\n\n/** Effective total for a selection, using the same math the server will apply. */\nexport function computeSelectionTotal(\n items: Array<{\n id: string;\n unitPrice: number;\n quantity: number;\n taxAmount: number;\n discountAmount: number;\n optional: boolean;\n }>,\n selections: Record<string, { selected: boolean; quantity: number }>,\n shippingTotal = 0,\n): number {\n let total = shippingTotal;\n for (const item of items) {\n const state = selections[item.id];\n if (item.optional && state && !state.selected) continue;\n const qty = state?.quantity ?? item.quantity;\n const { taxAmount, discountAmount } = scaleLineCharges(item, qty);\n total += item.unitPrice * qty + taxAmount - discountAmount;\n }\n return total;\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { QuotesClient, computeSelectionTotal, createQuotesClient } from './client';\nimport { ThrottleQuotesError } from './errors';\nimport { createQuoteCart, type QuoteCart, type QuoteCartLine, type QuoteCartOptions } from './quote-cart';\nimport type {\n AcceptQuoteInput,\n BuyerQuote,\n ItemSelection,\n QuoteRequestItem,\n QuotesClientOptions,\n RequestFormConfig,\n SubmitQuoteRequestInput,\n} from './types';\n\nexport interface UseQuoteResult {\n quote: BuyerQuote | null;\n loading: boolean;\n error: ThrottleQuotesError | null;\n /** Re-fetch — call after a `revision_superseded` to show the current offer. */\n refresh: () => Promise<void>;\n /** Buyer's current line choices, keyed by item id. */\n selections: Record<string, { selected: boolean; quantity: number }>;\n setSelected: (itemId: string, selected: boolean) => void;\n setQuantity: (itemId: string, quantity: number) => void;\n /** Live total for the current selections, using the server's own math. */\n total: number;\n accept: (\n input: Omit<AcceptQuoteInput, 'revisionId' | 'itemSelections'>,\n ) => Promise<{ checkoutUrl: string }>;\n /** Live checkout URL for a buyer returning to an already-accepted quote —\n * the one minted at acceptance has usually expired by then. */\n resumeCheckout: () => Promise<{ checkoutUrl: string }>;\n requestRevision: (message: string, authorName?: string) => Promise<void>;\n decline: (reason?: string, authorName?: string) => Promise<void>;\n addComment: (body: string, authorName?: string) => Promise<void>;\n pdfUrl: string;\n submitting: boolean;\n}\n\n/**\n * Everything the buyer quote page needs.\n *\n * Fires the view beacon once per mount, keeps line selections in local state,\n * and recomputes the total with the same rule the server applies at\n * acceptance — so the number the buyer sees is the number they are charged.\n */\nexport function useQuote(token: string, options: QuotesClientOptions = {}): UseQuoteResult {\n const client = useMemo(\n () => new QuotesClient(options),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [options.baseUrl, options.timeoutMs],\n );\n const [quote, setQuote] = useState<BuyerQuote | null>(null);\n const [loading, setLoading] = useState(true);\n const [submitting, setSubmitting] = useState(false);\n const [error, setError] = useState<ThrottleQuotesError | null>(null);\n const [selections, setSelections] = useState<Record<string, { selected: boolean; quantity: number }>>({});\n const beaconSent = useRef(false);\n\n const load = useCallback(async () => {\n setLoading(true);\n try {\n const q = await client.getQuote(token);\n setQuote(q);\n setError(null);\n // Seed selections from the revision's own defaults so a re-issue that\n // adds or locks lines resets the buyer's choices to the new offer.\n const next: Record<string, { selected: boolean; quantity: number }> = {};\n for (const item of q.currentRevision?.items ?? []) {\n next[item.id] = { selected: item.optional ? item.optionalSelected : true, quantity: item.quantity };\n }\n setSelections(next);\n } catch (e) {\n setError(e as ThrottleQuotesError);\n } finally {\n setLoading(false);\n }\n }, [client, token]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n useEffect(() => {\n if (!quote || beaconSent.current) return;\n beaconSent.current = true;\n void client.recordView(token);\n }, [client, quote, token]);\n\n const setSelected = useCallback((itemId: string, selected: boolean) => {\n setSelections((prev) => ({ ...prev, [itemId]: { ...(prev[itemId] ?? { quantity: 1 }), selected } }));\n }, []);\n\n const setQuantity = useCallback((itemId: string, quantity: number) => {\n setSelections((prev) => ({\n ...prev,\n [itemId]: { ...(prev[itemId] ?? { selected: true }), quantity: Math.max(1, Math.floor(quantity) || 1) },\n }));\n }, []);\n\n const total = useMemo(\n () =>\n computeSelectionTotal(\n quote?.currentRevision?.items ?? [],\n selections,\n quote?.currentRevision?.shippingTotal ?? 0,\n ),\n [quote, selections],\n );\n\n /** Only send selections the rep actually allows the buyer to change. */\n const buildSelections = useCallback((): ItemSelection[] => {\n const items = quote?.currentRevision?.items ?? [];\n const out: ItemSelection[] = [];\n for (const item of items) {\n const state = selections[item.id];\n if (!state) continue;\n const sel: ItemSelection = { itemId: item.id };\n let include = false;\n if (item.optional && state.selected !== item.optionalSelected) {\n sel.selected = state.selected;\n include = true;\n }\n if (item.qtyEditable && state.quantity !== item.quantity) {\n sel.quantity = state.quantity;\n include = true;\n }\n if (include) out.push(sel);\n }\n return out;\n }, [quote, selections]);\n\n const guard = useCallback(\n async <T,>(fn: () => Promise<T>): Promise<T> => {\n setSubmitting(true);\n try {\n const result = await fn();\n setError(null);\n return result;\n } catch (e) {\n const err = e as ThrottleQuotesError;\n setError(err);\n // A superseded offer means our copy is stale — pull the current one so\n // the buyer sees what they can actually accept (spec §8.1).\n if (err.isSuperseded) void load();\n throw err;\n } finally {\n setSubmitting(false);\n }\n },\n [load],\n );\n\n const accept = useCallback(\n async (input: Omit<AcceptQuoteInput, 'revisionId' | 'itemSelections'>) => {\n const revisionId = quote?.currentRevision?.id;\n if (!revisionId) throw new Error('useQuote: no current revision to accept');\n const itemSelections = buildSelections();\n const res = await guard(() =>\n client.accept(token, {\n ...input,\n revisionId,\n ...(itemSelections.length ? { itemSelections } : {}),\n }),\n );\n return { checkoutUrl: res.checkoutUrl };\n },\n [buildSelections, client, guard, quote, token],\n );\n\n const resumeCheckout = useCallback(\n async () => guard(() => client.resumeCheckout(token)),\n [client, guard, token],\n );\n\n const requestRevision = useCallback(\n async (message: string, authorName?: string) => {\n const q = await guard(() => client.requestRevision(token, { message, authorName }));\n setQuote(q);\n },\n [client, guard, token],\n );\n\n const decline = useCallback(\n async (reason?: string, authorName?: string) => {\n const q = await guard(() => client.decline(token, { reason, authorName }));\n setQuote(q);\n },\n [client, guard, token],\n );\n\n const addComment = useCallback(\n async (body: string, authorName?: string) => {\n await guard(() => client.addComment(token, { body, authorName }));\n await load();\n },\n [client, guard, load, token],\n );\n\n return {\n quote,\n loading,\n error,\n refresh: load,\n selections,\n setSelected,\n setQuantity,\n total,\n accept,\n resumeCheckout,\n requestRevision,\n decline,\n addComment,\n pdfUrl: client.pdfUrl(token),\n submitting,\n };\n}\n\nexport interface UseQuoteRequestFormResult {\n config: RequestFormConfig | null;\n loading: boolean;\n error: ThrottleQuotesError | null;\n submitting: boolean;\n submitted: boolean;\n submit: (input: SubmitQuoteRequestInput) => Promise<void>;\n /** Mount time; pass through so the API's minimum-fill-time check passes. */\n formLoadedAt: number;\n}\n\n/** Loads a hosted RFQ form's config and submits it, with bot fields handled. */\nexport function useQuoteRequestForm(\n formToken: string,\n options: QuotesClientOptions = {},\n): UseQuoteRequestFormResult {\n const client = useMemo(\n () => new QuotesClient(options),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [options.baseUrl, options.timeoutMs],\n );\n const [config, setConfig] = useState<RequestFormConfig | null>(null);\n const [loading, setLoading] = useState(true);\n const [submitting, setSubmitting] = useState(false);\n const [submitted, setSubmitted] = useState(false);\n const [error, setError] = useState<ThrottleQuotesError | null>(null);\n const formLoadedAt = useRef(Date.now()).current;\n\n useEffect(() => {\n let cancelled = false;\n (async () => {\n try {\n const cfg = await client.getRequestForm(formToken);\n if (!cancelled) setConfig(cfg);\n } catch (e) {\n if (!cancelled) setError(e as ThrottleQuotesError);\n } finally {\n if (!cancelled) setLoading(false);\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [client, formToken]);\n\n const submit = useCallback(\n async (input: SubmitQuoteRequestInput) => {\n setSubmitting(true);\n try {\n await client.submitRequest(formToken, { formLoadedAt, ...input });\n setSubmitted(true);\n setError(null);\n } catch (e) {\n setError(e as ThrottleQuotesError);\n throw e;\n } finally {\n setSubmitting(false);\n }\n },\n [client, formLoadedAt, formToken],\n );\n\n return { config, loading, error, submitting, submitted, submit, formLoadedAt };\n}\n\nexport { createQuotesClient };\n\nexport interface UseQuoteCartResult {\n lines: QuoteCartLine[];\n /** Total quantity across lines — for a header badge. */\n count: number;\n add: (item: QuoteRequestItem) => void;\n addMany: (items: QuoteRequestItem[]) => void;\n update: (key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>) => void;\n remove: (key: string) => void;\n clear: () => void;\n /** The underlying cart, for use outside React (an Add to Quote button in plain JS). */\n cart: QuoteCart;\n}\n\n/**\n * A quote cart the buyer fills while browsing, then submits as one request.\n *\n * The cart persists across page views, so an Add to Quote button on a product\n * page and a Request quote page elsewhere on the site see the same lines.\n * Pass the result of `toRequestItems()` (or `lines`) to\n * `useQuoteRequestForm().submit` as `items`.\n */\nexport function useQuoteCart(options: QuoteCartOptions = {}): UseQuoteCartResult {\n const cart = useMemo(\n () => createQuoteCart(options),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [options.storageKey, options.maxLines],\n );\n // Seeded empty rather than from storage: reading localStorage during render\n // makes the server and the first client render disagree, which is a\n // hydration mismatch. The effect below fills it in immediately after mount.\n const [lines, setLines] = useState<QuoteCartLine[]>([]);\n\n useEffect(() => {\n setLines(cart.read());\n return cart.subscribe(setLines);\n }, [cart]);\n\n return {\n lines,\n count: lines.reduce((sum, line) => sum + line.quantity, 0),\n add: useCallback((item: QuoteRequestItem) => void cart.add(item), [cart]),\n addMany: useCallback((items: QuoteRequestItem[]) => void cart.addMany(items), [cart]),\n update: useCallback(\n (key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>) => void cart.update(key, patch),\n [cart],\n ),\n remove: useCallback((key: string) => void cart.remove(key), [cart]),\n clear: useCallback(() => cart.clear(), [cart]),\n cart,\n };\n}\n","import type { QuoteRequestItem } from './types';\n\n/**\n * A quote cart: the buyer's running list of things to be quoted, built up\n * while they browse.\n *\n * Throttle stores no catalog, so the storefront owns product data and hands\n * lines in — the same arrangement as the shopping cart. Nothing here is sent\n * until `read()` is submitted through `QuotesClient.submitRequest`, and the\n * amounts a storefront attaches are the buyer's TARGET price, never a price\n * we will honour.\n *\n * Lines are keyed by `sku ?? referenceId ?? name`, so adding the same product\n * twice increases its quantity instead of stacking duplicate rows.\n */\n\nexport interface QuoteCartLine extends QuoteRequestItem {\n /** Stable identity for updates and removals. */\n key: string;\n quantity: number;\n}\n\nexport interface QuoteCartStorage {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nexport interface QuoteCartOptions {\n /** Namespacing key, so two storefronts on one origin cannot collide. */\n storageKey?: string;\n /**\n * Where the cart survives page navigation. Defaults to `localStorage` in a\n * browser and to memory everywhere else (SSR, tests) — a cart that throws\n * during server rendering would take the whole page down.\n */\n storage?: QuoteCartStorage | null;\n /** Refuse to grow past this many distinct lines. The API caps at 100. */\n maxLines?: number;\n}\n\nconst DEFAULT_KEY = 'throttle:quote-cart';\nconst DEFAULT_MAX_LINES = 100;\n\nfunction memoryStorage(): QuoteCartStorage {\n const map = new Map<string, string>();\n return {\n getItem: (key) => map.get(key) ?? null,\n setItem: (key, value) => void map.set(key, value),\n removeItem: (key) => void map.delete(key),\n };\n}\n\nfunction defaultStorage(): QuoteCartStorage {\n try {\n if (typeof globalThis !== 'undefined' && (globalThis as any).localStorage) {\n return (globalThis as any).localStorage as QuoteCartStorage;\n }\n } catch {\n // Safari in private mode throws on access rather than returning null.\n }\n return memoryStorage();\n}\n\nfunction lineKey(item: QuoteRequestItem): string {\n const raw = item.sku?.trim() || item.referenceId?.trim() || item.name.trim();\n return raw.toLowerCase();\n}\n\nfunction normalizeQuantity(value: number | null | undefined): number {\n const n = Math.floor(Number(value ?? 1));\n return Number.isFinite(n) && n > 0 ? n : 1;\n}\n\nexport interface QuoteCart {\n /** Current lines, oldest first. */\n read(): QuoteCartLine[];\n /** Total quantity across lines — what a header badge should show. */\n count(): number;\n /** Add a line, merging quantities into an existing one with the same key. */\n add(item: QuoteRequestItem): QuoteCartLine[];\n /** Add several at once (a \"quote this whole cart\" button). */\n addMany(items: QuoteRequestItem[]): QuoteCartLine[];\n /** Change any field of a line. A quantity below 1 removes it. */\n update(key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>): QuoteCartLine[];\n remove(key: string): QuoteCartLine[];\n clear(): void;\n /** Subscribe to every change. Returns an unsubscribe function. */\n subscribe(listener: (lines: QuoteCartLine[]) => void): () => void;\n /** The `items` payload for `QuotesClient.submitRequest`. */\n toRequestItems(): QuoteRequestItem[];\n}\n\nexport function createQuoteCart(options: QuoteCartOptions = {}): QuoteCart {\n const storageKey = options.storageKey ?? DEFAULT_KEY;\n const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n const storage = options.storage === null ? memoryStorage() : (options.storage ?? defaultStorage());\n const listeners = new Set<(lines: QuoteCartLine[]) => void>();\n\n function load(): QuoteCartLine[] {\n try {\n const raw = storage.getItem(storageKey);\n if (!raw) return [];\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n // Storage is buyer-writable — anything shaped wrong is dropped rather\n // than trusted into a submission.\n return parsed\n .filter((line: any) => line && typeof line.name === 'string' && line.name.trim())\n .map((line: any) => ({\n ...line,\n key: typeof line.key === 'string' && line.key ? line.key : lineKey(line),\n quantity: normalizeQuantity(line.quantity),\n }))\n .slice(0, maxLines);\n } catch {\n return [];\n }\n }\n\n function save(lines: QuoteCartLine[]): QuoteCartLine[] {\n try {\n storage.setItem(storageKey, JSON.stringify(lines));\n } catch {\n // Quota or a disabled store — the cart still works for this page view.\n }\n for (const listener of listeners) listener(lines);\n return lines;\n }\n\n return {\n read: load,\n\n count() {\n return load().reduce((sum, line) => sum + line.quantity, 0);\n },\n\n add(item) {\n return this.addMany([item]);\n },\n\n addMany(items) {\n const lines = load();\n for (const item of items) {\n if (!item?.name?.trim()) continue;\n const key = lineKey(item);\n const existing = lines.find((line) => line.key === key);\n if (existing) {\n existing.quantity += normalizeQuantity(item.quantity);\n // Later detail wins — a product page knows more than a listing card.\n if (item.note) existing.note = item.note;\n if (item.targetUnitPrice != null) existing.targetUnitPrice = item.targetUnitPrice;\n continue;\n }\n if (lines.length >= maxLines) break;\n lines.push({ ...item, key, quantity: normalizeQuantity(item.quantity) });\n }\n return save(lines);\n },\n\n update(key, patch) {\n const lines = load();\n const index = lines.findIndex((line) => line.key === key);\n if (index === -1) return lines;\n if (patch.quantity != null && Math.floor(Number(patch.quantity)) < 1) {\n lines.splice(index, 1);\n return save(lines);\n }\n lines[index] = {\n ...lines[index],\n ...patch,\n key,\n quantity: normalizeQuantity(patch.quantity ?? lines[index].quantity),\n };\n return save(lines);\n },\n\n remove(key) {\n return save(load().filter((line) => line.key !== key));\n },\n\n clear() {\n try {\n storage.removeItem(storageKey);\n } catch {\n // Ignore — the notification below still resets any subscriber.\n }\n for (const listener of listeners) listener([]);\n },\n\n subscribe(listener) {\n listeners.add(listener);\n return () => void listeners.delete(listener);\n },\n\n toRequestItems() {\n return load().map(({ key: _key, ...item }) => item);\n },\n };\n}\n","import { useState, type CSSProperties, type ReactNode } from 'react';\nimport { formatMoney } from './client';\nimport { useQuote } from './hooks';\nimport type { QuotesClientOptions } from './types';\n\nexport interface QuoteViewProps {\n /** The `qtl_…` token from the buyer's link. */\n token: string;\n options?: QuotesClientOptions;\n /**\n * Where to send the buyer after acceptance. Defaults to assigning\n * `window.location.href`, which is what the hosted page does.\n */\n onCheckout?: (checkoutUrl: string) => void;\n /** Rendered instead of the built-in markup while loading. */\n loadingFallback?: ReactNode;\n className?: string;\n}\n\nconst row: CSSProperties = { display: 'flex', justifyContent: 'space-between', gap: 16, padding: '8px 0' };\nconst muted: CSSProperties = { color: '#666', fontSize: 13 };\n\n/**\n * Drop-in buyer quote page: line items with optional toggles and quantity\n * editing, live totals, revision history, comment thread, and the\n * accept / request-changes / decline actions.\n *\n * Intentionally unstyled beyond a readable default — it is meant to be\n * restyled or replaced. `useQuote` carries all the behaviour if you want to\n * build your own presentation.\n */\nexport function QuoteView({ token, options, onCheckout, loadingFallback, className }: QuoteViewProps) {\n const q = useQuote(token, options ?? {});\n const [name, setName] = useState('');\n const [email, setEmail] = useState('');\n const [showAccept, setShowAccept] = useState(false);\n const [comment, setComment] = useState('');\n\n if (q.loading) return <>{loadingFallback ?? <p>Loading quote…</p>}</>;\n if (!q.quote) {\n return <p>This quote link is no longer available.</p>;\n }\n\n const quote = q.quote;\n const rev = quote.currentRevision;\n const currency = quote.currency;\n const ordered = quote.status === 'ordered';\n const accepted = quote.status === 'accepted';\n const canAct = quote.status === 'ready';\n\n const handleResume = async () => {\n const { checkoutUrl } = await q.resumeCheckout();\n if (onCheckout) onCheckout(checkoutUrl);\n else if (typeof window !== 'undefined') window.location.href = checkoutUrl;\n };\n\n const handleAccept = async () => {\n const { checkoutUrl } = await q.accept({ acceptedByName: name, acceptedByEmail: email });\n if (onCheckout) onCheckout(checkoutUrl);\n else if (typeof window !== 'undefined') window.location.href = checkoutUrl;\n };\n\n return (\n <div className={className}>\n <header>\n {quote.branding?.displayName ? <p style={muted}>{quote.branding.displayName}</p> : null}\n <h1>Quote {quote.quoteNumber}</h1>\n <p style={muted}>{statusLabel(quote.status)}</p>\n {quote.buyerReference ? <p style={muted}>Your reference: {quote.buyerReference}</p> : null}\n {rev ? (\n <p style={muted}>\n Revision {rev.revisionNumber}\n {rev.expiresAt ? ` · valid until ${new Date(rev.expiresAt).toLocaleDateString()}` : null}\n {' · '}\n <a href={q.pdfUrl}>Download PDF</a>\n </p>\n ) : null}\n </header>\n\n {q.error ? (\n <p role=\"alert\">\n {q.error.isSuperseded\n ? 'This quote was updated — showing the latest version.'\n : q.error.message}\n </p>\n ) : null}\n\n {rev ? (\n <table style={{ width: '100%', borderCollapse: 'collapse' }}>\n <thead>\n <tr>\n <th align=\"left\">Item</th>\n <th align=\"right\">Unit price</th>\n <th align=\"right\">Qty</th>\n <th align=\"right\">Total</th>\n </tr>\n </thead>\n <tbody>\n {rev.items.map((item) => {\n const state = q.selections[item.id] ?? { selected: true, quantity: item.quantity };\n const included = item.optional ? state.selected : true;\n return (\n <tr key={item.id}>\n <td>\n {item.optional ? (\n <label>\n <input\n type=\"checkbox\"\n checked={state.selected}\n disabled={!canAct}\n onChange={(e) => q.setSelected(item.id, e.target.checked)}\n />{' '}\n {item.name} <span style={muted}>optional</span>\n </label>\n ) : (\n item.name\n )}\n {item.description ? <div style={muted}>{item.description}</div> : null}\n </td>\n <td align=\"right\">{formatMoney(item.unitPrice, currency)}</td>\n <td align=\"right\">\n {item.qtyEditable && canAct ? (\n <input\n type=\"number\"\n min={1}\n value={state.quantity}\n disabled={!included}\n aria-label={`Quantity for ${item.name}`}\n onChange={(e) => q.setQuantity(item.id, Number(e.target.value))}\n style={{ width: 64 }}\n />\n ) : (\n state.quantity\n )}\n </td>\n <td align=\"right\">\n {included\n ? formatMoney(\n item.unitPrice * state.quantity + item.taxAmount - item.discountAmount,\n currency,\n )\n : formatMoney(0, currency)}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n ) : null}\n\n {rev ? (\n <section>\n <div style={row}>\n <strong>Total</strong>\n <strong>{formatMoney(q.total, currency)}</strong>\n </div>\n <p style={muted}>{termsLabel(rev.paymentTerms, rev.depositAmount, currency)}</p>\n {rev.leadTime ? <p style={muted}>Lead time: {rev.leadTime}</p> : null}\n {rev.buyerNote ? <blockquote>{rev.buyerNote}</blockquote> : null}\n </section>\n ) : null}\n\n {ordered ? (\n <p>\n Ordered{quote.orderId ? ` — order ${quote.orderId}` : ''}. Thank you.\n </p>\n ) : accepted ? (\n // Accepted but not yet paid. The checkout session minted at acceptance\n // expires in 30 minutes, so this asks the server for a live one rather\n // than linking to a URL that is probably dead.\n <section>\n <p>You accepted this quote — payment is not finished yet.</p>\n <button type=\"button\" disabled={q.submitting} onClick={() => void handleResume()}>\n Continue to checkout\n </button>\n </section>\n ) : canAct ? (\n <section>\n {showAccept ? (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n void handleAccept();\n }}\n >\n <label>\n Your name\n <input value={name} onChange={(e) => setName(e.target.value)} required />\n </label>\n <label>\n Email\n <input type=\"email\" value={email} onChange={(e) => setEmail(e.target.value)} required />\n </label>\n <button type=\"submit\" disabled={q.submitting || !name || !email}>\n Confirm &amp; continue to payment\n </button>\n </form>\n ) : (\n <button type=\"button\" onClick={() => setShowAccept(true)}>\n Accept &amp; checkout\n </button>\n )}\n <button\n type=\"button\"\n disabled={q.submitting}\n onClick={() => void q.requestRevision('Please revise this quote.', name || undefined)}\n >\n Request changes\n </button>\n <button type=\"button\" disabled={q.submitting} onClick={() => void q.decline(undefined, name || undefined)}>\n Decline\n </button>\n </section>\n ) : null}\n\n {quote.revisionHistory.length > 0 ? (\n <details>\n <summary>Previous versions ({quote.revisionHistory.length})</summary>\n <ul>\n {quote.revisionHistory.map((r) => (\n <li key={r.id}>\n Revision {r.revisionNumber} — {formatMoney(r.total, currency)}\n {r.changeSummary ? ` · ${r.changeSummary}` : ''}\n </li>\n ))}\n </ul>\n </details>\n ) : null}\n\n <section>\n <h2>Questions</h2>\n {quote.comments.map((c) => (\n <p key={c.id}>\n <strong>{c.authorName ?? (c.authorType === 'merchant' ? 'Seller' : 'You')}:</strong> {c.body}\n </p>\n ))}\n <form\n onSubmit={(e) => {\n e.preventDefault();\n const body = comment.trim();\n if (!body) return;\n setComment('');\n void q.addComment(body, name || undefined);\n }}\n >\n <input\n value={comment}\n onChange={(e) => setComment(e.target.value)}\n placeholder=\"Ask a question…\"\n aria-label=\"Ask a question\"\n />\n <button type=\"submit\" disabled={q.submitting || !comment.trim()}>\n Send\n </button>\n </form>\n </section>\n </div>\n );\n}\n\nfunction statusLabel(status: string): string {\n switch (status) {\n case 'in_progress':\n return 'We are preparing your quote';\n case 'ready':\n return 'Ready for your review';\n case 'being_revised':\n return 'We are revising this quote';\n case 'accepted':\n return 'Accepted — completing payment';\n case 'ordered':\n return 'Ordered';\n case 'declined':\n return 'Declined';\n case 'expired':\n return 'This quote has expired — request updated pricing';\n default:\n return status;\n }\n}\n\nfunction termsLabel(\n terms: { mode: string; netN?: number; balanceNetN?: number },\n depositAmount: number | null,\n currency: string,\n): string {\n if (terms.mode === 'net_terms') return `Payment terms: Net ${terms.netN}`;\n if (terms.mode === 'deposit_balance') {\n const dep = depositAmount != null ? formatMoney(depositAmount, currency) : 'a deposit';\n return `${dep} due today, balance Net ${terms.balanceNetN}`;\n }\n return 'Payment in full';\n}\n","import { useState, type CSSProperties, type ReactNode } from 'react';\nimport { useQuoteRequestForm } from './hooks';\nimport type { QuoteRequestItem, QuotesClientOptions } from './types';\n\nexport interface QuoteRequestFormProps {\n /** The `qrf_…` token from the merchant's hosted form URL. */\n formToken: string;\n options?: QuotesClientOptions;\n /**\n * Turnstile token, when the form requires it. Render the widget yourself and\n * pass its token through — this package deliberately pulls in no Cloudflare\n * script. If `config.fields.requireTurnstile` is set and this is empty, the\n * API fails the submission closed.\n */\n turnstileToken?: string;\n /**\n * Lines to submit with the request — typically `useQuoteCart().lines` after\n * the buyer has been adding products while browsing. When present, the\n * written message is optional.\n */\n items?: QuoteRequestItem[];\n onSubmitted?: () => void;\n loadingFallback?: ReactNode;\n className?: string;\n}\n\n/** Off-screen but still focusable-free; bots fill it, humans never see it. */\nconst honeypotStyle: CSSProperties = {\n position: 'absolute',\n width: 1,\n height: 1,\n padding: 0,\n margin: -1,\n overflow: 'hidden',\n clip: 'rect(0 0 0 0)',\n whiteSpace: 'nowrap',\n border: 0,\n};\n\n/**\n * Hosted RFQ intake form.\n *\n * Bot protection is wired in for you: the honeypot field and the mount-time\n * anchor that feeds the API's minimum-fill-time check. A honeypot hit returns\n * a success-shaped response and creates nothing, so `onSubmitted` fires either\n * way — by design, a bot must not learn it was caught.\n */\nexport function QuoteRequestForm({\n formToken,\n options,\n turnstileToken,\n items,\n onSubmitted,\n loadingFallback,\n className,\n}: QuoteRequestFormProps) {\n const f = useQuoteRequestForm(formToken, options ?? {});\n // Lines the host collected while the buyer browsed (see `useQuoteCart`).\n // With any of these present the written message becomes optional: the ask\n // is the list.\n const lines = items ?? [];\n const [values, setValues] = useState({\n email: '',\n name: '',\n companyName: '',\n phone: '',\n buyerReference: '',\n message: '',\n website: '', // honeypot\n });\n\n const set = (k: keyof typeof values) => (e: { target: { value: string } }) =>\n setValues((prev) => ({ ...prev, [k]: e.target.value }));\n\n if (f.loading) return <>{loadingFallback ?? <p>Loading…</p>}</>;\n if (!f.config) return <p>This request form is not available.</p>;\n\n if (f.submitted) {\n const redirect = f.config.redirectUrl;\n // Re-check the scheme client-side even though the API rejects non-http(s)\n // on write — a stale row must not be able to script the host page.\n if (redirect && /^https?:\\/\\//i.test(redirect) && typeof window !== 'undefined') {\n window.location.href = redirect;\n }\n return <p>{f.config.successMessage ?? 'Thanks — we will be in touch shortly.'}</p>;\n }\n\n const fields = f.config.fields ?? {};\n\n return (\n <form\n className={className}\n onSubmit={(e) => {\n e.preventDefault();\n void f\n .submit({\n email: values.email,\n message: values.message || null,\n ...(lines.length > 0 ? { items: lines } : {}),\n name: values.name || null,\n companyName: fields.companyName ? values.companyName || null : null,\n phone: fields.phone ? values.phone || null : null,\n buyerReference: fields.buyerReference ? values.buyerReference || null : null,\n website: values.website || null,\n ...(turnstileToken ? { turnstileToken } : {}),\n })\n .then(() => onSubmitted?.())\n .catch(() => {\n /* surfaced via f.error */\n });\n }}\n >\n {f.config.title ? <h1>{f.config.title}</h1> : null}\n {f.config.introText ? <p>{f.config.introText}</p> : null}\n {f.error ? <p role=\"alert\">{f.error.message}</p> : null}\n\n <label>\n Email\n <input type=\"email\" value={values.email} onChange={set('email')} required />\n </label>\n <label>\n Name\n <input value={values.name} onChange={set('name')} />\n </label>\n {fields.companyName ? (\n <label>\n Company\n <input value={values.companyName} onChange={set('companyName')} />\n </label>\n ) : null}\n {fields.phone ? (\n <label>\n Phone\n <input value={values.phone} onChange={set('phone')} />\n </label>\n ) : null}\n {fields.buyerReference ? (\n <label>\n Your reference #\n <input value={values.buyerReference} onChange={set('buyerReference')} />\n </label>\n ) : null}\n {lines.length > 0 ? (\n <ul aria-label=\"Items on this request\">\n {lines.map((line, index) => (\n <li key={line.sku ?? line.referenceId ?? `${line.name}-${index}`}>\n {line.quantity ?? 1} × {line.name}\n {line.sku ? ` (${line.sku})` : ''}\n {line.note ? ` — ${line.note}` : ''}\n </li>\n ))}\n </ul>\n ) : null}\n <label>\n {lines.length > 0 ? 'Anything else we should know?' : 'What do you need?'}\n <textarea\n value={values.message}\n onChange={set('message')}\n required={lines.length === 0}\n />\n </label>\n\n <div style={honeypotStyle} aria-hidden=\"true\">\n <label>\n Website\n <input\n name=\"website\"\n tabIndex={-1}\n autoComplete=\"off\"\n value={values.website}\n onChange={set('website')}\n />\n </label>\n </div>\n\n <button\n type=\"submit\"\n disabled={f.submitting || !values.email || (!values.message && lines.length === 0)}\n >\n {f.submitting ? 'Sending…' : 'Request a quote'}\n </button>\n </form>\n );\n}\n"],"mappings":"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","names":["import_react","import_react","import_jsx_runtime"]}
1
+ {"version":3,"sources":["../src/index.tsx","../src/errors.ts","../src/client.ts","../src/hooks.ts","../src/quote-cart.ts","../src/QuoteView.tsx","../../core-cadence/src/cadence.ts","../../core-cadence/src/labels.ts","../src/QuoteRequestForm.tsx"],"sourcesContent":["export {\n QuotesClient,\n createQuotesClient,\n formatMoney,\n scaleLineCharges,\n computeSelectionTotal,\n lineDueAtAcceptance,\n isFreeTrialLine,\n} from './client';\nexport { ThrottleQuotesError } from './errors';\nexport { useQuote, useQuoteCart, useQuoteRequestForm } from './hooks';\nexport type { UseQuoteResult, UseQuoteCartResult, UseQuoteRequestFormResult } from './hooks';\nexport { createQuoteCart } from './quote-cart';\nexport type { QuoteCart, QuoteCartLine, QuoteCartOptions, QuoteCartStorage } from './quote-cart';\nexport { QuoteView } from './QuoteView';\nexport type { QuoteViewProps } from './QuoteView';\nexport { QuoteRequestForm } from './QuoteRequestForm';\nexport type { QuoteRequestFormProps } from './QuoteRequestForm';\nexport type {\n AcceptQuoteInput,\n AcceptQuoteResult,\n BuyerQuote,\n BuyerQuoteStatus,\n ItemSelection,\n PaymentTerms,\n QuoteBranding,\n QuoteComment,\n QuoteItem,\n QuoteItemRecurring,\n QuoteRequestItem,\n QuoteRevision,\n QuotesClientOptions,\n RequestFormConfig,\n SubmitQuoteRequestInput,\n ThrottleEnvelope,\n} from './types';\n","import { ThrottleError, type ThrottleErrorArgs } from '@usethrottle/errors';\n\n/**\n * Every failure from the quote-link routes surfaces as this. The `code` is the\n * API's own error code, so callers can branch on the ones that matter to a\n * buyer flow rather than parsing messages:\n *\n * revision_superseded — a newer revision was issued; `details.currentRevisionId`\n * tells you what to reload\n * quote_expired — the offer lapsed (HTTP 410)\n * invalid_item_selection — locked-line edit, non-optional deselect, or unknown item\n * accept_in_progress — a concurrent accept won; retry shortly\n * not_found — unknown/rotated token, or a draft/archived quote\n */\nexport class ThrottleQuotesError extends ThrottleError {\n constructor(args: {\n code: string;\n message: string;\n statusCode: number;\n details?: unknown;\n responseHeaders?: ThrottleErrorArgs['responseHeaders'];\n }) {\n super(args);\n this.name = 'ThrottleQuotesError';\n }\n\n /** The offer moved on — reload and show the buyer the current revision. */\n get isSuperseded(): boolean {\n return this.code === 'revision_superseded';\n }\n\n get isExpired(): boolean {\n return this.code === 'quote_expired';\n }\n\n /** Revision the server considers current, when it told us (spec §8.1). */\n get currentRevisionId(): string | undefined {\n const d = this.details as { currentRevisionId?: string } | undefined;\n return d?.currentRevisionId;\n }\n}\n","import { ThrottleQuotesError } from './errors';\nimport type {\n AcceptQuoteInput,\n AcceptQuoteResult,\n BuyerQuote,\n QuoteComment,\n QuotesClientOptions,\n RequestFormConfig,\n SubmitQuoteRequestInput,\n ThrottleEnvelope,\n} from './types';\n\nconst DEFAULT_BASE = 'https://api.usethrottle.dev';\n\n/**\n * Headless client for the public quote surfaces. Everything is authorized by\n * the capability token in the URL — there is no API key here, and there must\n * not be: this runs in the buyer's browser.\n */\nexport class QuotesClient {\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof globalThis.fetch;\n private readonly timeoutMs: number;\n\n constructor(opts: QuotesClientOptions = {}) {\n const fetchImpl = opts.fetch ?? globalThis.fetch;\n if (!fetchImpl) throw new Error('QuotesClient: fetch is required');\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE).replace(/\\/+$/, '');\n this.fetchImpl = fetchImpl;\n this.timeoutMs = opts.timeoutMs ?? 30_000;\n }\n\n private async request<T>(method: string, path: string, body?: unknown): Promise<T> {\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);\n try {\n const res = await this.fetchImpl(`${this.baseUrl}${path}`, {\n method,\n headers: body === undefined ? {} : { 'content-type': 'application/json' },\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n signal: ctrl.signal,\n });\n const json = (await res.json().catch(() => ({}))) as ThrottleEnvelope<T>;\n if (!res.ok) {\n throw new ThrottleQuotesError({\n code: json.error?.code ?? 'unknown_error',\n message: json.error?.message ?? `HTTP ${res.status}`,\n statusCode: res.status,\n details: json.error?.details,\n responseHeaders: res.headers,\n });\n }\n return json.data as T;\n } finally {\n clearTimeout(timer);\n }\n }\n\n /** Fetch the quote a buyer link points at. 404s for unknown/rotated tokens. */\n getQuote(token: string): Promise<BuyerQuote> {\n return this.request<BuyerQuote>('GET', `/api/v1/quote-links/${encodeURIComponent(token)}`);\n }\n\n /**\n * Tell the merchant the buyer opened the quote. Fire-and-forget: the server\n * throttles the resulting webhook to one per revision per 6h, and a failed\n * beacon must never break the page.\n */\n async recordView(token: string): Promise<void> {\n try {\n await this.request('POST', `/api/v1/quote-links/${encodeURIComponent(token)}/view`, {});\n } catch {\n /* telemetry only */\n }\n }\n\n addComment(token: string, input: { body: string; authorName?: string; authorEmail?: string }) {\n return this.request<QuoteComment>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/comments`,\n input,\n );\n }\n\n requestRevision(token: string, input: { message?: string; authorName?: string } = {}) {\n return this.request<BuyerQuote>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/request-revision`,\n input,\n );\n }\n\n decline(token: string, input: { reason?: string; authorName?: string } = {}) {\n return this.request<BuyerQuote>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/decline`,\n input,\n );\n }\n\n /**\n * Accept and get the checkout URL to send the buyer to.\n *\n * Re-accepting the same revision is idempotent and replays the same URL.\n * Accepting a superseded revision throws `revision_superseded` carrying\n * `currentRevisionId`; an expired offer throws `quote_expired`.\n */\n accept(token: string, input: AcceptQuoteInput): Promise<AcceptQuoteResult> {\n return this.request<AcceptQuoteResult>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/accept`,\n input,\n );\n }\n\n /**\n * Send a buyer who already accepted back to a LIVE checkout.\n *\n * Checkout sessions expire after 30 minutes, so the URL returned by\n * `accept` is usually dead by the time the buyer returns to the link. This\n * re-opens a session against the same cart when it is (no re-pricing).\n * Throws `checkout_not_resumable` if the quote was never accepted, and\n * `checkout_completed` once it has been paid.\n */\n resumeCheckout(token: string): Promise<{ checkoutUrl: string }> {\n return this.request<{ checkoutUrl: string }>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/checkout`,\n {},\n );\n }\n\n /** URL of the PDF for the revision the buyer is looking at (server 302s to signed storage). */\n pdfUrl(token: string): string {\n return `${this.baseUrl}/api/v1/quote-links/${encodeURIComponent(token)}/pdf`;\n }\n\n // ── RFQ intake ─────────────────────────────────────────────────────────────\n\n getRequestForm(formToken: string): Promise<RequestFormConfig> {\n return this.request<RequestFormConfig>(\n 'GET',\n `/api/v1/quote-request-forms/${encodeURIComponent(formToken)}/public`,\n );\n }\n\n /**\n * Submit an RFQ. The response is deliberately thin (`{ submitted: true }`) —\n * anonymous callers learn no internal identifiers. A submission caught by\n * the honeypot returns the same shape without creating anything, so a bot\n * cannot tell it was detected; treat success as \"accepted\", not \"created\".\n */\n submitRequest(formToken: string, input: SubmitQuoteRequestInput): Promise<{ submitted: boolean }> {\n return this.request<{ submitted: boolean }>('POST', '/api/v1/quote-requests', {\n formToken,\n ...input,\n });\n }\n}\n\nexport function createQuotesClient(opts: QuotesClientOptions = {}): QuotesClient {\n return new QuotesClient(opts);\n}\n\n// ── money formatting ─────────────────────────────────────────────────────────\n\n/** Minor units → a display string, e.g. (104000, 'USD') → \"$1,040.00\". */\nexport function formatMoney(minorUnits: number, currency: string, locale?: string): string {\n return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(minorUnits / 100);\n}\n\n/**\n * Scale a line's quoted tax/discount to an edited quantity.\n *\n * MUST match `scaleLineCharges` in @commerce/quotes — the server recomputes\n * the accepted total with this rule, and any divergence shows the buyer one\n * total and charges another.\n */\nexport function scaleLineCharges(\n item: { quantity: number; taxAmount: number; discountAmount: number },\n quantity: number,\n): { taxAmount: number; discountAmount: number } {\n const base = item.quantity;\n if (!base || base <= 0 || quantity === base) {\n return { taxAmount: item.taxAmount, discountAmount: item.discountAmount };\n }\n const ratio = quantity / base;\n return {\n taxAmount: Math.round(item.taxAmount * ratio),\n discountAmount: Math.round(item.discountAmount * ratio),\n };\n}\n\n/** A subscription line with a free trial: nothing is charged for it at acceptance. */\nexport function isFreeTrialLine(item: { recurring?: { trialDays?: number | null } | null }): boolean {\n return (item.recurring?.trialDays ?? 0) > 0;\n}\n\n/**\n * What one line charges at acceptance, at `quantity`. MUST match\n * `lineDueAtAcceptance` in @commerce/quotes (shared test vectors).\n */\nexport function lineDueAtAcceptance(\n item: {\n unitPrice: number;\n quantity: number;\n taxAmount: number;\n discountAmount: number;\n recurring?: { trialDays?: number | null } | null;\n },\n quantity: number,\n): number {\n const { taxAmount, discountAmount } = scaleLineCharges(item, quantity);\n return (isFreeTrialLine(item) ? 0 : item.unitPrice * quantity) + taxAmount - discountAmount;\n}\n\n/** Effective total for a selection, using the same math the server will apply. */\nexport function computeSelectionTotal(\n items: Array<{\n id: string;\n unitPrice: number;\n quantity: number;\n taxAmount: number;\n discountAmount: number;\n optional: boolean;\n recurring?: { trialDays?: number | null } | null;\n }>,\n selections: Record<string, { selected: boolean; quantity: number }>,\n shippingTotal = 0,\n): number {\n let total = shippingTotal;\n for (const item of items) {\n const state = selections[item.id];\n if (item.optional && state && !state.selected) continue;\n const qty = state?.quantity ?? item.quantity;\n total += lineDueAtAcceptance(item, qty);\n }\n return total;\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { QuotesClient, computeSelectionTotal, createQuotesClient } from './client';\nimport { ThrottleQuotesError } from './errors';\nimport { createQuoteCart, type QuoteCart, type QuoteCartLine, type QuoteCartOptions } from './quote-cart';\nimport type {\n AcceptQuoteInput,\n BuyerQuote,\n ItemSelection,\n QuoteRequestItem,\n QuotesClientOptions,\n RequestFormConfig,\n SubmitQuoteRequestInput,\n} from './types';\n\nexport interface UseQuoteResult {\n quote: BuyerQuote | null;\n loading: boolean;\n error: ThrottleQuotesError | null;\n /** Re-fetch — call after a `revision_superseded` to show the current offer. */\n refresh: () => Promise<void>;\n /** Buyer's current line choices, keyed by item id. */\n selections: Record<string, { selected: boolean; quantity: number }>;\n setSelected: (itemId: string, selected: boolean) => void;\n setQuantity: (itemId: string, quantity: number) => void;\n /** Live total for the current selections, using the server's own math. */\n total: number;\n accept: (\n input: Omit<AcceptQuoteInput, 'revisionId' | 'itemSelections'>,\n ) => Promise<{ checkoutUrl: string }>;\n /** Live checkout URL for a buyer returning to an already-accepted quote —\n * the one minted at acceptance has usually expired by then. */\n resumeCheckout: () => Promise<{ checkoutUrl: string }>;\n requestRevision: (message: string, authorName?: string) => Promise<void>;\n decline: (reason?: string, authorName?: string) => Promise<void>;\n addComment: (body: string, authorName?: string) => Promise<void>;\n pdfUrl: string;\n submitting: boolean;\n}\n\n/**\n * Everything the buyer quote page needs.\n *\n * Fires the view beacon once per mount, keeps line selections in local state,\n * and recomputes the total with the same rule the server applies at\n * acceptance — so the number the buyer sees is the number they are charged.\n */\nexport function useQuote(token: string, options: QuotesClientOptions = {}): UseQuoteResult {\n const client = useMemo(\n () => new QuotesClient(options),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [options.baseUrl, options.timeoutMs],\n );\n const [quote, setQuote] = useState<BuyerQuote | null>(null);\n const [loading, setLoading] = useState(true);\n const [submitting, setSubmitting] = useState(false);\n const [error, setError] = useState<ThrottleQuotesError | null>(null);\n const [selections, setSelections] = useState<Record<string, { selected: boolean; quantity: number }>>({});\n const beaconSent = useRef(false);\n\n const load = useCallback(async () => {\n setLoading(true);\n try {\n const q = await client.getQuote(token);\n setQuote(q);\n setError(null);\n // Seed selections from the revision's own defaults so a re-issue that\n // adds or locks lines resets the buyer's choices to the new offer.\n const next: Record<string, { selected: boolean; quantity: number }> = {};\n for (const item of q.currentRevision?.items ?? []) {\n next[item.id] = { selected: item.optional ? item.optionalSelected : true, quantity: item.quantity };\n }\n setSelections(next);\n } catch (e) {\n setError(e as ThrottleQuotesError);\n } finally {\n setLoading(false);\n }\n }, [client, token]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n useEffect(() => {\n if (!quote || beaconSent.current) return;\n beaconSent.current = true;\n void client.recordView(token);\n }, [client, quote, token]);\n\n const setSelected = useCallback((itemId: string, selected: boolean) => {\n setSelections((prev) => ({ ...prev, [itemId]: { ...(prev[itemId] ?? { quantity: 1 }), selected } }));\n }, []);\n\n const setQuantity = useCallback((itemId: string, quantity: number) => {\n setSelections((prev) => ({\n ...prev,\n [itemId]: { ...(prev[itemId] ?? { selected: true }), quantity: Math.max(1, Math.floor(quantity) || 1) },\n }));\n }, []);\n\n const total = useMemo(\n () =>\n computeSelectionTotal(\n quote?.currentRevision?.items ?? [],\n selections,\n quote?.currentRevision?.shippingTotal ?? 0,\n ),\n [quote, selections],\n );\n\n /** Only send selections the rep actually allows the buyer to change. */\n const buildSelections = useCallback((): ItemSelection[] => {\n const items = quote?.currentRevision?.items ?? [];\n const out: ItemSelection[] = [];\n for (const item of items) {\n const state = selections[item.id];\n if (!state) continue;\n const sel: ItemSelection = { itemId: item.id };\n let include = false;\n if (item.optional && state.selected !== item.optionalSelected) {\n sel.selected = state.selected;\n include = true;\n }\n if (item.qtyEditable && state.quantity !== item.quantity) {\n sel.quantity = state.quantity;\n include = true;\n }\n if (include) out.push(sel);\n }\n return out;\n }, [quote, selections]);\n\n const guard = useCallback(\n async <T,>(fn: () => Promise<T>): Promise<T> => {\n setSubmitting(true);\n try {\n const result = await fn();\n setError(null);\n return result;\n } catch (e) {\n const err = e as ThrottleQuotesError;\n setError(err);\n // A superseded offer means our copy is stale — pull the current one so\n // the buyer sees what they can actually accept (spec §8.1).\n if (err.isSuperseded) void load();\n throw err;\n } finally {\n setSubmitting(false);\n }\n },\n [load],\n );\n\n const accept = useCallback(\n async (input: Omit<AcceptQuoteInput, 'revisionId' | 'itemSelections'>) => {\n const revisionId = quote?.currentRevision?.id;\n if (!revisionId) throw new Error('useQuote: no current revision to accept');\n const itemSelections = buildSelections();\n const res = await guard(() =>\n client.accept(token, {\n ...input,\n revisionId,\n ...(itemSelections.length ? { itemSelections } : {}),\n }),\n );\n return { checkoutUrl: res.checkoutUrl };\n },\n [buildSelections, client, guard, quote, token],\n );\n\n const resumeCheckout = useCallback(\n async () => guard(() => client.resumeCheckout(token)),\n [client, guard, token],\n );\n\n const requestRevision = useCallback(\n async (message: string, authorName?: string) => {\n const q = await guard(() => client.requestRevision(token, { message, authorName }));\n setQuote(q);\n },\n [client, guard, token],\n );\n\n const decline = useCallback(\n async (reason?: string, authorName?: string) => {\n const q = await guard(() => client.decline(token, { reason, authorName }));\n setQuote(q);\n },\n [client, guard, token],\n );\n\n const addComment = useCallback(\n async (body: string, authorName?: string) => {\n await guard(() => client.addComment(token, { body, authorName }));\n await load();\n },\n [client, guard, load, token],\n );\n\n return {\n quote,\n loading,\n error,\n refresh: load,\n selections,\n setSelected,\n setQuantity,\n total,\n accept,\n resumeCheckout,\n requestRevision,\n decline,\n addComment,\n pdfUrl: client.pdfUrl(token),\n submitting,\n };\n}\n\nexport interface UseQuoteRequestFormResult {\n config: RequestFormConfig | null;\n loading: boolean;\n error: ThrottleQuotesError | null;\n submitting: boolean;\n submitted: boolean;\n submit: (input: SubmitQuoteRequestInput) => Promise<void>;\n /** Mount time; pass through so the API's minimum-fill-time check passes. */\n formLoadedAt: number;\n}\n\n/** Loads a hosted RFQ form's config and submits it, with bot fields handled. */\nexport function useQuoteRequestForm(\n formToken: string,\n options: QuotesClientOptions = {},\n): UseQuoteRequestFormResult {\n const client = useMemo(\n () => new QuotesClient(options),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [options.baseUrl, options.timeoutMs],\n );\n const [config, setConfig] = useState<RequestFormConfig | null>(null);\n const [loading, setLoading] = useState(true);\n const [submitting, setSubmitting] = useState(false);\n const [submitted, setSubmitted] = useState(false);\n const [error, setError] = useState<ThrottleQuotesError | null>(null);\n const formLoadedAt = useRef(Date.now()).current;\n\n useEffect(() => {\n let cancelled = false;\n (async () => {\n try {\n const cfg = await client.getRequestForm(formToken);\n if (!cancelled) setConfig(cfg);\n } catch (e) {\n if (!cancelled) setError(e as ThrottleQuotesError);\n } finally {\n if (!cancelled) setLoading(false);\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [client, formToken]);\n\n const submit = useCallback(\n async (input: SubmitQuoteRequestInput) => {\n setSubmitting(true);\n try {\n await client.submitRequest(formToken, { formLoadedAt, ...input });\n setSubmitted(true);\n setError(null);\n } catch (e) {\n setError(e as ThrottleQuotesError);\n throw e;\n } finally {\n setSubmitting(false);\n }\n },\n [client, formLoadedAt, formToken],\n );\n\n return { config, loading, error, submitting, submitted, submit, formLoadedAt };\n}\n\nexport { createQuotesClient };\n\nexport interface UseQuoteCartResult {\n lines: QuoteCartLine[];\n /** Total quantity across lines — for a header badge. */\n count: number;\n add: (item: QuoteRequestItem) => void;\n addMany: (items: QuoteRequestItem[]) => void;\n update: (key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>) => void;\n remove: (key: string) => void;\n clear: () => void;\n /** The underlying cart, for use outside React (an Add to Quote button in plain JS). */\n cart: QuoteCart;\n}\n\n/**\n * A quote cart the buyer fills while browsing, then submits as one request.\n *\n * The cart persists across page views, so an Add to Quote button on a product\n * page and a Request quote page elsewhere on the site see the same lines.\n * Pass the result of `toRequestItems()` (or `lines`) to\n * `useQuoteRequestForm().submit` as `items`.\n */\nexport function useQuoteCart(options: QuoteCartOptions = {}): UseQuoteCartResult {\n const cart = useMemo(\n () => createQuoteCart(options),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [options.storageKey, options.maxLines],\n );\n // Seeded empty rather than from storage: reading localStorage during render\n // makes the server and the first client render disagree, which is a\n // hydration mismatch. The effect below fills it in immediately after mount.\n const [lines, setLines] = useState<QuoteCartLine[]>([]);\n\n useEffect(() => {\n setLines(cart.read());\n return cart.subscribe(setLines);\n }, [cart]);\n\n return {\n lines,\n count: lines.reduce((sum, line) => sum + line.quantity, 0),\n add: useCallback((item: QuoteRequestItem) => void cart.add(item), [cart]),\n addMany: useCallback((items: QuoteRequestItem[]) => void cart.addMany(items), [cart]),\n update: useCallback(\n (key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>) => void cart.update(key, patch),\n [cart],\n ),\n remove: useCallback((key: string) => void cart.remove(key), [cart]),\n clear: useCallback(() => cart.clear(), [cart]),\n cart,\n };\n}\n","import type { QuoteRequestItem } from './types';\n\n/**\n * A quote cart: the buyer's running list of things to be quoted, built up\n * while they browse.\n *\n * Throttle stores no catalog, so the storefront owns product data and hands\n * lines in — the same arrangement as the shopping cart. Nothing here is sent\n * until `read()` is submitted through `QuotesClient.submitRequest`, and the\n * amounts a storefront attaches are the buyer's TARGET price, never a price\n * we will honour.\n *\n * Lines are keyed by `sku ?? referenceId ?? name`, so adding the same product\n * twice increases its quantity instead of stacking duplicate rows.\n */\n\nexport interface QuoteCartLine extends QuoteRequestItem {\n /** Stable identity for updates and removals. */\n key: string;\n quantity: number;\n}\n\nexport interface QuoteCartStorage {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nexport interface QuoteCartOptions {\n /** Namespacing key, so two storefronts on one origin cannot collide. */\n storageKey?: string;\n /**\n * Where the cart survives page navigation. Defaults to `localStorage` in a\n * browser and to memory everywhere else (SSR, tests) — a cart that throws\n * during server rendering would take the whole page down.\n */\n storage?: QuoteCartStorage | null;\n /** Refuse to grow past this many distinct lines. The API caps at 100. */\n maxLines?: number;\n}\n\nconst DEFAULT_KEY = 'throttle:quote-cart';\nconst DEFAULT_MAX_LINES = 100;\n\nfunction memoryStorage(): QuoteCartStorage {\n const map = new Map<string, string>();\n return {\n getItem: (key) => map.get(key) ?? null,\n setItem: (key, value) => void map.set(key, value),\n removeItem: (key) => void map.delete(key),\n };\n}\n\nfunction defaultStorage(): QuoteCartStorage {\n try {\n if (typeof globalThis !== 'undefined' && (globalThis as any).localStorage) {\n return (globalThis as any).localStorage as QuoteCartStorage;\n }\n } catch {\n // Safari in private mode throws on access rather than returning null.\n }\n return memoryStorage();\n}\n\nfunction lineKey(item: QuoteRequestItem): string {\n const raw = item.sku?.trim() || item.referenceId?.trim() || item.name.trim();\n return raw.toLowerCase();\n}\n\nfunction normalizeQuantity(value: number | null | undefined): number {\n const n = Math.floor(Number(value ?? 1));\n return Number.isFinite(n) && n > 0 ? n : 1;\n}\n\nexport interface QuoteCart {\n /** Current lines, oldest first. */\n read(): QuoteCartLine[];\n /** Total quantity across lines — what a header badge should show. */\n count(): number;\n /** Add a line, merging quantities into an existing one with the same key. */\n add(item: QuoteRequestItem): QuoteCartLine[];\n /** Add several at once (a \"quote this whole cart\" button). */\n addMany(items: QuoteRequestItem[]): QuoteCartLine[];\n /** Change any field of a line. A quantity below 1 removes it. */\n update(key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>): QuoteCartLine[];\n remove(key: string): QuoteCartLine[];\n clear(): void;\n /** Subscribe to every change. Returns an unsubscribe function. */\n subscribe(listener: (lines: QuoteCartLine[]) => void): () => void;\n /** The `items` payload for `QuotesClient.submitRequest`. */\n toRequestItems(): QuoteRequestItem[];\n}\n\nexport function createQuoteCart(options: QuoteCartOptions = {}): QuoteCart {\n const storageKey = options.storageKey ?? DEFAULT_KEY;\n const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n const storage = options.storage === null ? memoryStorage() : (options.storage ?? defaultStorage());\n const listeners = new Set<(lines: QuoteCartLine[]) => void>();\n\n function load(): QuoteCartLine[] {\n try {\n const raw = storage.getItem(storageKey);\n if (!raw) return [];\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n // Storage is buyer-writable — anything shaped wrong is dropped rather\n // than trusted into a submission.\n return parsed\n .filter((line: any) => line && typeof line.name === 'string' && line.name.trim())\n .map((line: any) => ({\n ...line,\n key: typeof line.key === 'string' && line.key ? line.key : lineKey(line),\n quantity: normalizeQuantity(line.quantity),\n }))\n .slice(0, maxLines);\n } catch {\n return [];\n }\n }\n\n function save(lines: QuoteCartLine[]): QuoteCartLine[] {\n try {\n storage.setItem(storageKey, JSON.stringify(lines));\n } catch {\n // Quota or a disabled store — the cart still works for this page view.\n }\n for (const listener of listeners) listener(lines);\n return lines;\n }\n\n return {\n read: load,\n\n count() {\n return load().reduce((sum, line) => sum + line.quantity, 0);\n },\n\n add(item) {\n return this.addMany([item]);\n },\n\n addMany(items) {\n const lines = load();\n for (const item of items) {\n if (!item?.name?.trim()) continue;\n const key = lineKey(item);\n const existing = lines.find((line) => line.key === key);\n if (existing) {\n existing.quantity += normalizeQuantity(item.quantity);\n // Later detail wins — a product page knows more than a listing card.\n if (item.note) existing.note = item.note;\n if (item.targetUnitPrice != null) existing.targetUnitPrice = item.targetUnitPrice;\n continue;\n }\n if (lines.length >= maxLines) break;\n lines.push({ ...item, key, quantity: normalizeQuantity(item.quantity) });\n }\n return save(lines);\n },\n\n update(key, patch) {\n const lines = load();\n const index = lines.findIndex((line) => line.key === key);\n if (index === -1) return lines;\n if (patch.quantity != null && Math.floor(Number(patch.quantity)) < 1) {\n lines.splice(index, 1);\n return save(lines);\n }\n lines[index] = {\n ...lines[index],\n ...patch,\n key,\n quantity: normalizeQuantity(patch.quantity ?? lines[index].quantity),\n };\n return save(lines);\n },\n\n remove(key) {\n return save(load().filter((line) => line.key !== key));\n },\n\n clear() {\n try {\n storage.removeItem(storageKey);\n } catch {\n // Ignore — the notification below still resets any subscriber.\n }\n for (const listener of listeners) listener([]);\n },\n\n subscribe(listener) {\n listeners.add(listener);\n return () => void listeners.delete(listener);\n },\n\n toRequestItems() {\n return load().map(({ key: _key, ...item }) => item);\n },\n };\n}\n","import { useState, type CSSProperties, type ReactNode } from 'react';\nimport { recurringTermsLabel } from '@core/cadence';\nimport { formatMoney, lineDueAtAcceptance } from './client';\nimport { useQuote } from './hooks';\nimport type { QuotesClientOptions } from './types';\n\nexport interface QuoteViewProps {\n /** The `qtl_…` token from the buyer's link. */\n token: string;\n options?: QuotesClientOptions;\n /**\n * Where to send the buyer after acceptance. Defaults to assigning\n * `window.location.href`, which is what the hosted page does.\n */\n onCheckout?: (checkoutUrl: string) => void;\n /** Rendered instead of the built-in markup while loading. */\n loadingFallback?: ReactNode;\n className?: string;\n}\n\nconst row: CSSProperties = { display: 'flex', justifyContent: 'space-between', gap: 16, padding: '8px 0' };\nconst muted: CSSProperties = { color: '#666', fontSize: 13 };\n\n/**\n * Drop-in buyer quote page: line items with optional toggles and quantity\n * editing, live totals, revision history, comment thread, and the\n * accept / request-changes / decline actions.\n *\n * Intentionally unstyled beyond a readable default — it is meant to be\n * restyled or replaced. `useQuote` carries all the behaviour if you want to\n * build your own presentation.\n */\nexport function QuoteView({ token, options, onCheckout, loadingFallback, className }: QuoteViewProps) {\n const q = useQuote(token, options ?? {});\n const [name, setName] = useState('');\n const [email, setEmail] = useState('');\n const [showAccept, setShowAccept] = useState(false);\n const [comment, setComment] = useState('');\n\n if (q.loading) return <>{loadingFallback ?? <p>Loading quote…</p>}</>;\n if (!q.quote) {\n return <p>This quote link is no longer available.</p>;\n }\n\n const quote = q.quote;\n const rev = quote.currentRevision;\n const currency = quote.currency;\n const ordered = quote.status === 'ordered';\n const accepted = quote.status === 'accepted';\n const canAct = quote.status === 'ready';\n // The subscription lines the buyer is keeping (an unticked optional plan drops out).\n const recurringKept = rev\n ? rev.items.filter((i) => i.recurring && (!i.optional || (q.selections[i.id]?.selected ?? true)))\n : [];\n\n const handleResume = async () => {\n const { checkoutUrl } = await q.resumeCheckout();\n if (onCheckout) onCheckout(checkoutUrl);\n else if (typeof window !== 'undefined') window.location.href = checkoutUrl;\n };\n\n const handleAccept = async () => {\n const { checkoutUrl } = await q.accept({ acceptedByName: name, acceptedByEmail: email });\n if (onCheckout) onCheckout(checkoutUrl);\n else if (typeof window !== 'undefined') window.location.href = checkoutUrl;\n };\n\n return (\n <div className={className}>\n <header>\n {quote.branding?.displayName ? <p style={muted}>{quote.branding.displayName}</p> : null}\n <h1>Quote {quote.quoteNumber}</h1>\n <p style={muted}>{statusLabel(quote.status)}</p>\n {quote.buyerReference ? <p style={muted}>Your reference: {quote.buyerReference}</p> : null}\n {rev ? (\n <p style={muted}>\n Revision {rev.revisionNumber}\n {rev.expiresAt ? ` · valid until ${new Date(rev.expiresAt).toLocaleDateString()}` : null}\n {' · '}\n <a href={q.pdfUrl}>Download PDF</a>\n </p>\n ) : null}\n </header>\n\n {q.error ? (\n <p role=\"alert\">\n {q.error.isSuperseded\n ? 'This quote was updated — showing the latest version.'\n : q.error.message}\n </p>\n ) : null}\n\n {rev ? (\n <table style={{ width: '100%', borderCollapse: 'collapse' }}>\n <thead>\n <tr>\n <th align=\"left\">Item</th>\n <th align=\"right\">Unit price</th>\n <th align=\"right\">Qty</th>\n <th align=\"right\">Total</th>\n </tr>\n </thead>\n <tbody>\n {rev.items.map((item) => {\n const state = q.selections[item.id] ?? { selected: true, quantity: item.quantity };\n const included = item.optional ? state.selected : true;\n return (\n <tr key={item.id}>\n <td>\n {item.optional ? (\n <label>\n <input\n type=\"checkbox\"\n checked={state.selected}\n disabled={!canAct}\n onChange={(e) => q.setSelected(item.id, e.target.checked)}\n />{' '}\n {item.name} <span style={muted}>optional</span>\n </label>\n ) : (\n item.name\n )}\n {item.description ? <div style={muted}>{item.description}</div> : null}\n {item.recurring ? (\n <div style={muted}>\n Subscription · {recurringTermsLabel(item.recurring, formatMoney(item.unitPrice * state.quantity, currency))}\n </div>\n ) : null}\n </td>\n <td align=\"right\">{formatMoney(item.unitPrice, currency)}</td>\n <td align=\"right\">\n {item.qtyEditable && canAct ? (\n <input\n type=\"number\"\n min={1}\n value={state.quantity}\n disabled={!included}\n aria-label={`Quantity for ${item.name}`}\n onChange={(e) => q.setQuantity(item.id, Number(e.target.value))}\n style={{ width: 64 }}\n />\n ) : (\n state.quantity\n )}\n </td>\n <td align=\"right\">\n {formatMoney(included ? lineDueAtAcceptance(item, state.quantity) : 0, currency)}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n ) : null}\n\n {rev ? (\n <section>\n <div style={row}>\n <strong>{recurringKept.length > 0 ? 'Due at acceptance' : 'Total'}</strong>\n <strong>{formatMoney(q.total, currency)}</strong>\n </div>\n {recurringKept.length > 0 ? (\n <div>\n <p style={muted}>Recurring after</p>\n {recurringKept.map((i) => (\n <p key={i.id} style={muted}>\n {i.name}: {recurringTermsLabel(i.recurring!, formatMoney(i.unitPrice * (q.selections[i.id]?.quantity ?? i.quantity), currency))}\n </p>\n ))}\n </div>\n ) : null}\n <p style={muted}>{termsLabel(rev.paymentTerms, rev.depositAmount, currency)}</p>\n {rev.leadTime ? <p style={muted}>Lead time: {rev.leadTime}</p> : null}\n {rev.buyerNote ? <blockquote>{rev.buyerNote}</blockquote> : null}\n </section>\n ) : null}\n\n {ordered ? (\n <p>\n Ordered{quote.orderId ? ` — order ${quote.orderId}` : ''}. Thank you.\n </p>\n ) : accepted ? (\n // Accepted but not yet paid. The checkout session minted at acceptance\n // expires in 30 minutes, so this asks the server for a live one rather\n // than linking to a URL that is probably dead.\n <section>\n <p>You accepted this quote — payment is not finished yet.</p>\n <button type=\"button\" disabled={q.submitting} onClick={() => void handleResume()}>\n Continue to checkout\n </button>\n </section>\n ) : canAct ? (\n <section>\n {showAccept ? (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n void handleAccept();\n }}\n >\n <label>\n Your name\n <input value={name} onChange={(e) => setName(e.target.value)} required />\n </label>\n <label>\n Email\n <input type=\"email\" value={email} onChange={(e) => setEmail(e.target.value)} required />\n </label>\n <button type=\"submit\" disabled={q.submitting || !name || !email}>\n Confirm &amp; continue to payment\n </button>\n </form>\n ) : (\n <button type=\"button\" onClick={() => setShowAccept(true)}>\n Accept &amp; checkout\n </button>\n )}\n <button\n type=\"button\"\n disabled={q.submitting}\n onClick={() => void q.requestRevision('Please revise this quote.', name || undefined)}\n >\n Request changes\n </button>\n <button type=\"button\" disabled={q.submitting} onClick={() => void q.decline(undefined, name || undefined)}>\n Decline\n </button>\n </section>\n ) : null}\n\n {quote.revisionHistory.length > 0 ? (\n <details>\n <summary>Previous versions ({quote.revisionHistory.length})</summary>\n <ul>\n {quote.revisionHistory.map((r) => (\n <li key={r.id}>\n Revision {r.revisionNumber} — {formatMoney(r.total, currency)}\n {r.changeSummary ? ` · ${r.changeSummary}` : ''}\n </li>\n ))}\n </ul>\n </details>\n ) : null}\n\n <section>\n <h2>Questions</h2>\n {quote.comments.map((c) => (\n <p key={c.id}>\n <strong>{c.authorName ?? (c.authorType === 'merchant' ? 'Seller' : 'You')}:</strong> {c.body}\n </p>\n ))}\n <form\n onSubmit={(e) => {\n e.preventDefault();\n const body = comment.trim();\n if (!body) return;\n setComment('');\n void q.addComment(body, name || undefined);\n }}\n >\n <input\n value={comment}\n onChange={(e) => setComment(e.target.value)}\n placeholder=\"Ask a question…\"\n aria-label=\"Ask a question\"\n />\n <button type=\"submit\" disabled={q.submitting || !comment.trim()}>\n Send\n </button>\n </form>\n </section>\n </div>\n );\n}\n\nfunction statusLabel(status: string): string {\n switch (status) {\n case 'in_progress':\n return 'We are preparing your quote';\n case 'ready':\n return 'Ready for your review';\n case 'being_revised':\n return 'We are revising this quote';\n case 'accepted':\n return 'Accepted — completing payment';\n case 'ordered':\n return 'Ordered';\n case 'declined':\n return 'Declined';\n case 'expired':\n return 'This quote has expired — request updated pricing';\n default:\n return status;\n }\n}\n\nfunction termsLabel(\n terms: { mode: string; netN?: number; balanceNetN?: number },\n depositAmount: number | null,\n currency: string,\n): string {\n if (terms.mode === 'net_terms') return `Payment terms: Net ${terms.netN}`;\n if (terms.mode === 'deposit_balance') {\n const dep = depositAmount != null ? formatMoney(depositAmount, currency) : 'a deposit';\n return `${dep} due today, balance Net ${terms.balanceNetN}`;\n }\n return 'Payment in full';\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","import { isIntervalUnit, isNamedInterval, type CadenceLike, type NamedInterval } from './cadence';\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}\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\". `each` is the amount billed every period (renewal price ×\n * seats), already formatted, so each surface keeps its own money style.\n */\nexport function recurringTermsLabel(terms: RecurringTermsLike, each: string): string {\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","import { useState, type CSSProperties, type ReactNode } from 'react';\nimport { useQuoteRequestForm } from './hooks';\nimport type { QuoteRequestItem, QuotesClientOptions } from './types';\n\nexport interface QuoteRequestFormProps {\n /** The `qrf_…` token from the merchant's hosted form URL. */\n formToken: string;\n options?: QuotesClientOptions;\n /**\n * Turnstile token, when the form requires it. Render the widget yourself and\n * pass its token through — this package deliberately pulls in no Cloudflare\n * script. If `config.fields.requireTurnstile` is set and this is empty, the\n * API fails the submission closed.\n */\n turnstileToken?: string;\n /**\n * Lines to submit with the request — typically `useQuoteCart().lines` after\n * the buyer has been adding products while browsing. When present, the\n * written message is optional.\n */\n items?: QuoteRequestItem[];\n onSubmitted?: () => void;\n loadingFallback?: ReactNode;\n className?: string;\n}\n\n/** Off-screen but still focusable-free; bots fill it, humans never see it. */\nconst honeypotStyle: CSSProperties = {\n position: 'absolute',\n width: 1,\n height: 1,\n padding: 0,\n margin: -1,\n overflow: 'hidden',\n clip: 'rect(0 0 0 0)',\n whiteSpace: 'nowrap',\n border: 0,\n};\n\n/**\n * Hosted RFQ intake form.\n *\n * Bot protection is wired in for you: the honeypot field and the mount-time\n * anchor that feeds the API's minimum-fill-time check. A honeypot hit returns\n * a success-shaped response and creates nothing, so `onSubmitted` fires either\n * way — by design, a bot must not learn it was caught.\n */\nexport function QuoteRequestForm({\n formToken,\n options,\n turnstileToken,\n items,\n onSubmitted,\n loadingFallback,\n className,\n}: QuoteRequestFormProps) {\n const f = useQuoteRequestForm(formToken, options ?? {});\n // Lines the host collected while the buyer browsed (see `useQuoteCart`).\n // With any of these present the written message becomes optional: the ask\n // is the list.\n const lines = items ?? [];\n const [values, setValues] = useState({\n email: '',\n name: '',\n companyName: '',\n phone: '',\n buyerReference: '',\n message: '',\n website: '', // honeypot\n });\n\n const set = (k: keyof typeof values) => (e: { target: { value: string } }) =>\n setValues((prev) => ({ ...prev, [k]: e.target.value }));\n\n if (f.loading) return <>{loadingFallback ?? <p>Loading…</p>}</>;\n if (!f.config) return <p>This request form is not available.</p>;\n\n if (f.submitted) {\n const redirect = f.config.redirectUrl;\n // Re-check the scheme client-side even though the API rejects non-http(s)\n // on write — a stale row must not be able to script the host page.\n if (redirect && /^https?:\\/\\//i.test(redirect) && typeof window !== 'undefined') {\n window.location.href = redirect;\n }\n return <p>{f.config.successMessage ?? 'Thanks — we will be in touch shortly.'}</p>;\n }\n\n const fields = f.config.fields ?? {};\n\n return (\n <form\n className={className}\n onSubmit={(e) => {\n e.preventDefault();\n void f\n .submit({\n email: values.email,\n message: values.message || null,\n ...(lines.length > 0 ? { items: lines } : {}),\n name: values.name || null,\n companyName: fields.companyName ? values.companyName || null : null,\n phone: fields.phone ? values.phone || null : null,\n buyerReference: fields.buyerReference ? values.buyerReference || null : null,\n website: values.website || null,\n ...(turnstileToken ? { turnstileToken } : {}),\n })\n .then(() => onSubmitted?.())\n .catch(() => {\n /* surfaced via f.error */\n });\n }}\n >\n {f.config.title ? <h1>{f.config.title}</h1> : null}\n {f.config.introText ? <p>{f.config.introText}</p> : null}\n {f.error ? <p role=\"alert\">{f.error.message}</p> : null}\n\n <label>\n Email\n <input type=\"email\" value={values.email} onChange={set('email')} required />\n </label>\n <label>\n Name\n <input value={values.name} onChange={set('name')} />\n </label>\n {fields.companyName ? (\n <label>\n Company\n <input value={values.companyName} onChange={set('companyName')} />\n </label>\n ) : null}\n {fields.phone ? (\n <label>\n Phone\n <input value={values.phone} onChange={set('phone')} />\n </label>\n ) : null}\n {fields.buyerReference ? (\n <label>\n Your reference #\n <input value={values.buyerReference} onChange={set('buyerReference')} />\n </label>\n ) : null}\n {lines.length > 0 ? (\n <ul aria-label=\"Items on this request\">\n {lines.map((line, index) => (\n <li key={line.sku ?? line.referenceId ?? `${line.name}-${index}`}>\n {line.quantity ?? 1} × {line.name}\n {line.sku ? ` (${line.sku})` : ''}\n {line.note ? ` — ${line.note}` : ''}\n </li>\n ))}\n </ul>\n ) : null}\n <label>\n {lines.length > 0 ? 'Anything else we should know?' : 'What do you need?'}\n <textarea\n value={values.message}\n onChange={set('message')}\n required={lines.length === 0}\n />\n </label>\n\n <div style={honeypotStyle} aria-hidden=\"true\">\n <label>\n Website\n <input\n name=\"website\"\n tabIndex={-1}\n autoComplete=\"off\"\n value={values.website}\n onChange={set('website')}\n />\n </label>\n </div>\n\n <button\n type=\"submit\"\n disabled={f.submitting || !values.email || (!values.message && lines.length === 0)}\n >\n {f.submitting ? 'Sending…' : 'Request a quote'}\n </button>\n </form>\n );\n}\n"],"mappings":"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","names":["import_react","import_react","import_jsx_runtime"]}
package/dist/index.d.cts CHANGED
@@ -16,6 +16,18 @@ type PaymentTerms = {
16
16
  };
17
17
  balanceNetN: number;
18
18
  };
19
+ /** A subscription line's terms (Throttle creates one subscription per line when the quote is paid). */
20
+ interface QuoteItemRecurring {
21
+ plan: string;
22
+ planName: string | null;
23
+ interval: string;
24
+ intervalUnit: string;
25
+ intervalCount: number;
26
+ /** Set on an installment plan: exactly this many payments. */
27
+ totalPayments: number | null;
28
+ /** 0 when there is no free trial. A free-trial line charges nothing at acceptance. */
29
+ trialDays: number;
30
+ }
19
31
  interface QuoteItem {
20
32
  id: string;
21
33
  type: string;
@@ -35,6 +47,8 @@ interface QuoteItem {
35
47
  qtyEditable: boolean;
36
48
  imageUrl: string | null;
37
49
  position: number;
50
+ /** Subscription terms, or null for a one-time line. `unitPrice` is the per-period price per seat. */
51
+ recurring: QuoteItemRecurring | null;
38
52
  }
39
53
  interface QuoteRevision {
40
54
  id: string;
@@ -261,6 +275,25 @@ declare function scaleLineCharges(item: {
261
275
  taxAmount: number;
262
276
  discountAmount: number;
263
277
  };
278
+ /** A subscription line with a free trial: nothing is charged for it at acceptance. */
279
+ declare function isFreeTrialLine(item: {
280
+ recurring?: {
281
+ trialDays?: number | null;
282
+ } | null;
283
+ }): boolean;
284
+ /**
285
+ * What one line charges at acceptance, at `quantity`. MUST match
286
+ * `lineDueAtAcceptance` in @commerce/quotes (shared test vectors).
287
+ */
288
+ declare function lineDueAtAcceptance(item: {
289
+ unitPrice: number;
290
+ quantity: number;
291
+ taxAmount: number;
292
+ discountAmount: number;
293
+ recurring?: {
294
+ trialDays?: number | null;
295
+ } | null;
296
+ }, quantity: number): number;
264
297
  /** Effective total for a selection, using the same math the server will apply. */
265
298
  declare function computeSelectionTotal(items: Array<{
266
299
  id: string;
@@ -269,6 +302,9 @@ declare function computeSelectionTotal(items: Array<{
269
302
  taxAmount: number;
270
303
  discountAmount: number;
271
304
  optional: boolean;
305
+ recurring?: {
306
+ trialDays?: number | null;
307
+ } | null;
272
308
  }>, selections: Record<string, {
273
309
  selected: boolean;
274
310
  quantity: number;
@@ -483,4 +519,4 @@ interface QuoteRequestFormProps {
483
519
  */
484
520
  declare function QuoteRequestForm({ formToken, options, turnstileToken, items, onSubmitted, loadingFallback, className, }: QuoteRequestFormProps): react_jsx_runtime.JSX.Element;
485
521
 
486
- export { type AcceptQuoteInput, type AcceptQuoteResult, type BuyerQuote, type BuyerQuoteStatus, type ItemSelection, type PaymentTerms, type QuoteBranding, type QuoteCart, type QuoteCartLine, type QuoteCartOptions, type QuoteCartStorage, type QuoteComment, type QuoteItem, QuoteRequestForm, type QuoteRequestFormProps, type QuoteRequestItem, type QuoteRevision, QuoteView, type QuoteViewProps, QuotesClient, type QuotesClientOptions, type RequestFormConfig, type SubmitQuoteRequestInput, type ThrottleEnvelope, ThrottleQuotesError, type UseQuoteCartResult, type UseQuoteRequestFormResult, type UseQuoteResult, computeSelectionTotal, createQuoteCart, createQuotesClient, formatMoney, scaleLineCharges, useQuote, useQuoteCart, useQuoteRequestForm };
522
+ export { type AcceptQuoteInput, type AcceptQuoteResult, type BuyerQuote, type BuyerQuoteStatus, type ItemSelection, type PaymentTerms, type QuoteBranding, type QuoteCart, type QuoteCartLine, type QuoteCartOptions, type QuoteCartStorage, type QuoteComment, type QuoteItem, type QuoteItemRecurring, QuoteRequestForm, type QuoteRequestFormProps, type QuoteRequestItem, type QuoteRevision, QuoteView, type QuoteViewProps, QuotesClient, type QuotesClientOptions, type RequestFormConfig, type SubmitQuoteRequestInput, type ThrottleEnvelope, ThrottleQuotesError, type UseQuoteCartResult, type UseQuoteRequestFormResult, type UseQuoteResult, computeSelectionTotal, createQuoteCart, createQuotesClient, formatMoney, isFreeTrialLine, lineDueAtAcceptance, scaleLineCharges, useQuote, useQuoteCart, useQuoteRequestForm };
package/dist/index.d.ts CHANGED
@@ -16,6 +16,18 @@ type PaymentTerms = {
16
16
  };
17
17
  balanceNetN: number;
18
18
  };
19
+ /** A subscription line's terms (Throttle creates one subscription per line when the quote is paid). */
20
+ interface QuoteItemRecurring {
21
+ plan: string;
22
+ planName: string | null;
23
+ interval: string;
24
+ intervalUnit: string;
25
+ intervalCount: number;
26
+ /** Set on an installment plan: exactly this many payments. */
27
+ totalPayments: number | null;
28
+ /** 0 when there is no free trial. A free-trial line charges nothing at acceptance. */
29
+ trialDays: number;
30
+ }
19
31
  interface QuoteItem {
20
32
  id: string;
21
33
  type: string;
@@ -35,6 +47,8 @@ interface QuoteItem {
35
47
  qtyEditable: boolean;
36
48
  imageUrl: string | null;
37
49
  position: number;
50
+ /** Subscription terms, or null for a one-time line. `unitPrice` is the per-period price per seat. */
51
+ recurring: QuoteItemRecurring | null;
38
52
  }
39
53
  interface QuoteRevision {
40
54
  id: string;
@@ -261,6 +275,25 @@ declare function scaleLineCharges(item: {
261
275
  taxAmount: number;
262
276
  discountAmount: number;
263
277
  };
278
+ /** A subscription line with a free trial: nothing is charged for it at acceptance. */
279
+ declare function isFreeTrialLine(item: {
280
+ recurring?: {
281
+ trialDays?: number | null;
282
+ } | null;
283
+ }): boolean;
284
+ /**
285
+ * What one line charges at acceptance, at `quantity`. MUST match
286
+ * `lineDueAtAcceptance` in @commerce/quotes (shared test vectors).
287
+ */
288
+ declare function lineDueAtAcceptance(item: {
289
+ unitPrice: number;
290
+ quantity: number;
291
+ taxAmount: number;
292
+ discountAmount: number;
293
+ recurring?: {
294
+ trialDays?: number | null;
295
+ } | null;
296
+ }, quantity: number): number;
264
297
  /** Effective total for a selection, using the same math the server will apply. */
265
298
  declare function computeSelectionTotal(items: Array<{
266
299
  id: string;
@@ -269,6 +302,9 @@ declare function computeSelectionTotal(items: Array<{
269
302
  taxAmount: number;
270
303
  discountAmount: number;
271
304
  optional: boolean;
305
+ recurring?: {
306
+ trialDays?: number | null;
307
+ } | null;
272
308
  }>, selections: Record<string, {
273
309
  selected: boolean;
274
310
  quantity: number;
@@ -483,4 +519,4 @@ interface QuoteRequestFormProps {
483
519
  */
484
520
  declare function QuoteRequestForm({ formToken, options, turnstileToken, items, onSubmitted, loadingFallback, className, }: QuoteRequestFormProps): react_jsx_runtime.JSX.Element;
485
521
 
486
- export { type AcceptQuoteInput, type AcceptQuoteResult, type BuyerQuote, type BuyerQuoteStatus, type ItemSelection, type PaymentTerms, type QuoteBranding, type QuoteCart, type QuoteCartLine, type QuoteCartOptions, type QuoteCartStorage, type QuoteComment, type QuoteItem, QuoteRequestForm, type QuoteRequestFormProps, type QuoteRequestItem, type QuoteRevision, QuoteView, type QuoteViewProps, QuotesClient, type QuotesClientOptions, type RequestFormConfig, type SubmitQuoteRequestInput, type ThrottleEnvelope, ThrottleQuotesError, type UseQuoteCartResult, type UseQuoteRequestFormResult, type UseQuoteResult, computeSelectionTotal, createQuoteCart, createQuotesClient, formatMoney, scaleLineCharges, useQuote, useQuoteCart, useQuoteRequestForm };
522
+ export { type AcceptQuoteInput, type AcceptQuoteResult, type BuyerQuote, type BuyerQuoteStatus, type ItemSelection, type PaymentTerms, type QuoteBranding, type QuoteCart, type QuoteCartLine, type QuoteCartOptions, type QuoteCartStorage, type QuoteComment, type QuoteItem, type QuoteItemRecurring, QuoteRequestForm, type QuoteRequestFormProps, type QuoteRequestItem, type QuoteRevision, QuoteView, type QuoteViewProps, QuotesClient, type QuotesClientOptions, type RequestFormConfig, type SubmitQuoteRequestInput, type ThrottleEnvelope, ThrottleQuotesError, type UseQuoteCartResult, type UseQuoteRequestFormResult, type UseQuoteResult, computeSelectionTotal, createQuoteCart, createQuotesClient, formatMoney, isFreeTrialLine, lineDueAtAcceptance, scaleLineCharges, useQuote, useQuoteCart, useQuoteRequestForm };
package/dist/index.js CHANGED
@@ -164,14 +164,20 @@ function scaleLineCharges(item, quantity) {
164
164
  discountAmount: Math.round(item.discountAmount * ratio)
165
165
  };
166
166
  }
167
+ function isFreeTrialLine(item) {
168
+ return (item.recurring?.trialDays ?? 0) > 0;
169
+ }
170
+ function lineDueAtAcceptance(item, quantity) {
171
+ const { taxAmount, discountAmount } = scaleLineCharges(item, quantity);
172
+ return (isFreeTrialLine(item) ? 0 : item.unitPrice * quantity) + taxAmount - discountAmount;
173
+ }
167
174
  function computeSelectionTotal(items, selections, shippingTotal = 0) {
168
175
  let total = shippingTotal;
169
176
  for (const item of items) {
170
177
  const state = selections[item.id];
171
178
  if (item.optional && state && !state.selected) continue;
172
179
  const qty = state?.quantity ?? item.quantity;
173
- const { taxAmount, discountAmount } = scaleLineCharges(item, qty);
174
- total += item.unitPrice * qty + taxAmount - discountAmount;
180
+ total += lineDueAtAcceptance(item, qty);
175
181
  }
176
182
  return total;
177
183
  }
@@ -522,6 +528,44 @@ function useQuoteCart(options = {}) {
522
528
 
523
529
  // src/QuoteView.tsx
524
530
  import { useState as useState2 } from "react";
531
+
532
+ // ../core-cadence/src/cadence.ts
533
+ var INTERVAL_UNITS = ["hour", "day", "week", "month"];
534
+ var NAMED_INTERVALS = ["weekly", "biweekly", "monthly", "quarterly", "yearly"];
535
+ var INTERVAL_VALUES = [...NAMED_INTERVALS, "custom"];
536
+ var HOUR_MS = 36e5;
537
+ var DAY_MS = 24 * HOUR_MS;
538
+ var WEEK_MS = 7 * DAY_MS;
539
+ function isNamedInterval(value) {
540
+ return typeof value === "string" && NAMED_INTERVALS.includes(value);
541
+ }
542
+ function isIntervalUnit(value) {
543
+ return typeof value === "string" && INTERVAL_UNITS.includes(value);
544
+ }
545
+
546
+ // ../core-cadence/src/labels.ts
547
+ var NAMED_NOUN = {
548
+ weekly: "week",
549
+ biweekly: "2 weeks",
550
+ monthly: "month",
551
+ quarterly: "quarter",
552
+ yearly: "year"
553
+ };
554
+ function cadenceNoun(cadence) {
555
+ if (isNamedInterval(cadence.interval)) return NAMED_NOUN[cadence.interval];
556
+ if (isIntervalUnit(cadence.intervalUnit) && cadence.intervalCount != null) {
557
+ return cadence.intervalCount === 1 ? cadence.intervalUnit : `${cadence.intervalCount} ${cadence.intervalUnit}s`;
558
+ }
559
+ return cadence.interval;
560
+ }
561
+ function recurringTermsLabel(terms, each) {
562
+ const noun = cadenceNoun(terms);
563
+ const trialDays = terms.trialDays ?? 0;
564
+ if (trialDays > 0) return `Free for ${trialDays} day${trialDays === 1 ? "" : "s"}, then ${each} / ${noun}`;
565
+ return `${each} / ${noun}` + (terms.totalPayments != null ? ` \xB7 ${terms.totalPayments} payments` : "");
566
+ }
567
+
568
+ // src/QuoteView.tsx
525
569
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
526
570
  var row = { display: "flex", justifyContent: "space-between", gap: 16, padding: "8px 0" };
527
571
  var muted = { color: "#666", fontSize: 13 };
@@ -541,6 +585,7 @@ function QuoteView({ token, options, onCheckout, loadingFallback, className }) {
541
585
  const ordered = quote.status === "ordered";
542
586
  const accepted = quote.status === "accepted";
543
587
  const canAct = quote.status === "ready";
588
+ const recurringKept = rev ? rev.items.filter((i) => i.recurring && (!i.optional || (q.selections[i.id]?.selected ?? true))) : [];
544
589
  const handleResume = async () => {
545
590
  const { checkoutUrl } = await q.resumeCheckout();
546
591
  if (onCheckout) onCheckout(checkoutUrl);
@@ -599,7 +644,11 @@ function QuoteView({ token, options, onCheckout, loadingFallback, className }) {
599
644
  " ",
600
645
  /* @__PURE__ */ jsx("span", { style: muted, children: "optional" })
601
646
  ] }) : item.name,
602
- item.description ? /* @__PURE__ */ jsx("div", { style: muted, children: item.description }) : null
647
+ item.description ? /* @__PURE__ */ jsx("div", { style: muted, children: item.description }) : null,
648
+ item.recurring ? /* @__PURE__ */ jsxs("div", { style: muted, children: [
649
+ "Subscription \xB7 ",
650
+ recurringTermsLabel(item.recurring, formatMoney(item.unitPrice * state.quantity, currency))
651
+ ] }) : null
603
652
  ] }),
604
653
  /* @__PURE__ */ jsx("td", { align: "right", children: formatMoney(item.unitPrice, currency) }),
605
654
  /* @__PURE__ */ jsx("td", { align: "right", children: item.qtyEditable && canAct ? /* @__PURE__ */ jsx(
@@ -614,18 +663,23 @@ function QuoteView({ token, options, onCheckout, loadingFallback, className }) {
614
663
  style: { width: 64 }
615
664
  }
616
665
  ) : state.quantity }),
617
- /* @__PURE__ */ jsx("td", { align: "right", children: included ? formatMoney(
618
- item.unitPrice * state.quantity + item.taxAmount - item.discountAmount,
619
- currency
620
- ) : formatMoney(0, currency) })
666
+ /* @__PURE__ */ jsx("td", { align: "right", children: formatMoney(included ? lineDueAtAcceptance(item, state.quantity) : 0, currency) })
621
667
  ] }, item.id);
622
668
  }) })
623
669
  ] }) : null,
624
670
  rev ? /* @__PURE__ */ jsxs("section", { children: [
625
671
  /* @__PURE__ */ jsxs("div", { style: row, children: [
626
- /* @__PURE__ */ jsx("strong", { children: "Total" }),
672
+ /* @__PURE__ */ jsx("strong", { children: recurringKept.length > 0 ? "Due at acceptance" : "Total" }),
627
673
  /* @__PURE__ */ jsx("strong", { children: formatMoney(q.total, currency) })
628
674
  ] }),
675
+ recurringKept.length > 0 ? /* @__PURE__ */ jsxs("div", { children: [
676
+ /* @__PURE__ */ jsx("p", { style: muted, children: "Recurring after" }),
677
+ recurringKept.map((i) => /* @__PURE__ */ jsxs("p", { style: muted, children: [
678
+ i.name,
679
+ ": ",
680
+ recurringTermsLabel(i.recurring, formatMoney(i.unitPrice * (q.selections[i.id]?.quantity ?? i.quantity), currency))
681
+ ] }, i.id))
682
+ ] }) : null,
629
683
  /* @__PURE__ */ jsx("p", { style: muted, children: termsLabel(rev.paymentTerms, rev.depositAmount, currency) }),
630
684
  rev.leadTime ? /* @__PURE__ */ jsxs("p", { style: muted, children: [
631
685
  "Lead time: ",
@@ -898,6 +952,8 @@ export {
898
952
  createQuoteCart,
899
953
  createQuotesClient,
900
954
  formatMoney,
955
+ isFreeTrialLine,
956
+ lineDueAtAcceptance,
901
957
  scaleLineCharges,
902
958
  useQuote,
903
959
  useQuoteCart,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/errors.ts","../src/client.ts","../src/hooks.ts","../src/quote-cart.ts","../src/QuoteView.tsx","../src/QuoteRequestForm.tsx"],"sourcesContent":["import { ThrottleError, type ThrottleErrorArgs } from '@usethrottle/errors';\n\n/**\n * Every failure from the quote-link routes surfaces as this. The `code` is the\n * API's own error code, so callers can branch on the ones that matter to a\n * buyer flow rather than parsing messages:\n *\n * revision_superseded — a newer revision was issued; `details.currentRevisionId`\n * tells you what to reload\n * quote_expired — the offer lapsed (HTTP 410)\n * invalid_item_selection — locked-line edit, non-optional deselect, or unknown item\n * accept_in_progress — a concurrent accept won; retry shortly\n * not_found — unknown/rotated token, or a draft/archived quote\n */\nexport class ThrottleQuotesError extends ThrottleError {\n constructor(args: {\n code: string;\n message: string;\n statusCode: number;\n details?: unknown;\n responseHeaders?: ThrottleErrorArgs['responseHeaders'];\n }) {\n super(args);\n this.name = 'ThrottleQuotesError';\n }\n\n /** The offer moved on — reload and show the buyer the current revision. */\n get isSuperseded(): boolean {\n return this.code === 'revision_superseded';\n }\n\n get isExpired(): boolean {\n return this.code === 'quote_expired';\n }\n\n /** Revision the server considers current, when it told us (spec §8.1). */\n get currentRevisionId(): string | undefined {\n const d = this.details as { currentRevisionId?: string } | undefined;\n return d?.currentRevisionId;\n }\n}\n","import { ThrottleQuotesError } from './errors';\nimport type {\n AcceptQuoteInput,\n AcceptQuoteResult,\n BuyerQuote,\n QuoteComment,\n QuotesClientOptions,\n RequestFormConfig,\n SubmitQuoteRequestInput,\n ThrottleEnvelope,\n} from './types';\n\nconst DEFAULT_BASE = 'https://api.usethrottle.dev';\n\n/**\n * Headless client for the public quote surfaces. Everything is authorized by\n * the capability token in the URL — there is no API key here, and there must\n * not be: this runs in the buyer's browser.\n */\nexport class QuotesClient {\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof globalThis.fetch;\n private readonly timeoutMs: number;\n\n constructor(opts: QuotesClientOptions = {}) {\n const fetchImpl = opts.fetch ?? globalThis.fetch;\n if (!fetchImpl) throw new Error('QuotesClient: fetch is required');\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE).replace(/\\/+$/, '');\n this.fetchImpl = fetchImpl;\n this.timeoutMs = opts.timeoutMs ?? 30_000;\n }\n\n private async request<T>(method: string, path: string, body?: unknown): Promise<T> {\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);\n try {\n const res = await this.fetchImpl(`${this.baseUrl}${path}`, {\n method,\n headers: body === undefined ? {} : { 'content-type': 'application/json' },\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n signal: ctrl.signal,\n });\n const json = (await res.json().catch(() => ({}))) as ThrottleEnvelope<T>;\n if (!res.ok) {\n throw new ThrottleQuotesError({\n code: json.error?.code ?? 'unknown_error',\n message: json.error?.message ?? `HTTP ${res.status}`,\n statusCode: res.status,\n details: json.error?.details,\n responseHeaders: res.headers,\n });\n }\n return json.data as T;\n } finally {\n clearTimeout(timer);\n }\n }\n\n /** Fetch the quote a buyer link points at. 404s for unknown/rotated tokens. */\n getQuote(token: string): Promise<BuyerQuote> {\n return this.request<BuyerQuote>('GET', `/api/v1/quote-links/${encodeURIComponent(token)}`);\n }\n\n /**\n * Tell the merchant the buyer opened the quote. Fire-and-forget: the server\n * throttles the resulting webhook to one per revision per 6h, and a failed\n * beacon must never break the page.\n */\n async recordView(token: string): Promise<void> {\n try {\n await this.request('POST', `/api/v1/quote-links/${encodeURIComponent(token)}/view`, {});\n } catch {\n /* telemetry only */\n }\n }\n\n addComment(token: string, input: { body: string; authorName?: string; authorEmail?: string }) {\n return this.request<QuoteComment>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/comments`,\n input,\n );\n }\n\n requestRevision(token: string, input: { message?: string; authorName?: string } = {}) {\n return this.request<BuyerQuote>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/request-revision`,\n input,\n );\n }\n\n decline(token: string, input: { reason?: string; authorName?: string } = {}) {\n return this.request<BuyerQuote>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/decline`,\n input,\n );\n }\n\n /**\n * Accept and get the checkout URL to send the buyer to.\n *\n * Re-accepting the same revision is idempotent and replays the same URL.\n * Accepting a superseded revision throws `revision_superseded` carrying\n * `currentRevisionId`; an expired offer throws `quote_expired`.\n */\n accept(token: string, input: AcceptQuoteInput): Promise<AcceptQuoteResult> {\n return this.request<AcceptQuoteResult>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/accept`,\n input,\n );\n }\n\n /**\n * Send a buyer who already accepted back to a LIVE checkout.\n *\n * Checkout sessions expire after 30 minutes, so the URL returned by\n * `accept` is usually dead by the time the buyer returns to the link. This\n * re-opens a session against the same cart when it is (no re-pricing).\n * Throws `checkout_not_resumable` if the quote was never accepted, and\n * `checkout_completed` once it has been paid.\n */\n resumeCheckout(token: string): Promise<{ checkoutUrl: string }> {\n return this.request<{ checkoutUrl: string }>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/checkout`,\n {},\n );\n }\n\n /** URL of the PDF for the revision the buyer is looking at (server 302s to signed storage). */\n pdfUrl(token: string): string {\n return `${this.baseUrl}/api/v1/quote-links/${encodeURIComponent(token)}/pdf`;\n }\n\n // ── RFQ intake ─────────────────────────────────────────────────────────────\n\n getRequestForm(formToken: string): Promise<RequestFormConfig> {\n return this.request<RequestFormConfig>(\n 'GET',\n `/api/v1/quote-request-forms/${encodeURIComponent(formToken)}/public`,\n );\n }\n\n /**\n * Submit an RFQ. The response is deliberately thin (`{ submitted: true }`) —\n * anonymous callers learn no internal identifiers. A submission caught by\n * the honeypot returns the same shape without creating anything, so a bot\n * cannot tell it was detected; treat success as \"accepted\", not \"created\".\n */\n submitRequest(formToken: string, input: SubmitQuoteRequestInput): Promise<{ submitted: boolean }> {\n return this.request<{ submitted: boolean }>('POST', '/api/v1/quote-requests', {\n formToken,\n ...input,\n });\n }\n}\n\nexport function createQuotesClient(opts: QuotesClientOptions = {}): QuotesClient {\n return new QuotesClient(opts);\n}\n\n// ── money formatting ─────────────────────────────────────────────────────────\n\n/** Minor units → a display string, e.g. (104000, 'USD') → \"$1,040.00\". */\nexport function formatMoney(minorUnits: number, currency: string, locale?: string): string {\n return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(minorUnits / 100);\n}\n\n/**\n * Scale a line's quoted tax/discount to an edited quantity.\n *\n * MUST match `scaleLineCharges` in @commerce/quotes — the server recomputes\n * the accepted total with this rule, and any divergence shows the buyer one\n * total and charges another.\n */\nexport function scaleLineCharges(\n item: { quantity: number; taxAmount: number; discountAmount: number },\n quantity: number,\n): { taxAmount: number; discountAmount: number } {\n const base = item.quantity;\n if (!base || base <= 0 || quantity === base) {\n return { taxAmount: item.taxAmount, discountAmount: item.discountAmount };\n }\n const ratio = quantity / base;\n return {\n taxAmount: Math.round(item.taxAmount * ratio),\n discountAmount: Math.round(item.discountAmount * ratio),\n };\n}\n\n/** Effective total for a selection, using the same math the server will apply. */\nexport function computeSelectionTotal(\n items: Array<{\n id: string;\n unitPrice: number;\n quantity: number;\n taxAmount: number;\n discountAmount: number;\n optional: boolean;\n }>,\n selections: Record<string, { selected: boolean; quantity: number }>,\n shippingTotal = 0,\n): number {\n let total = shippingTotal;\n for (const item of items) {\n const state = selections[item.id];\n if (item.optional && state && !state.selected) continue;\n const qty = state?.quantity ?? item.quantity;\n const { taxAmount, discountAmount } = scaleLineCharges(item, qty);\n total += item.unitPrice * qty + taxAmount - discountAmount;\n }\n return total;\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { QuotesClient, computeSelectionTotal, createQuotesClient } from './client';\nimport { ThrottleQuotesError } from './errors';\nimport { createQuoteCart, type QuoteCart, type QuoteCartLine, type QuoteCartOptions } from './quote-cart';\nimport type {\n AcceptQuoteInput,\n BuyerQuote,\n ItemSelection,\n QuoteRequestItem,\n QuotesClientOptions,\n RequestFormConfig,\n SubmitQuoteRequestInput,\n} from './types';\n\nexport interface UseQuoteResult {\n quote: BuyerQuote | null;\n loading: boolean;\n error: ThrottleQuotesError | null;\n /** Re-fetch — call after a `revision_superseded` to show the current offer. */\n refresh: () => Promise<void>;\n /** Buyer's current line choices, keyed by item id. */\n selections: Record<string, { selected: boolean; quantity: number }>;\n setSelected: (itemId: string, selected: boolean) => void;\n setQuantity: (itemId: string, quantity: number) => void;\n /** Live total for the current selections, using the server's own math. */\n total: number;\n accept: (\n input: Omit<AcceptQuoteInput, 'revisionId' | 'itemSelections'>,\n ) => Promise<{ checkoutUrl: string }>;\n /** Live checkout URL for a buyer returning to an already-accepted quote —\n * the one minted at acceptance has usually expired by then. */\n resumeCheckout: () => Promise<{ checkoutUrl: string }>;\n requestRevision: (message: string, authorName?: string) => Promise<void>;\n decline: (reason?: string, authorName?: string) => Promise<void>;\n addComment: (body: string, authorName?: string) => Promise<void>;\n pdfUrl: string;\n submitting: boolean;\n}\n\n/**\n * Everything the buyer quote page needs.\n *\n * Fires the view beacon once per mount, keeps line selections in local state,\n * and recomputes the total with the same rule the server applies at\n * acceptance — so the number the buyer sees is the number they are charged.\n */\nexport function useQuote(token: string, options: QuotesClientOptions = {}): UseQuoteResult {\n const client = useMemo(\n () => new QuotesClient(options),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [options.baseUrl, options.timeoutMs],\n );\n const [quote, setQuote] = useState<BuyerQuote | null>(null);\n const [loading, setLoading] = useState(true);\n const [submitting, setSubmitting] = useState(false);\n const [error, setError] = useState<ThrottleQuotesError | null>(null);\n const [selections, setSelections] = useState<Record<string, { selected: boolean; quantity: number }>>({});\n const beaconSent = useRef(false);\n\n const load = useCallback(async () => {\n setLoading(true);\n try {\n const q = await client.getQuote(token);\n setQuote(q);\n setError(null);\n // Seed selections from the revision's own defaults so a re-issue that\n // adds or locks lines resets the buyer's choices to the new offer.\n const next: Record<string, { selected: boolean; quantity: number }> = {};\n for (const item of q.currentRevision?.items ?? []) {\n next[item.id] = { selected: item.optional ? item.optionalSelected : true, quantity: item.quantity };\n }\n setSelections(next);\n } catch (e) {\n setError(e as ThrottleQuotesError);\n } finally {\n setLoading(false);\n }\n }, [client, token]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n useEffect(() => {\n if (!quote || beaconSent.current) return;\n beaconSent.current = true;\n void client.recordView(token);\n }, [client, quote, token]);\n\n const setSelected = useCallback((itemId: string, selected: boolean) => {\n setSelections((prev) => ({ ...prev, [itemId]: { ...(prev[itemId] ?? { quantity: 1 }), selected } }));\n }, []);\n\n const setQuantity = useCallback((itemId: string, quantity: number) => {\n setSelections((prev) => ({\n ...prev,\n [itemId]: { ...(prev[itemId] ?? { selected: true }), quantity: Math.max(1, Math.floor(quantity) || 1) },\n }));\n }, []);\n\n const total = useMemo(\n () =>\n computeSelectionTotal(\n quote?.currentRevision?.items ?? [],\n selections,\n quote?.currentRevision?.shippingTotal ?? 0,\n ),\n [quote, selections],\n );\n\n /** Only send selections the rep actually allows the buyer to change. */\n const buildSelections = useCallback((): ItemSelection[] => {\n const items = quote?.currentRevision?.items ?? [];\n const out: ItemSelection[] = [];\n for (const item of items) {\n const state = selections[item.id];\n if (!state) continue;\n const sel: ItemSelection = { itemId: item.id };\n let include = false;\n if (item.optional && state.selected !== item.optionalSelected) {\n sel.selected = state.selected;\n include = true;\n }\n if (item.qtyEditable && state.quantity !== item.quantity) {\n sel.quantity = state.quantity;\n include = true;\n }\n if (include) out.push(sel);\n }\n return out;\n }, [quote, selections]);\n\n const guard = useCallback(\n async <T,>(fn: () => Promise<T>): Promise<T> => {\n setSubmitting(true);\n try {\n const result = await fn();\n setError(null);\n return result;\n } catch (e) {\n const err = e as ThrottleQuotesError;\n setError(err);\n // A superseded offer means our copy is stale — pull the current one so\n // the buyer sees what they can actually accept (spec §8.1).\n if (err.isSuperseded) void load();\n throw err;\n } finally {\n setSubmitting(false);\n }\n },\n [load],\n );\n\n const accept = useCallback(\n async (input: Omit<AcceptQuoteInput, 'revisionId' | 'itemSelections'>) => {\n const revisionId = quote?.currentRevision?.id;\n if (!revisionId) throw new Error('useQuote: no current revision to accept');\n const itemSelections = buildSelections();\n const res = await guard(() =>\n client.accept(token, {\n ...input,\n revisionId,\n ...(itemSelections.length ? { itemSelections } : {}),\n }),\n );\n return { checkoutUrl: res.checkoutUrl };\n },\n [buildSelections, client, guard, quote, token],\n );\n\n const resumeCheckout = useCallback(\n async () => guard(() => client.resumeCheckout(token)),\n [client, guard, token],\n );\n\n const requestRevision = useCallback(\n async (message: string, authorName?: string) => {\n const q = await guard(() => client.requestRevision(token, { message, authorName }));\n setQuote(q);\n },\n [client, guard, token],\n );\n\n const decline = useCallback(\n async (reason?: string, authorName?: string) => {\n const q = await guard(() => client.decline(token, { reason, authorName }));\n setQuote(q);\n },\n [client, guard, token],\n );\n\n const addComment = useCallback(\n async (body: string, authorName?: string) => {\n await guard(() => client.addComment(token, { body, authorName }));\n await load();\n },\n [client, guard, load, token],\n );\n\n return {\n quote,\n loading,\n error,\n refresh: load,\n selections,\n setSelected,\n setQuantity,\n total,\n accept,\n resumeCheckout,\n requestRevision,\n decline,\n addComment,\n pdfUrl: client.pdfUrl(token),\n submitting,\n };\n}\n\nexport interface UseQuoteRequestFormResult {\n config: RequestFormConfig | null;\n loading: boolean;\n error: ThrottleQuotesError | null;\n submitting: boolean;\n submitted: boolean;\n submit: (input: SubmitQuoteRequestInput) => Promise<void>;\n /** Mount time; pass through so the API's minimum-fill-time check passes. */\n formLoadedAt: number;\n}\n\n/** Loads a hosted RFQ form's config and submits it, with bot fields handled. */\nexport function useQuoteRequestForm(\n formToken: string,\n options: QuotesClientOptions = {},\n): UseQuoteRequestFormResult {\n const client = useMemo(\n () => new QuotesClient(options),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [options.baseUrl, options.timeoutMs],\n );\n const [config, setConfig] = useState<RequestFormConfig | null>(null);\n const [loading, setLoading] = useState(true);\n const [submitting, setSubmitting] = useState(false);\n const [submitted, setSubmitted] = useState(false);\n const [error, setError] = useState<ThrottleQuotesError | null>(null);\n const formLoadedAt = useRef(Date.now()).current;\n\n useEffect(() => {\n let cancelled = false;\n (async () => {\n try {\n const cfg = await client.getRequestForm(formToken);\n if (!cancelled) setConfig(cfg);\n } catch (e) {\n if (!cancelled) setError(e as ThrottleQuotesError);\n } finally {\n if (!cancelled) setLoading(false);\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [client, formToken]);\n\n const submit = useCallback(\n async (input: SubmitQuoteRequestInput) => {\n setSubmitting(true);\n try {\n await client.submitRequest(formToken, { formLoadedAt, ...input });\n setSubmitted(true);\n setError(null);\n } catch (e) {\n setError(e as ThrottleQuotesError);\n throw e;\n } finally {\n setSubmitting(false);\n }\n },\n [client, formLoadedAt, formToken],\n );\n\n return { config, loading, error, submitting, submitted, submit, formLoadedAt };\n}\n\nexport { createQuotesClient };\n\nexport interface UseQuoteCartResult {\n lines: QuoteCartLine[];\n /** Total quantity across lines — for a header badge. */\n count: number;\n add: (item: QuoteRequestItem) => void;\n addMany: (items: QuoteRequestItem[]) => void;\n update: (key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>) => void;\n remove: (key: string) => void;\n clear: () => void;\n /** The underlying cart, for use outside React (an Add to Quote button in plain JS). */\n cart: QuoteCart;\n}\n\n/**\n * A quote cart the buyer fills while browsing, then submits as one request.\n *\n * The cart persists across page views, so an Add to Quote button on a product\n * page and a Request quote page elsewhere on the site see the same lines.\n * Pass the result of `toRequestItems()` (or `lines`) to\n * `useQuoteRequestForm().submit` as `items`.\n */\nexport function useQuoteCart(options: QuoteCartOptions = {}): UseQuoteCartResult {\n const cart = useMemo(\n () => createQuoteCart(options),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [options.storageKey, options.maxLines],\n );\n // Seeded empty rather than from storage: reading localStorage during render\n // makes the server and the first client render disagree, which is a\n // hydration mismatch. The effect below fills it in immediately after mount.\n const [lines, setLines] = useState<QuoteCartLine[]>([]);\n\n useEffect(() => {\n setLines(cart.read());\n return cart.subscribe(setLines);\n }, [cart]);\n\n return {\n lines,\n count: lines.reduce((sum, line) => sum + line.quantity, 0),\n add: useCallback((item: QuoteRequestItem) => void cart.add(item), [cart]),\n addMany: useCallback((items: QuoteRequestItem[]) => void cart.addMany(items), [cart]),\n update: useCallback(\n (key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>) => void cart.update(key, patch),\n [cart],\n ),\n remove: useCallback((key: string) => void cart.remove(key), [cart]),\n clear: useCallback(() => cart.clear(), [cart]),\n cart,\n };\n}\n","import type { QuoteRequestItem } from './types';\n\n/**\n * A quote cart: the buyer's running list of things to be quoted, built up\n * while they browse.\n *\n * Throttle stores no catalog, so the storefront owns product data and hands\n * lines in — the same arrangement as the shopping cart. Nothing here is sent\n * until `read()` is submitted through `QuotesClient.submitRequest`, and the\n * amounts a storefront attaches are the buyer's TARGET price, never a price\n * we will honour.\n *\n * Lines are keyed by `sku ?? referenceId ?? name`, so adding the same product\n * twice increases its quantity instead of stacking duplicate rows.\n */\n\nexport interface QuoteCartLine extends QuoteRequestItem {\n /** Stable identity for updates and removals. */\n key: string;\n quantity: number;\n}\n\nexport interface QuoteCartStorage {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nexport interface QuoteCartOptions {\n /** Namespacing key, so two storefronts on one origin cannot collide. */\n storageKey?: string;\n /**\n * Where the cart survives page navigation. Defaults to `localStorage` in a\n * browser and to memory everywhere else (SSR, tests) — a cart that throws\n * during server rendering would take the whole page down.\n */\n storage?: QuoteCartStorage | null;\n /** Refuse to grow past this many distinct lines. The API caps at 100. */\n maxLines?: number;\n}\n\nconst DEFAULT_KEY = 'throttle:quote-cart';\nconst DEFAULT_MAX_LINES = 100;\n\nfunction memoryStorage(): QuoteCartStorage {\n const map = new Map<string, string>();\n return {\n getItem: (key) => map.get(key) ?? null,\n setItem: (key, value) => void map.set(key, value),\n removeItem: (key) => void map.delete(key),\n };\n}\n\nfunction defaultStorage(): QuoteCartStorage {\n try {\n if (typeof globalThis !== 'undefined' && (globalThis as any).localStorage) {\n return (globalThis as any).localStorage as QuoteCartStorage;\n }\n } catch {\n // Safari in private mode throws on access rather than returning null.\n }\n return memoryStorage();\n}\n\nfunction lineKey(item: QuoteRequestItem): string {\n const raw = item.sku?.trim() || item.referenceId?.trim() || item.name.trim();\n return raw.toLowerCase();\n}\n\nfunction normalizeQuantity(value: number | null | undefined): number {\n const n = Math.floor(Number(value ?? 1));\n return Number.isFinite(n) && n > 0 ? n : 1;\n}\n\nexport interface QuoteCart {\n /** Current lines, oldest first. */\n read(): QuoteCartLine[];\n /** Total quantity across lines — what a header badge should show. */\n count(): number;\n /** Add a line, merging quantities into an existing one with the same key. */\n add(item: QuoteRequestItem): QuoteCartLine[];\n /** Add several at once (a \"quote this whole cart\" button). */\n addMany(items: QuoteRequestItem[]): QuoteCartLine[];\n /** Change any field of a line. A quantity below 1 removes it. */\n update(key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>): QuoteCartLine[];\n remove(key: string): QuoteCartLine[];\n clear(): void;\n /** Subscribe to every change. Returns an unsubscribe function. */\n subscribe(listener: (lines: QuoteCartLine[]) => void): () => void;\n /** The `items` payload for `QuotesClient.submitRequest`. */\n toRequestItems(): QuoteRequestItem[];\n}\n\nexport function createQuoteCart(options: QuoteCartOptions = {}): QuoteCart {\n const storageKey = options.storageKey ?? DEFAULT_KEY;\n const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n const storage = options.storage === null ? memoryStorage() : (options.storage ?? defaultStorage());\n const listeners = new Set<(lines: QuoteCartLine[]) => void>();\n\n function load(): QuoteCartLine[] {\n try {\n const raw = storage.getItem(storageKey);\n if (!raw) return [];\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n // Storage is buyer-writable — anything shaped wrong is dropped rather\n // than trusted into a submission.\n return parsed\n .filter((line: any) => line && typeof line.name === 'string' && line.name.trim())\n .map((line: any) => ({\n ...line,\n key: typeof line.key === 'string' && line.key ? line.key : lineKey(line),\n quantity: normalizeQuantity(line.quantity),\n }))\n .slice(0, maxLines);\n } catch {\n return [];\n }\n }\n\n function save(lines: QuoteCartLine[]): QuoteCartLine[] {\n try {\n storage.setItem(storageKey, JSON.stringify(lines));\n } catch {\n // Quota or a disabled store — the cart still works for this page view.\n }\n for (const listener of listeners) listener(lines);\n return lines;\n }\n\n return {\n read: load,\n\n count() {\n return load().reduce((sum, line) => sum + line.quantity, 0);\n },\n\n add(item) {\n return this.addMany([item]);\n },\n\n addMany(items) {\n const lines = load();\n for (const item of items) {\n if (!item?.name?.trim()) continue;\n const key = lineKey(item);\n const existing = lines.find((line) => line.key === key);\n if (existing) {\n existing.quantity += normalizeQuantity(item.quantity);\n // Later detail wins — a product page knows more than a listing card.\n if (item.note) existing.note = item.note;\n if (item.targetUnitPrice != null) existing.targetUnitPrice = item.targetUnitPrice;\n continue;\n }\n if (lines.length >= maxLines) break;\n lines.push({ ...item, key, quantity: normalizeQuantity(item.quantity) });\n }\n return save(lines);\n },\n\n update(key, patch) {\n const lines = load();\n const index = lines.findIndex((line) => line.key === key);\n if (index === -1) return lines;\n if (patch.quantity != null && Math.floor(Number(patch.quantity)) < 1) {\n lines.splice(index, 1);\n return save(lines);\n }\n lines[index] = {\n ...lines[index],\n ...patch,\n key,\n quantity: normalizeQuantity(patch.quantity ?? lines[index].quantity),\n };\n return save(lines);\n },\n\n remove(key) {\n return save(load().filter((line) => line.key !== key));\n },\n\n clear() {\n try {\n storage.removeItem(storageKey);\n } catch {\n // Ignore — the notification below still resets any subscriber.\n }\n for (const listener of listeners) listener([]);\n },\n\n subscribe(listener) {\n listeners.add(listener);\n return () => void listeners.delete(listener);\n },\n\n toRequestItems() {\n return load().map(({ key: _key, ...item }) => item);\n },\n };\n}\n","import { useState, type CSSProperties, type ReactNode } from 'react';\nimport { formatMoney } from './client';\nimport { useQuote } from './hooks';\nimport type { QuotesClientOptions } from './types';\n\nexport interface QuoteViewProps {\n /** The `qtl_…` token from the buyer's link. */\n token: string;\n options?: QuotesClientOptions;\n /**\n * Where to send the buyer after acceptance. Defaults to assigning\n * `window.location.href`, which is what the hosted page does.\n */\n onCheckout?: (checkoutUrl: string) => void;\n /** Rendered instead of the built-in markup while loading. */\n loadingFallback?: ReactNode;\n className?: string;\n}\n\nconst row: CSSProperties = { display: 'flex', justifyContent: 'space-between', gap: 16, padding: '8px 0' };\nconst muted: CSSProperties = { color: '#666', fontSize: 13 };\n\n/**\n * Drop-in buyer quote page: line items with optional toggles and quantity\n * editing, live totals, revision history, comment thread, and the\n * accept / request-changes / decline actions.\n *\n * Intentionally unstyled beyond a readable default — it is meant to be\n * restyled or replaced. `useQuote` carries all the behaviour if you want to\n * build your own presentation.\n */\nexport function QuoteView({ token, options, onCheckout, loadingFallback, className }: QuoteViewProps) {\n const q = useQuote(token, options ?? {});\n const [name, setName] = useState('');\n const [email, setEmail] = useState('');\n const [showAccept, setShowAccept] = useState(false);\n const [comment, setComment] = useState('');\n\n if (q.loading) return <>{loadingFallback ?? <p>Loading quote…</p>}</>;\n if (!q.quote) {\n return <p>This quote link is no longer available.</p>;\n }\n\n const quote = q.quote;\n const rev = quote.currentRevision;\n const currency = quote.currency;\n const ordered = quote.status === 'ordered';\n const accepted = quote.status === 'accepted';\n const canAct = quote.status === 'ready';\n\n const handleResume = async () => {\n const { checkoutUrl } = await q.resumeCheckout();\n if (onCheckout) onCheckout(checkoutUrl);\n else if (typeof window !== 'undefined') window.location.href = checkoutUrl;\n };\n\n const handleAccept = async () => {\n const { checkoutUrl } = await q.accept({ acceptedByName: name, acceptedByEmail: email });\n if (onCheckout) onCheckout(checkoutUrl);\n else if (typeof window !== 'undefined') window.location.href = checkoutUrl;\n };\n\n return (\n <div className={className}>\n <header>\n {quote.branding?.displayName ? <p style={muted}>{quote.branding.displayName}</p> : null}\n <h1>Quote {quote.quoteNumber}</h1>\n <p style={muted}>{statusLabel(quote.status)}</p>\n {quote.buyerReference ? <p style={muted}>Your reference: {quote.buyerReference}</p> : null}\n {rev ? (\n <p style={muted}>\n Revision {rev.revisionNumber}\n {rev.expiresAt ? ` · valid until ${new Date(rev.expiresAt).toLocaleDateString()}` : null}\n {' · '}\n <a href={q.pdfUrl}>Download PDF</a>\n </p>\n ) : null}\n </header>\n\n {q.error ? (\n <p role=\"alert\">\n {q.error.isSuperseded\n ? 'This quote was updated — showing the latest version.'\n : q.error.message}\n </p>\n ) : null}\n\n {rev ? (\n <table style={{ width: '100%', borderCollapse: 'collapse' }}>\n <thead>\n <tr>\n <th align=\"left\">Item</th>\n <th align=\"right\">Unit price</th>\n <th align=\"right\">Qty</th>\n <th align=\"right\">Total</th>\n </tr>\n </thead>\n <tbody>\n {rev.items.map((item) => {\n const state = q.selections[item.id] ?? { selected: true, quantity: item.quantity };\n const included = item.optional ? state.selected : true;\n return (\n <tr key={item.id}>\n <td>\n {item.optional ? (\n <label>\n <input\n type=\"checkbox\"\n checked={state.selected}\n disabled={!canAct}\n onChange={(e) => q.setSelected(item.id, e.target.checked)}\n />{' '}\n {item.name} <span style={muted}>optional</span>\n </label>\n ) : (\n item.name\n )}\n {item.description ? <div style={muted}>{item.description}</div> : null}\n </td>\n <td align=\"right\">{formatMoney(item.unitPrice, currency)}</td>\n <td align=\"right\">\n {item.qtyEditable && canAct ? (\n <input\n type=\"number\"\n min={1}\n value={state.quantity}\n disabled={!included}\n aria-label={`Quantity for ${item.name}`}\n onChange={(e) => q.setQuantity(item.id, Number(e.target.value))}\n style={{ width: 64 }}\n />\n ) : (\n state.quantity\n )}\n </td>\n <td align=\"right\">\n {included\n ? formatMoney(\n item.unitPrice * state.quantity + item.taxAmount - item.discountAmount,\n currency,\n )\n : formatMoney(0, currency)}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n ) : null}\n\n {rev ? (\n <section>\n <div style={row}>\n <strong>Total</strong>\n <strong>{formatMoney(q.total, currency)}</strong>\n </div>\n <p style={muted}>{termsLabel(rev.paymentTerms, rev.depositAmount, currency)}</p>\n {rev.leadTime ? <p style={muted}>Lead time: {rev.leadTime}</p> : null}\n {rev.buyerNote ? <blockquote>{rev.buyerNote}</blockquote> : null}\n </section>\n ) : null}\n\n {ordered ? (\n <p>\n Ordered{quote.orderId ? ` — order ${quote.orderId}` : ''}. Thank you.\n </p>\n ) : accepted ? (\n // Accepted but not yet paid. The checkout session minted at acceptance\n // expires in 30 minutes, so this asks the server for a live one rather\n // than linking to a URL that is probably dead.\n <section>\n <p>You accepted this quote — payment is not finished yet.</p>\n <button type=\"button\" disabled={q.submitting} onClick={() => void handleResume()}>\n Continue to checkout\n </button>\n </section>\n ) : canAct ? (\n <section>\n {showAccept ? (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n void handleAccept();\n }}\n >\n <label>\n Your name\n <input value={name} onChange={(e) => setName(e.target.value)} required />\n </label>\n <label>\n Email\n <input type=\"email\" value={email} onChange={(e) => setEmail(e.target.value)} required />\n </label>\n <button type=\"submit\" disabled={q.submitting || !name || !email}>\n Confirm &amp; continue to payment\n </button>\n </form>\n ) : (\n <button type=\"button\" onClick={() => setShowAccept(true)}>\n Accept &amp; checkout\n </button>\n )}\n <button\n type=\"button\"\n disabled={q.submitting}\n onClick={() => void q.requestRevision('Please revise this quote.', name || undefined)}\n >\n Request changes\n </button>\n <button type=\"button\" disabled={q.submitting} onClick={() => void q.decline(undefined, name || undefined)}>\n Decline\n </button>\n </section>\n ) : null}\n\n {quote.revisionHistory.length > 0 ? (\n <details>\n <summary>Previous versions ({quote.revisionHistory.length})</summary>\n <ul>\n {quote.revisionHistory.map((r) => (\n <li key={r.id}>\n Revision {r.revisionNumber} — {formatMoney(r.total, currency)}\n {r.changeSummary ? ` · ${r.changeSummary}` : ''}\n </li>\n ))}\n </ul>\n </details>\n ) : null}\n\n <section>\n <h2>Questions</h2>\n {quote.comments.map((c) => (\n <p key={c.id}>\n <strong>{c.authorName ?? (c.authorType === 'merchant' ? 'Seller' : 'You')}:</strong> {c.body}\n </p>\n ))}\n <form\n onSubmit={(e) => {\n e.preventDefault();\n const body = comment.trim();\n if (!body) return;\n setComment('');\n void q.addComment(body, name || undefined);\n }}\n >\n <input\n value={comment}\n onChange={(e) => setComment(e.target.value)}\n placeholder=\"Ask a question…\"\n aria-label=\"Ask a question\"\n />\n <button type=\"submit\" disabled={q.submitting || !comment.trim()}>\n Send\n </button>\n </form>\n </section>\n </div>\n );\n}\n\nfunction statusLabel(status: string): string {\n switch (status) {\n case 'in_progress':\n return 'We are preparing your quote';\n case 'ready':\n return 'Ready for your review';\n case 'being_revised':\n return 'We are revising this quote';\n case 'accepted':\n return 'Accepted — completing payment';\n case 'ordered':\n return 'Ordered';\n case 'declined':\n return 'Declined';\n case 'expired':\n return 'This quote has expired — request updated pricing';\n default:\n return status;\n }\n}\n\nfunction termsLabel(\n terms: { mode: string; netN?: number; balanceNetN?: number },\n depositAmount: number | null,\n currency: string,\n): string {\n if (terms.mode === 'net_terms') return `Payment terms: Net ${terms.netN}`;\n if (terms.mode === 'deposit_balance') {\n const dep = depositAmount != null ? formatMoney(depositAmount, currency) : 'a deposit';\n return `${dep} due today, balance Net ${terms.balanceNetN}`;\n }\n return 'Payment in full';\n}\n","import { useState, type CSSProperties, type ReactNode } from 'react';\nimport { useQuoteRequestForm } from './hooks';\nimport type { QuoteRequestItem, QuotesClientOptions } from './types';\n\nexport interface QuoteRequestFormProps {\n /** The `qrf_…` token from the merchant's hosted form URL. */\n formToken: string;\n options?: QuotesClientOptions;\n /**\n * Turnstile token, when the form requires it. Render the widget yourself and\n * pass its token through — this package deliberately pulls in no Cloudflare\n * script. If `config.fields.requireTurnstile` is set and this is empty, the\n * API fails the submission closed.\n */\n turnstileToken?: string;\n /**\n * Lines to submit with the request — typically `useQuoteCart().lines` after\n * the buyer has been adding products while browsing. When present, the\n * written message is optional.\n */\n items?: QuoteRequestItem[];\n onSubmitted?: () => void;\n loadingFallback?: ReactNode;\n className?: string;\n}\n\n/** Off-screen but still focusable-free; bots fill it, humans never see it. */\nconst honeypotStyle: CSSProperties = {\n position: 'absolute',\n width: 1,\n height: 1,\n padding: 0,\n margin: -1,\n overflow: 'hidden',\n clip: 'rect(0 0 0 0)',\n whiteSpace: 'nowrap',\n border: 0,\n};\n\n/**\n * Hosted RFQ intake form.\n *\n * Bot protection is wired in for you: the honeypot field and the mount-time\n * anchor that feeds the API's minimum-fill-time check. A honeypot hit returns\n * a success-shaped response and creates nothing, so `onSubmitted` fires either\n * way — by design, a bot must not learn it was caught.\n */\nexport function QuoteRequestForm({\n formToken,\n options,\n turnstileToken,\n items,\n onSubmitted,\n loadingFallback,\n className,\n}: QuoteRequestFormProps) {\n const f = useQuoteRequestForm(formToken, options ?? {});\n // Lines the host collected while the buyer browsed (see `useQuoteCart`).\n // With any of these present the written message becomes optional: the ask\n // is the list.\n const lines = items ?? [];\n const [values, setValues] = useState({\n email: '',\n name: '',\n companyName: '',\n phone: '',\n buyerReference: '',\n message: '',\n website: '', // honeypot\n });\n\n const set = (k: keyof typeof values) => (e: { target: { value: string } }) =>\n setValues((prev) => ({ ...prev, [k]: e.target.value }));\n\n if (f.loading) return <>{loadingFallback ?? <p>Loading…</p>}</>;\n if (!f.config) return <p>This request form is not available.</p>;\n\n if (f.submitted) {\n const redirect = f.config.redirectUrl;\n // Re-check the scheme client-side even though the API rejects non-http(s)\n // on write — a stale row must not be able to script the host page.\n if (redirect && /^https?:\\/\\//i.test(redirect) && typeof window !== 'undefined') {\n window.location.href = redirect;\n }\n return <p>{f.config.successMessage ?? 'Thanks — we will be in touch shortly.'}</p>;\n }\n\n const fields = f.config.fields ?? {};\n\n return (\n <form\n className={className}\n onSubmit={(e) => {\n e.preventDefault();\n void f\n .submit({\n email: values.email,\n message: values.message || null,\n ...(lines.length > 0 ? { items: lines } : {}),\n name: values.name || null,\n companyName: fields.companyName ? values.companyName || null : null,\n phone: fields.phone ? values.phone || null : null,\n buyerReference: fields.buyerReference ? values.buyerReference || null : null,\n website: values.website || null,\n ...(turnstileToken ? { turnstileToken } : {}),\n })\n .then(() => onSubmitted?.())\n .catch(() => {\n /* surfaced via f.error */\n });\n }}\n >\n {f.config.title ? <h1>{f.config.title}</h1> : null}\n {f.config.introText ? <p>{f.config.introText}</p> : null}\n {f.error ? <p role=\"alert\">{f.error.message}</p> : null}\n\n <label>\n Email\n <input type=\"email\" value={values.email} onChange={set('email')} required />\n </label>\n <label>\n Name\n <input value={values.name} onChange={set('name')} />\n </label>\n {fields.companyName ? (\n <label>\n Company\n <input value={values.companyName} onChange={set('companyName')} />\n </label>\n ) : null}\n {fields.phone ? (\n <label>\n Phone\n <input value={values.phone} onChange={set('phone')} />\n </label>\n ) : null}\n {fields.buyerReference ? (\n <label>\n Your reference #\n <input value={values.buyerReference} onChange={set('buyerReference')} />\n </label>\n ) : null}\n {lines.length > 0 ? (\n <ul aria-label=\"Items on this request\">\n {lines.map((line, index) => (\n <li key={line.sku ?? line.referenceId ?? `${line.name}-${index}`}>\n {line.quantity ?? 1} × {line.name}\n {line.sku ? ` (${line.sku})` : ''}\n {line.note ? ` — ${line.note}` : ''}\n </li>\n ))}\n </ul>\n ) : null}\n <label>\n {lines.length > 0 ? 'Anything else we should know?' : 'What do you need?'}\n <textarea\n value={values.message}\n onChange={set('message')}\n required={lines.length === 0}\n />\n </label>\n\n <div style={honeypotStyle} aria-hidden=\"true\">\n <label>\n Website\n <input\n name=\"website\"\n tabIndex={-1}\n autoComplete=\"off\"\n value={values.website}\n onChange={set('website')}\n />\n </label>\n </div>\n\n <button\n type=\"submit\"\n disabled={f.submitting || !values.email || (!values.message && lines.length === 0)}\n >\n {f.submitting ? 'Sending…' : 'Request a quote'}\n </button>\n </form>\n );\n}\n"],"mappings":"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","names":["useState","useState","useState","Fragment","jsx","jsxs","useState"]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/client.ts","../src/hooks.ts","../src/quote-cart.ts","../src/QuoteView.tsx","../../core-cadence/src/cadence.ts","../../core-cadence/src/labels.ts","../src/QuoteRequestForm.tsx"],"sourcesContent":["import { ThrottleError, type ThrottleErrorArgs } from '@usethrottle/errors';\n\n/**\n * Every failure from the quote-link routes surfaces as this. The `code` is the\n * API's own error code, so callers can branch on the ones that matter to a\n * buyer flow rather than parsing messages:\n *\n * revision_superseded — a newer revision was issued; `details.currentRevisionId`\n * tells you what to reload\n * quote_expired — the offer lapsed (HTTP 410)\n * invalid_item_selection — locked-line edit, non-optional deselect, or unknown item\n * accept_in_progress — a concurrent accept won; retry shortly\n * not_found — unknown/rotated token, or a draft/archived quote\n */\nexport class ThrottleQuotesError extends ThrottleError {\n constructor(args: {\n code: string;\n message: string;\n statusCode: number;\n details?: unknown;\n responseHeaders?: ThrottleErrorArgs['responseHeaders'];\n }) {\n super(args);\n this.name = 'ThrottleQuotesError';\n }\n\n /** The offer moved on — reload and show the buyer the current revision. */\n get isSuperseded(): boolean {\n return this.code === 'revision_superseded';\n }\n\n get isExpired(): boolean {\n return this.code === 'quote_expired';\n }\n\n /** Revision the server considers current, when it told us (spec §8.1). */\n get currentRevisionId(): string | undefined {\n const d = this.details as { currentRevisionId?: string } | undefined;\n return d?.currentRevisionId;\n }\n}\n","import { ThrottleQuotesError } from './errors';\nimport type {\n AcceptQuoteInput,\n AcceptQuoteResult,\n BuyerQuote,\n QuoteComment,\n QuotesClientOptions,\n RequestFormConfig,\n SubmitQuoteRequestInput,\n ThrottleEnvelope,\n} from './types';\n\nconst DEFAULT_BASE = 'https://api.usethrottle.dev';\n\n/**\n * Headless client for the public quote surfaces. Everything is authorized by\n * the capability token in the URL — there is no API key here, and there must\n * not be: this runs in the buyer's browser.\n */\nexport class QuotesClient {\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof globalThis.fetch;\n private readonly timeoutMs: number;\n\n constructor(opts: QuotesClientOptions = {}) {\n const fetchImpl = opts.fetch ?? globalThis.fetch;\n if (!fetchImpl) throw new Error('QuotesClient: fetch is required');\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE).replace(/\\/+$/, '');\n this.fetchImpl = fetchImpl;\n this.timeoutMs = opts.timeoutMs ?? 30_000;\n }\n\n private async request<T>(method: string, path: string, body?: unknown): Promise<T> {\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);\n try {\n const res = await this.fetchImpl(`${this.baseUrl}${path}`, {\n method,\n headers: body === undefined ? {} : { 'content-type': 'application/json' },\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n signal: ctrl.signal,\n });\n const json = (await res.json().catch(() => ({}))) as ThrottleEnvelope<T>;\n if (!res.ok) {\n throw new ThrottleQuotesError({\n code: json.error?.code ?? 'unknown_error',\n message: json.error?.message ?? `HTTP ${res.status}`,\n statusCode: res.status,\n details: json.error?.details,\n responseHeaders: res.headers,\n });\n }\n return json.data as T;\n } finally {\n clearTimeout(timer);\n }\n }\n\n /** Fetch the quote a buyer link points at. 404s for unknown/rotated tokens. */\n getQuote(token: string): Promise<BuyerQuote> {\n return this.request<BuyerQuote>('GET', `/api/v1/quote-links/${encodeURIComponent(token)}`);\n }\n\n /**\n * Tell the merchant the buyer opened the quote. Fire-and-forget: the server\n * throttles the resulting webhook to one per revision per 6h, and a failed\n * beacon must never break the page.\n */\n async recordView(token: string): Promise<void> {\n try {\n await this.request('POST', `/api/v1/quote-links/${encodeURIComponent(token)}/view`, {});\n } catch {\n /* telemetry only */\n }\n }\n\n addComment(token: string, input: { body: string; authorName?: string; authorEmail?: string }) {\n return this.request<QuoteComment>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/comments`,\n input,\n );\n }\n\n requestRevision(token: string, input: { message?: string; authorName?: string } = {}) {\n return this.request<BuyerQuote>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/request-revision`,\n input,\n );\n }\n\n decline(token: string, input: { reason?: string; authorName?: string } = {}) {\n return this.request<BuyerQuote>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/decline`,\n input,\n );\n }\n\n /**\n * Accept and get the checkout URL to send the buyer to.\n *\n * Re-accepting the same revision is idempotent and replays the same URL.\n * Accepting a superseded revision throws `revision_superseded` carrying\n * `currentRevisionId`; an expired offer throws `quote_expired`.\n */\n accept(token: string, input: AcceptQuoteInput): Promise<AcceptQuoteResult> {\n return this.request<AcceptQuoteResult>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/accept`,\n input,\n );\n }\n\n /**\n * Send a buyer who already accepted back to a LIVE checkout.\n *\n * Checkout sessions expire after 30 minutes, so the URL returned by\n * `accept` is usually dead by the time the buyer returns to the link. This\n * re-opens a session against the same cart when it is (no re-pricing).\n * Throws `checkout_not_resumable` if the quote was never accepted, and\n * `checkout_completed` once it has been paid.\n */\n resumeCheckout(token: string): Promise<{ checkoutUrl: string }> {\n return this.request<{ checkoutUrl: string }>(\n 'POST',\n `/api/v1/quote-links/${encodeURIComponent(token)}/checkout`,\n {},\n );\n }\n\n /** URL of the PDF for the revision the buyer is looking at (server 302s to signed storage). */\n pdfUrl(token: string): string {\n return `${this.baseUrl}/api/v1/quote-links/${encodeURIComponent(token)}/pdf`;\n }\n\n // ── RFQ intake ─────────────────────────────────────────────────────────────\n\n getRequestForm(formToken: string): Promise<RequestFormConfig> {\n return this.request<RequestFormConfig>(\n 'GET',\n `/api/v1/quote-request-forms/${encodeURIComponent(formToken)}/public`,\n );\n }\n\n /**\n * Submit an RFQ. The response is deliberately thin (`{ submitted: true }`) —\n * anonymous callers learn no internal identifiers. A submission caught by\n * the honeypot returns the same shape without creating anything, so a bot\n * cannot tell it was detected; treat success as \"accepted\", not \"created\".\n */\n submitRequest(formToken: string, input: SubmitQuoteRequestInput): Promise<{ submitted: boolean }> {\n return this.request<{ submitted: boolean }>('POST', '/api/v1/quote-requests', {\n formToken,\n ...input,\n });\n }\n}\n\nexport function createQuotesClient(opts: QuotesClientOptions = {}): QuotesClient {\n return new QuotesClient(opts);\n}\n\n// ── money formatting ─────────────────────────────────────────────────────────\n\n/** Minor units → a display string, e.g. (104000, 'USD') → \"$1,040.00\". */\nexport function formatMoney(minorUnits: number, currency: string, locale?: string): string {\n return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(minorUnits / 100);\n}\n\n/**\n * Scale a line's quoted tax/discount to an edited quantity.\n *\n * MUST match `scaleLineCharges` in @commerce/quotes — the server recomputes\n * the accepted total with this rule, and any divergence shows the buyer one\n * total and charges another.\n */\nexport function scaleLineCharges(\n item: { quantity: number; taxAmount: number; discountAmount: number },\n quantity: number,\n): { taxAmount: number; discountAmount: number } {\n const base = item.quantity;\n if (!base || base <= 0 || quantity === base) {\n return { taxAmount: item.taxAmount, discountAmount: item.discountAmount };\n }\n const ratio = quantity / base;\n return {\n taxAmount: Math.round(item.taxAmount * ratio),\n discountAmount: Math.round(item.discountAmount * ratio),\n };\n}\n\n/** A subscription line with a free trial: nothing is charged for it at acceptance. */\nexport function isFreeTrialLine(item: { recurring?: { trialDays?: number | null } | null }): boolean {\n return (item.recurring?.trialDays ?? 0) > 0;\n}\n\n/**\n * What one line charges at acceptance, at `quantity`. MUST match\n * `lineDueAtAcceptance` in @commerce/quotes (shared test vectors).\n */\nexport function lineDueAtAcceptance(\n item: {\n unitPrice: number;\n quantity: number;\n taxAmount: number;\n discountAmount: number;\n recurring?: { trialDays?: number | null } | null;\n },\n quantity: number,\n): number {\n const { taxAmount, discountAmount } = scaleLineCharges(item, quantity);\n return (isFreeTrialLine(item) ? 0 : item.unitPrice * quantity) + taxAmount - discountAmount;\n}\n\n/** Effective total for a selection, using the same math the server will apply. */\nexport function computeSelectionTotal(\n items: Array<{\n id: string;\n unitPrice: number;\n quantity: number;\n taxAmount: number;\n discountAmount: number;\n optional: boolean;\n recurring?: { trialDays?: number | null } | null;\n }>,\n selections: Record<string, { selected: boolean; quantity: number }>,\n shippingTotal = 0,\n): number {\n let total = shippingTotal;\n for (const item of items) {\n const state = selections[item.id];\n if (item.optional && state && !state.selected) continue;\n const qty = state?.quantity ?? item.quantity;\n total += lineDueAtAcceptance(item, qty);\n }\n return total;\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { QuotesClient, computeSelectionTotal, createQuotesClient } from './client';\nimport { ThrottleQuotesError } from './errors';\nimport { createQuoteCart, type QuoteCart, type QuoteCartLine, type QuoteCartOptions } from './quote-cart';\nimport type {\n AcceptQuoteInput,\n BuyerQuote,\n ItemSelection,\n QuoteRequestItem,\n QuotesClientOptions,\n RequestFormConfig,\n SubmitQuoteRequestInput,\n} from './types';\n\nexport interface UseQuoteResult {\n quote: BuyerQuote | null;\n loading: boolean;\n error: ThrottleQuotesError | null;\n /** Re-fetch — call after a `revision_superseded` to show the current offer. */\n refresh: () => Promise<void>;\n /** Buyer's current line choices, keyed by item id. */\n selections: Record<string, { selected: boolean; quantity: number }>;\n setSelected: (itemId: string, selected: boolean) => void;\n setQuantity: (itemId: string, quantity: number) => void;\n /** Live total for the current selections, using the server's own math. */\n total: number;\n accept: (\n input: Omit<AcceptQuoteInput, 'revisionId' | 'itemSelections'>,\n ) => Promise<{ checkoutUrl: string }>;\n /** Live checkout URL for a buyer returning to an already-accepted quote —\n * the one minted at acceptance has usually expired by then. */\n resumeCheckout: () => Promise<{ checkoutUrl: string }>;\n requestRevision: (message: string, authorName?: string) => Promise<void>;\n decline: (reason?: string, authorName?: string) => Promise<void>;\n addComment: (body: string, authorName?: string) => Promise<void>;\n pdfUrl: string;\n submitting: boolean;\n}\n\n/**\n * Everything the buyer quote page needs.\n *\n * Fires the view beacon once per mount, keeps line selections in local state,\n * and recomputes the total with the same rule the server applies at\n * acceptance — so the number the buyer sees is the number they are charged.\n */\nexport function useQuote(token: string, options: QuotesClientOptions = {}): UseQuoteResult {\n const client = useMemo(\n () => new QuotesClient(options),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [options.baseUrl, options.timeoutMs],\n );\n const [quote, setQuote] = useState<BuyerQuote | null>(null);\n const [loading, setLoading] = useState(true);\n const [submitting, setSubmitting] = useState(false);\n const [error, setError] = useState<ThrottleQuotesError | null>(null);\n const [selections, setSelections] = useState<Record<string, { selected: boolean; quantity: number }>>({});\n const beaconSent = useRef(false);\n\n const load = useCallback(async () => {\n setLoading(true);\n try {\n const q = await client.getQuote(token);\n setQuote(q);\n setError(null);\n // Seed selections from the revision's own defaults so a re-issue that\n // adds or locks lines resets the buyer's choices to the new offer.\n const next: Record<string, { selected: boolean; quantity: number }> = {};\n for (const item of q.currentRevision?.items ?? []) {\n next[item.id] = { selected: item.optional ? item.optionalSelected : true, quantity: item.quantity };\n }\n setSelections(next);\n } catch (e) {\n setError(e as ThrottleQuotesError);\n } finally {\n setLoading(false);\n }\n }, [client, token]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n useEffect(() => {\n if (!quote || beaconSent.current) return;\n beaconSent.current = true;\n void client.recordView(token);\n }, [client, quote, token]);\n\n const setSelected = useCallback((itemId: string, selected: boolean) => {\n setSelections((prev) => ({ ...prev, [itemId]: { ...(prev[itemId] ?? { quantity: 1 }), selected } }));\n }, []);\n\n const setQuantity = useCallback((itemId: string, quantity: number) => {\n setSelections((prev) => ({\n ...prev,\n [itemId]: { ...(prev[itemId] ?? { selected: true }), quantity: Math.max(1, Math.floor(quantity) || 1) },\n }));\n }, []);\n\n const total = useMemo(\n () =>\n computeSelectionTotal(\n quote?.currentRevision?.items ?? [],\n selections,\n quote?.currentRevision?.shippingTotal ?? 0,\n ),\n [quote, selections],\n );\n\n /** Only send selections the rep actually allows the buyer to change. */\n const buildSelections = useCallback((): ItemSelection[] => {\n const items = quote?.currentRevision?.items ?? [];\n const out: ItemSelection[] = [];\n for (const item of items) {\n const state = selections[item.id];\n if (!state) continue;\n const sel: ItemSelection = { itemId: item.id };\n let include = false;\n if (item.optional && state.selected !== item.optionalSelected) {\n sel.selected = state.selected;\n include = true;\n }\n if (item.qtyEditable && state.quantity !== item.quantity) {\n sel.quantity = state.quantity;\n include = true;\n }\n if (include) out.push(sel);\n }\n return out;\n }, [quote, selections]);\n\n const guard = useCallback(\n async <T,>(fn: () => Promise<T>): Promise<T> => {\n setSubmitting(true);\n try {\n const result = await fn();\n setError(null);\n return result;\n } catch (e) {\n const err = e as ThrottleQuotesError;\n setError(err);\n // A superseded offer means our copy is stale — pull the current one so\n // the buyer sees what they can actually accept (spec §8.1).\n if (err.isSuperseded) void load();\n throw err;\n } finally {\n setSubmitting(false);\n }\n },\n [load],\n );\n\n const accept = useCallback(\n async (input: Omit<AcceptQuoteInput, 'revisionId' | 'itemSelections'>) => {\n const revisionId = quote?.currentRevision?.id;\n if (!revisionId) throw new Error('useQuote: no current revision to accept');\n const itemSelections = buildSelections();\n const res = await guard(() =>\n client.accept(token, {\n ...input,\n revisionId,\n ...(itemSelections.length ? { itemSelections } : {}),\n }),\n );\n return { checkoutUrl: res.checkoutUrl };\n },\n [buildSelections, client, guard, quote, token],\n );\n\n const resumeCheckout = useCallback(\n async () => guard(() => client.resumeCheckout(token)),\n [client, guard, token],\n );\n\n const requestRevision = useCallback(\n async (message: string, authorName?: string) => {\n const q = await guard(() => client.requestRevision(token, { message, authorName }));\n setQuote(q);\n },\n [client, guard, token],\n );\n\n const decline = useCallback(\n async (reason?: string, authorName?: string) => {\n const q = await guard(() => client.decline(token, { reason, authorName }));\n setQuote(q);\n },\n [client, guard, token],\n );\n\n const addComment = useCallback(\n async (body: string, authorName?: string) => {\n await guard(() => client.addComment(token, { body, authorName }));\n await load();\n },\n [client, guard, load, token],\n );\n\n return {\n quote,\n loading,\n error,\n refresh: load,\n selections,\n setSelected,\n setQuantity,\n total,\n accept,\n resumeCheckout,\n requestRevision,\n decline,\n addComment,\n pdfUrl: client.pdfUrl(token),\n submitting,\n };\n}\n\nexport interface UseQuoteRequestFormResult {\n config: RequestFormConfig | null;\n loading: boolean;\n error: ThrottleQuotesError | null;\n submitting: boolean;\n submitted: boolean;\n submit: (input: SubmitQuoteRequestInput) => Promise<void>;\n /** Mount time; pass through so the API's minimum-fill-time check passes. */\n formLoadedAt: number;\n}\n\n/** Loads a hosted RFQ form's config and submits it, with bot fields handled. */\nexport function useQuoteRequestForm(\n formToken: string,\n options: QuotesClientOptions = {},\n): UseQuoteRequestFormResult {\n const client = useMemo(\n () => new QuotesClient(options),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [options.baseUrl, options.timeoutMs],\n );\n const [config, setConfig] = useState<RequestFormConfig | null>(null);\n const [loading, setLoading] = useState(true);\n const [submitting, setSubmitting] = useState(false);\n const [submitted, setSubmitted] = useState(false);\n const [error, setError] = useState<ThrottleQuotesError | null>(null);\n const formLoadedAt = useRef(Date.now()).current;\n\n useEffect(() => {\n let cancelled = false;\n (async () => {\n try {\n const cfg = await client.getRequestForm(formToken);\n if (!cancelled) setConfig(cfg);\n } catch (e) {\n if (!cancelled) setError(e as ThrottleQuotesError);\n } finally {\n if (!cancelled) setLoading(false);\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [client, formToken]);\n\n const submit = useCallback(\n async (input: SubmitQuoteRequestInput) => {\n setSubmitting(true);\n try {\n await client.submitRequest(formToken, { formLoadedAt, ...input });\n setSubmitted(true);\n setError(null);\n } catch (e) {\n setError(e as ThrottleQuotesError);\n throw e;\n } finally {\n setSubmitting(false);\n }\n },\n [client, formLoadedAt, formToken],\n );\n\n return { config, loading, error, submitting, submitted, submit, formLoadedAt };\n}\n\nexport { createQuotesClient };\n\nexport interface UseQuoteCartResult {\n lines: QuoteCartLine[];\n /** Total quantity across lines — for a header badge. */\n count: number;\n add: (item: QuoteRequestItem) => void;\n addMany: (items: QuoteRequestItem[]) => void;\n update: (key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>) => void;\n remove: (key: string) => void;\n clear: () => void;\n /** The underlying cart, for use outside React (an Add to Quote button in plain JS). */\n cart: QuoteCart;\n}\n\n/**\n * A quote cart the buyer fills while browsing, then submits as one request.\n *\n * The cart persists across page views, so an Add to Quote button on a product\n * page and a Request quote page elsewhere on the site see the same lines.\n * Pass the result of `toRequestItems()` (or `lines`) to\n * `useQuoteRequestForm().submit` as `items`.\n */\nexport function useQuoteCart(options: QuoteCartOptions = {}): UseQuoteCartResult {\n const cart = useMemo(\n () => createQuoteCart(options),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [options.storageKey, options.maxLines],\n );\n // Seeded empty rather than from storage: reading localStorage during render\n // makes the server and the first client render disagree, which is a\n // hydration mismatch. The effect below fills it in immediately after mount.\n const [lines, setLines] = useState<QuoteCartLine[]>([]);\n\n useEffect(() => {\n setLines(cart.read());\n return cart.subscribe(setLines);\n }, [cart]);\n\n return {\n lines,\n count: lines.reduce((sum, line) => sum + line.quantity, 0),\n add: useCallback((item: QuoteRequestItem) => void cart.add(item), [cart]),\n addMany: useCallback((items: QuoteRequestItem[]) => void cart.addMany(items), [cart]),\n update: useCallback(\n (key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>) => void cart.update(key, patch),\n [cart],\n ),\n remove: useCallback((key: string) => void cart.remove(key), [cart]),\n clear: useCallback(() => cart.clear(), [cart]),\n cart,\n };\n}\n","import type { QuoteRequestItem } from './types';\n\n/**\n * A quote cart: the buyer's running list of things to be quoted, built up\n * while they browse.\n *\n * Throttle stores no catalog, so the storefront owns product data and hands\n * lines in — the same arrangement as the shopping cart. Nothing here is sent\n * until `read()` is submitted through `QuotesClient.submitRequest`, and the\n * amounts a storefront attaches are the buyer's TARGET price, never a price\n * we will honour.\n *\n * Lines are keyed by `sku ?? referenceId ?? name`, so adding the same product\n * twice increases its quantity instead of stacking duplicate rows.\n */\n\nexport interface QuoteCartLine extends QuoteRequestItem {\n /** Stable identity for updates and removals. */\n key: string;\n quantity: number;\n}\n\nexport interface QuoteCartStorage {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nexport interface QuoteCartOptions {\n /** Namespacing key, so two storefronts on one origin cannot collide. */\n storageKey?: string;\n /**\n * Where the cart survives page navigation. Defaults to `localStorage` in a\n * browser and to memory everywhere else (SSR, tests) — a cart that throws\n * during server rendering would take the whole page down.\n */\n storage?: QuoteCartStorage | null;\n /** Refuse to grow past this many distinct lines. The API caps at 100. */\n maxLines?: number;\n}\n\nconst DEFAULT_KEY = 'throttle:quote-cart';\nconst DEFAULT_MAX_LINES = 100;\n\nfunction memoryStorage(): QuoteCartStorage {\n const map = new Map<string, string>();\n return {\n getItem: (key) => map.get(key) ?? null,\n setItem: (key, value) => void map.set(key, value),\n removeItem: (key) => void map.delete(key),\n };\n}\n\nfunction defaultStorage(): QuoteCartStorage {\n try {\n if (typeof globalThis !== 'undefined' && (globalThis as any).localStorage) {\n return (globalThis as any).localStorage as QuoteCartStorage;\n }\n } catch {\n // Safari in private mode throws on access rather than returning null.\n }\n return memoryStorage();\n}\n\nfunction lineKey(item: QuoteRequestItem): string {\n const raw = item.sku?.trim() || item.referenceId?.trim() || item.name.trim();\n return raw.toLowerCase();\n}\n\nfunction normalizeQuantity(value: number | null | undefined): number {\n const n = Math.floor(Number(value ?? 1));\n return Number.isFinite(n) && n > 0 ? n : 1;\n}\n\nexport interface QuoteCart {\n /** Current lines, oldest first. */\n read(): QuoteCartLine[];\n /** Total quantity across lines — what a header badge should show. */\n count(): number;\n /** Add a line, merging quantities into an existing one with the same key. */\n add(item: QuoteRequestItem): QuoteCartLine[];\n /** Add several at once (a \"quote this whole cart\" button). */\n addMany(items: QuoteRequestItem[]): QuoteCartLine[];\n /** Change any field of a line. A quantity below 1 removes it. */\n update(key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>): QuoteCartLine[];\n remove(key: string): QuoteCartLine[];\n clear(): void;\n /** Subscribe to every change. Returns an unsubscribe function. */\n subscribe(listener: (lines: QuoteCartLine[]) => void): () => void;\n /** The `items` payload for `QuotesClient.submitRequest`. */\n toRequestItems(): QuoteRequestItem[];\n}\n\nexport function createQuoteCart(options: QuoteCartOptions = {}): QuoteCart {\n const storageKey = options.storageKey ?? DEFAULT_KEY;\n const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n const storage = options.storage === null ? memoryStorage() : (options.storage ?? defaultStorage());\n const listeners = new Set<(lines: QuoteCartLine[]) => void>();\n\n function load(): QuoteCartLine[] {\n try {\n const raw = storage.getItem(storageKey);\n if (!raw) return [];\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n // Storage is buyer-writable — anything shaped wrong is dropped rather\n // than trusted into a submission.\n return parsed\n .filter((line: any) => line && typeof line.name === 'string' && line.name.trim())\n .map((line: any) => ({\n ...line,\n key: typeof line.key === 'string' && line.key ? line.key : lineKey(line),\n quantity: normalizeQuantity(line.quantity),\n }))\n .slice(0, maxLines);\n } catch {\n return [];\n }\n }\n\n function save(lines: QuoteCartLine[]): QuoteCartLine[] {\n try {\n storage.setItem(storageKey, JSON.stringify(lines));\n } catch {\n // Quota or a disabled store — the cart still works for this page view.\n }\n for (const listener of listeners) listener(lines);\n return lines;\n }\n\n return {\n read: load,\n\n count() {\n return load().reduce((sum, line) => sum + line.quantity, 0);\n },\n\n add(item) {\n return this.addMany([item]);\n },\n\n addMany(items) {\n const lines = load();\n for (const item of items) {\n if (!item?.name?.trim()) continue;\n const key = lineKey(item);\n const existing = lines.find((line) => line.key === key);\n if (existing) {\n existing.quantity += normalizeQuantity(item.quantity);\n // Later detail wins — a product page knows more than a listing card.\n if (item.note) existing.note = item.note;\n if (item.targetUnitPrice != null) existing.targetUnitPrice = item.targetUnitPrice;\n continue;\n }\n if (lines.length >= maxLines) break;\n lines.push({ ...item, key, quantity: normalizeQuantity(item.quantity) });\n }\n return save(lines);\n },\n\n update(key, patch) {\n const lines = load();\n const index = lines.findIndex((line) => line.key === key);\n if (index === -1) return lines;\n if (patch.quantity != null && Math.floor(Number(patch.quantity)) < 1) {\n lines.splice(index, 1);\n return save(lines);\n }\n lines[index] = {\n ...lines[index],\n ...patch,\n key,\n quantity: normalizeQuantity(patch.quantity ?? lines[index].quantity),\n };\n return save(lines);\n },\n\n remove(key) {\n return save(load().filter((line) => line.key !== key));\n },\n\n clear() {\n try {\n storage.removeItem(storageKey);\n } catch {\n // Ignore — the notification below still resets any subscriber.\n }\n for (const listener of listeners) listener([]);\n },\n\n subscribe(listener) {\n listeners.add(listener);\n return () => void listeners.delete(listener);\n },\n\n toRequestItems() {\n return load().map(({ key: _key, ...item }) => item);\n },\n };\n}\n","import { useState, type CSSProperties, type ReactNode } from 'react';\nimport { recurringTermsLabel } from '@core/cadence';\nimport { formatMoney, lineDueAtAcceptance } from './client';\nimport { useQuote } from './hooks';\nimport type { QuotesClientOptions } from './types';\n\nexport interface QuoteViewProps {\n /** The `qtl_…` token from the buyer's link. */\n token: string;\n options?: QuotesClientOptions;\n /**\n * Where to send the buyer after acceptance. Defaults to assigning\n * `window.location.href`, which is what the hosted page does.\n */\n onCheckout?: (checkoutUrl: string) => void;\n /** Rendered instead of the built-in markup while loading. */\n loadingFallback?: ReactNode;\n className?: string;\n}\n\nconst row: CSSProperties = { display: 'flex', justifyContent: 'space-between', gap: 16, padding: '8px 0' };\nconst muted: CSSProperties = { color: '#666', fontSize: 13 };\n\n/**\n * Drop-in buyer quote page: line items with optional toggles and quantity\n * editing, live totals, revision history, comment thread, and the\n * accept / request-changes / decline actions.\n *\n * Intentionally unstyled beyond a readable default — it is meant to be\n * restyled or replaced. `useQuote` carries all the behaviour if you want to\n * build your own presentation.\n */\nexport function QuoteView({ token, options, onCheckout, loadingFallback, className }: QuoteViewProps) {\n const q = useQuote(token, options ?? {});\n const [name, setName] = useState('');\n const [email, setEmail] = useState('');\n const [showAccept, setShowAccept] = useState(false);\n const [comment, setComment] = useState('');\n\n if (q.loading) return <>{loadingFallback ?? <p>Loading quote…</p>}</>;\n if (!q.quote) {\n return <p>This quote link is no longer available.</p>;\n }\n\n const quote = q.quote;\n const rev = quote.currentRevision;\n const currency = quote.currency;\n const ordered = quote.status === 'ordered';\n const accepted = quote.status === 'accepted';\n const canAct = quote.status === 'ready';\n // The subscription lines the buyer is keeping (an unticked optional plan drops out).\n const recurringKept = rev\n ? rev.items.filter((i) => i.recurring && (!i.optional || (q.selections[i.id]?.selected ?? true)))\n : [];\n\n const handleResume = async () => {\n const { checkoutUrl } = await q.resumeCheckout();\n if (onCheckout) onCheckout(checkoutUrl);\n else if (typeof window !== 'undefined') window.location.href = checkoutUrl;\n };\n\n const handleAccept = async () => {\n const { checkoutUrl } = await q.accept({ acceptedByName: name, acceptedByEmail: email });\n if (onCheckout) onCheckout(checkoutUrl);\n else if (typeof window !== 'undefined') window.location.href = checkoutUrl;\n };\n\n return (\n <div className={className}>\n <header>\n {quote.branding?.displayName ? <p style={muted}>{quote.branding.displayName}</p> : null}\n <h1>Quote {quote.quoteNumber}</h1>\n <p style={muted}>{statusLabel(quote.status)}</p>\n {quote.buyerReference ? <p style={muted}>Your reference: {quote.buyerReference}</p> : null}\n {rev ? (\n <p style={muted}>\n Revision {rev.revisionNumber}\n {rev.expiresAt ? ` · valid until ${new Date(rev.expiresAt).toLocaleDateString()}` : null}\n {' · '}\n <a href={q.pdfUrl}>Download PDF</a>\n </p>\n ) : null}\n </header>\n\n {q.error ? (\n <p role=\"alert\">\n {q.error.isSuperseded\n ? 'This quote was updated — showing the latest version.'\n : q.error.message}\n </p>\n ) : null}\n\n {rev ? (\n <table style={{ width: '100%', borderCollapse: 'collapse' }}>\n <thead>\n <tr>\n <th align=\"left\">Item</th>\n <th align=\"right\">Unit price</th>\n <th align=\"right\">Qty</th>\n <th align=\"right\">Total</th>\n </tr>\n </thead>\n <tbody>\n {rev.items.map((item) => {\n const state = q.selections[item.id] ?? { selected: true, quantity: item.quantity };\n const included = item.optional ? state.selected : true;\n return (\n <tr key={item.id}>\n <td>\n {item.optional ? (\n <label>\n <input\n type=\"checkbox\"\n checked={state.selected}\n disabled={!canAct}\n onChange={(e) => q.setSelected(item.id, e.target.checked)}\n />{' '}\n {item.name} <span style={muted}>optional</span>\n </label>\n ) : (\n item.name\n )}\n {item.description ? <div style={muted}>{item.description}</div> : null}\n {item.recurring ? (\n <div style={muted}>\n Subscription · {recurringTermsLabel(item.recurring, formatMoney(item.unitPrice * state.quantity, currency))}\n </div>\n ) : null}\n </td>\n <td align=\"right\">{formatMoney(item.unitPrice, currency)}</td>\n <td align=\"right\">\n {item.qtyEditable && canAct ? (\n <input\n type=\"number\"\n min={1}\n value={state.quantity}\n disabled={!included}\n aria-label={`Quantity for ${item.name}`}\n onChange={(e) => q.setQuantity(item.id, Number(e.target.value))}\n style={{ width: 64 }}\n />\n ) : (\n state.quantity\n )}\n </td>\n <td align=\"right\">\n {formatMoney(included ? lineDueAtAcceptance(item, state.quantity) : 0, currency)}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n ) : null}\n\n {rev ? (\n <section>\n <div style={row}>\n <strong>{recurringKept.length > 0 ? 'Due at acceptance' : 'Total'}</strong>\n <strong>{formatMoney(q.total, currency)}</strong>\n </div>\n {recurringKept.length > 0 ? (\n <div>\n <p style={muted}>Recurring after</p>\n {recurringKept.map((i) => (\n <p key={i.id} style={muted}>\n {i.name}: {recurringTermsLabel(i.recurring!, formatMoney(i.unitPrice * (q.selections[i.id]?.quantity ?? i.quantity), currency))}\n </p>\n ))}\n </div>\n ) : null}\n <p style={muted}>{termsLabel(rev.paymentTerms, rev.depositAmount, currency)}</p>\n {rev.leadTime ? <p style={muted}>Lead time: {rev.leadTime}</p> : null}\n {rev.buyerNote ? <blockquote>{rev.buyerNote}</blockquote> : null}\n </section>\n ) : null}\n\n {ordered ? (\n <p>\n Ordered{quote.orderId ? ` — order ${quote.orderId}` : ''}. Thank you.\n </p>\n ) : accepted ? (\n // Accepted but not yet paid. The checkout session minted at acceptance\n // expires in 30 minutes, so this asks the server for a live one rather\n // than linking to a URL that is probably dead.\n <section>\n <p>You accepted this quote — payment is not finished yet.</p>\n <button type=\"button\" disabled={q.submitting} onClick={() => void handleResume()}>\n Continue to checkout\n </button>\n </section>\n ) : canAct ? (\n <section>\n {showAccept ? (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n void handleAccept();\n }}\n >\n <label>\n Your name\n <input value={name} onChange={(e) => setName(e.target.value)} required />\n </label>\n <label>\n Email\n <input type=\"email\" value={email} onChange={(e) => setEmail(e.target.value)} required />\n </label>\n <button type=\"submit\" disabled={q.submitting || !name || !email}>\n Confirm &amp; continue to payment\n </button>\n </form>\n ) : (\n <button type=\"button\" onClick={() => setShowAccept(true)}>\n Accept &amp; checkout\n </button>\n )}\n <button\n type=\"button\"\n disabled={q.submitting}\n onClick={() => void q.requestRevision('Please revise this quote.', name || undefined)}\n >\n Request changes\n </button>\n <button type=\"button\" disabled={q.submitting} onClick={() => void q.decline(undefined, name || undefined)}>\n Decline\n </button>\n </section>\n ) : null}\n\n {quote.revisionHistory.length > 0 ? (\n <details>\n <summary>Previous versions ({quote.revisionHistory.length})</summary>\n <ul>\n {quote.revisionHistory.map((r) => (\n <li key={r.id}>\n Revision {r.revisionNumber} — {formatMoney(r.total, currency)}\n {r.changeSummary ? ` · ${r.changeSummary}` : ''}\n </li>\n ))}\n </ul>\n </details>\n ) : null}\n\n <section>\n <h2>Questions</h2>\n {quote.comments.map((c) => (\n <p key={c.id}>\n <strong>{c.authorName ?? (c.authorType === 'merchant' ? 'Seller' : 'You')}:</strong> {c.body}\n </p>\n ))}\n <form\n onSubmit={(e) => {\n e.preventDefault();\n const body = comment.trim();\n if (!body) return;\n setComment('');\n void q.addComment(body, name || undefined);\n }}\n >\n <input\n value={comment}\n onChange={(e) => setComment(e.target.value)}\n placeholder=\"Ask a question…\"\n aria-label=\"Ask a question\"\n />\n <button type=\"submit\" disabled={q.submitting || !comment.trim()}>\n Send\n </button>\n </form>\n </section>\n </div>\n );\n}\n\nfunction statusLabel(status: string): string {\n switch (status) {\n case 'in_progress':\n return 'We are preparing your quote';\n case 'ready':\n return 'Ready for your review';\n case 'being_revised':\n return 'We are revising this quote';\n case 'accepted':\n return 'Accepted — completing payment';\n case 'ordered':\n return 'Ordered';\n case 'declined':\n return 'Declined';\n case 'expired':\n return 'This quote has expired — request updated pricing';\n default:\n return status;\n }\n}\n\nfunction termsLabel(\n terms: { mode: string; netN?: number; balanceNetN?: number },\n depositAmount: number | null,\n currency: string,\n): string {\n if (terms.mode === 'net_terms') return `Payment terms: Net ${terms.netN}`;\n if (terms.mode === 'deposit_balance') {\n const dep = depositAmount != null ? formatMoney(depositAmount, currency) : 'a deposit';\n return `${dep} due today, balance Net ${terms.balanceNetN}`;\n }\n return 'Payment in full';\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","import { isIntervalUnit, isNamedInterval, type CadenceLike, type NamedInterval } from './cadence';\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}\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\". `each` is the amount billed every period (renewal price ×\n * seats), already formatted, so each surface keeps its own money style.\n */\nexport function recurringTermsLabel(terms: RecurringTermsLike, each: string): string {\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","import { useState, type CSSProperties, type ReactNode } from 'react';\nimport { useQuoteRequestForm } from './hooks';\nimport type { QuoteRequestItem, QuotesClientOptions } from './types';\n\nexport interface QuoteRequestFormProps {\n /** The `qrf_…` token from the merchant's hosted form URL. */\n formToken: string;\n options?: QuotesClientOptions;\n /**\n * Turnstile token, when the form requires it. Render the widget yourself and\n * pass its token through — this package deliberately pulls in no Cloudflare\n * script. If `config.fields.requireTurnstile` is set and this is empty, the\n * API fails the submission closed.\n */\n turnstileToken?: string;\n /**\n * Lines to submit with the request — typically `useQuoteCart().lines` after\n * the buyer has been adding products while browsing. When present, the\n * written message is optional.\n */\n items?: QuoteRequestItem[];\n onSubmitted?: () => void;\n loadingFallback?: ReactNode;\n className?: string;\n}\n\n/** Off-screen but still focusable-free; bots fill it, humans never see it. */\nconst honeypotStyle: CSSProperties = {\n position: 'absolute',\n width: 1,\n height: 1,\n padding: 0,\n margin: -1,\n overflow: 'hidden',\n clip: 'rect(0 0 0 0)',\n whiteSpace: 'nowrap',\n border: 0,\n};\n\n/**\n * Hosted RFQ intake form.\n *\n * Bot protection is wired in for you: the honeypot field and the mount-time\n * anchor that feeds the API's minimum-fill-time check. A honeypot hit returns\n * a success-shaped response and creates nothing, so `onSubmitted` fires either\n * way — by design, a bot must not learn it was caught.\n */\nexport function QuoteRequestForm({\n formToken,\n options,\n turnstileToken,\n items,\n onSubmitted,\n loadingFallback,\n className,\n}: QuoteRequestFormProps) {\n const f = useQuoteRequestForm(formToken, options ?? {});\n // Lines the host collected while the buyer browsed (see `useQuoteCart`).\n // With any of these present the written message becomes optional: the ask\n // is the list.\n const lines = items ?? [];\n const [values, setValues] = useState({\n email: '',\n name: '',\n companyName: '',\n phone: '',\n buyerReference: '',\n message: '',\n website: '', // honeypot\n });\n\n const set = (k: keyof typeof values) => (e: { target: { value: string } }) =>\n setValues((prev) => ({ ...prev, [k]: e.target.value }));\n\n if (f.loading) return <>{loadingFallback ?? <p>Loading…</p>}</>;\n if (!f.config) return <p>This request form is not available.</p>;\n\n if (f.submitted) {\n const redirect = f.config.redirectUrl;\n // Re-check the scheme client-side even though the API rejects non-http(s)\n // on write — a stale row must not be able to script the host page.\n if (redirect && /^https?:\\/\\//i.test(redirect) && typeof window !== 'undefined') {\n window.location.href = redirect;\n }\n return <p>{f.config.successMessage ?? 'Thanks — we will be in touch shortly.'}</p>;\n }\n\n const fields = f.config.fields ?? {};\n\n return (\n <form\n className={className}\n onSubmit={(e) => {\n e.preventDefault();\n void f\n .submit({\n email: values.email,\n message: values.message || null,\n ...(lines.length > 0 ? { items: lines } : {}),\n name: values.name || null,\n companyName: fields.companyName ? values.companyName || null : null,\n phone: fields.phone ? values.phone || null : null,\n buyerReference: fields.buyerReference ? values.buyerReference || null : null,\n website: values.website || null,\n ...(turnstileToken ? { turnstileToken } : {}),\n })\n .then(() => onSubmitted?.())\n .catch(() => {\n /* surfaced via f.error */\n });\n }}\n >\n {f.config.title ? <h1>{f.config.title}</h1> : null}\n {f.config.introText ? <p>{f.config.introText}</p> : null}\n {f.error ? <p role=\"alert\">{f.error.message}</p> : null}\n\n <label>\n Email\n <input type=\"email\" value={values.email} onChange={set('email')} required />\n </label>\n <label>\n Name\n <input value={values.name} onChange={set('name')} />\n </label>\n {fields.companyName ? (\n <label>\n Company\n <input value={values.companyName} onChange={set('companyName')} />\n </label>\n ) : null}\n {fields.phone ? (\n <label>\n Phone\n <input value={values.phone} onChange={set('phone')} />\n </label>\n ) : null}\n {fields.buyerReference ? (\n <label>\n Your reference #\n <input value={values.buyerReference} onChange={set('buyerReference')} />\n </label>\n ) : null}\n {lines.length > 0 ? (\n <ul aria-label=\"Items on this request\">\n {lines.map((line, index) => (\n <li key={line.sku ?? line.referenceId ?? `${line.name}-${index}`}>\n {line.quantity ?? 1} × {line.name}\n {line.sku ? ` (${line.sku})` : ''}\n {line.note ? ` — ${line.note}` : ''}\n </li>\n ))}\n </ul>\n ) : null}\n <label>\n {lines.length > 0 ? 'Anything else we should know?' : 'What do you need?'}\n <textarea\n value={values.message}\n onChange={set('message')}\n required={lines.length === 0}\n />\n </label>\n\n <div style={honeypotStyle} aria-hidden=\"true\">\n <label>\n Website\n <input\n name=\"website\"\n tabIndex={-1}\n autoComplete=\"off\"\n value={values.website}\n onChange={set('website')}\n />\n </label>\n </div>\n\n <button\n type=\"submit\"\n disabled={f.submitting || !values.email || (!values.message && lines.length === 0)}\n >\n {f.submitting ? 'Sending…' : 'Request a quote'}\n </button>\n </form>\n );\n}\n"],"mappings":"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","names":["useState","useState","useState","Fragment","jsx","jsxs","useState"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usethrottle/quotes",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Buyer-facing React components, hooks and server proxy for Throttle B2B quotes — hosted quote links and RFQ request forms.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -45,7 +45,8 @@
45
45
  "react-dom": "^19.1.0",
46
46
  "tsup": "^8.0.0",
47
47
  "typescript": "^5.7.0",
48
- "vitest": "^2.1.9"
48
+ "vitest": "^2.1.9",
49
+ "@core/cadence": "0.0.1"
49
50
  },
50
51
  "publishConfig": {
51
52
  "access": "public"