@burdenoff/website-sdk 2026.1013.2 → 2026.1021.1

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.
@@ -32,6 +32,168 @@ var __defProp = Object.defineProperty;
32
32
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
33
33
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
34
34
 
35
+ // src/components/pricing-catalog.ts
36
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
37
+ function isUuidLike(value) {
38
+ return UUID_PATTERN.test(value.trim());
39
+ }
40
+ var PricingProductKeyError = class extends Error {
41
+ constructor(productId) {
42
+ super(
43
+ `PricingPage was given the product id "${productId}", which matches no public product. publicPlans(productId) expects the product slug (for example "vibecontrols"); pass product.slug.`
44
+ );
45
+ this.name = "PricingProductKeyError";
46
+ }
47
+ };
48
+ function resolvePricingProductKey(productId, candidates) {
49
+ const trimmed = productId.trim();
50
+ if (!isUuidLike(trimmed)) return trimmed;
51
+ const wanted = trimmed.toLowerCase();
52
+ const match = candidates.find(
53
+ (c) => !!c && c.id.toLowerCase() === wanted && c.slug.trim().length > 0
54
+ );
55
+ return match ? match.slug.trim() : null;
56
+ }
57
+ var PERIOD_SUFFIX = /\s*[([]\s*(monthly|month|annual|annually|yearly|year)\s*[)\]]\s*$/i;
58
+ var PRODUCT_PREFIX = /^\S.*?\s[-–—]\s+(?=\S)/;
59
+ function cleanPlanDisplayName(name) {
60
+ const withoutPeriod = name.replace(PERIOD_SUFFIX, "").trim();
61
+ const withoutProduct = withoutPeriod.replace(PRODUCT_PREFIX, "").trim();
62
+ if (withoutProduct.length > 0) return withoutProduct;
63
+ if (withoutPeriod.length > 0) return withoutPeriod;
64
+ return name.trim();
65
+ }
66
+ function isFiniteNumber(value) {
67
+ return typeof value === "number" && Number.isFinite(value);
68
+ }
69
+ function planPriceIn(plan, currency) {
70
+ const code = currency.toUpperCase();
71
+ if (plan.price === 0) return 0;
72
+ if (plan.currency.toUpperCase() === code) return plan.price;
73
+ const prices = plan.currencyPrices;
74
+ if (!prices) return null;
75
+ const direct = prices[code];
76
+ if (isFiniteNumber(direct)) return direct;
77
+ const key = Object.keys(prices).find((k) => k.toUpperCase() === code);
78
+ const value = key === void 0 ? void 0 : prices[key];
79
+ return isFiniteNumber(value) ? value : null;
80
+ }
81
+ function availablePlanCurrencies(plans) {
82
+ if (plans.length === 0) return [];
83
+ const all = /* @__PURE__ */ new Set();
84
+ for (const plan of plans) {
85
+ all.add(plan.currency.toUpperCase());
86
+ for (const key of Object.keys(plan.currencyPrices ?? {})) {
87
+ all.add(key.toUpperCase());
88
+ }
89
+ }
90
+ const usable = [...all].filter(
91
+ (code) => plans.every((plan) => planPriceIn(plan, code) !== null)
92
+ );
93
+ const base = plans.find((p) => p.price > 0)?.currency.toUpperCase();
94
+ return usable.sort((a, b) => {
95
+ if (a === base) return -1;
96
+ if (b === base) return 1;
97
+ return a.localeCompare(b);
98
+ });
99
+ }
100
+ var EURO_REGIONS = [
101
+ "AT",
102
+ "BE",
103
+ "CY",
104
+ "DE",
105
+ "EE",
106
+ "ES",
107
+ "FI",
108
+ "FR",
109
+ "GR",
110
+ "HR",
111
+ "IE",
112
+ "IT",
113
+ "LT",
114
+ "LU",
115
+ "LV",
116
+ "MT",
117
+ "NL",
118
+ "PT",
119
+ "SI",
120
+ "SK"
121
+ ];
122
+ var REGION_CURRENCY = Object.freeze({
123
+ AU: "AUD",
124
+ BR: "BRL",
125
+ CA: "CAD",
126
+ CH: "CHF",
127
+ CN: "CNY",
128
+ DK: "DKK",
129
+ GB: "GBP",
130
+ HK: "HKD",
131
+ IN: "INR",
132
+ JP: "JPY",
133
+ KR: "KRW",
134
+ MX: "MXN",
135
+ MY: "MYR",
136
+ NO: "NOK",
137
+ NZ: "NZD",
138
+ PL: "PLN",
139
+ SE: "SEK",
140
+ SG: "SGD",
141
+ TH: "THB",
142
+ US: "USD",
143
+ ...Object.fromEntries(EURO_REGIONS.map((r) => [r, "EUR"]))
144
+ });
145
+ var TIME_ZONE_REGION = Object.freeze({
146
+ "Asia/Kolkata": "IN",
147
+ "Asia/Calcutta": "IN"
148
+ });
149
+ function regionOfLanguage(tag) {
150
+ const parts = tag.replace(/_/g, "-").split("-");
151
+ for (const part of parts.slice(1)) {
152
+ if (/^[A-Za-z]{2}$/.test(part)) return part.toUpperCase();
153
+ }
154
+ return null;
155
+ }
156
+ function detectRegionalCurrency(available, locale) {
157
+ const sellable = new Set(available.map((c) => c.toUpperCase()));
158
+ const regions = [];
159
+ const tzRegion = locale.timeZone ? TIME_ZONE_REGION[locale.timeZone] : null;
160
+ if (tzRegion) regions.push(tzRegion);
161
+ for (const tag of locale.languages ?? []) {
162
+ const region = regionOfLanguage(tag);
163
+ if (region) regions.push(region);
164
+ }
165
+ for (const region of regions) {
166
+ const currency = REGION_CURRENCY[region];
167
+ if (currency && sellable.has(currency)) return currency;
168
+ }
169
+ return null;
170
+ }
171
+ function formatCatalogPrice(amount, currency) {
172
+ const code = currency.toUpperCase();
173
+ const whole = amount % 1 === 0;
174
+ try {
175
+ return new Intl.NumberFormat("en-US", {
176
+ style: "currency",
177
+ currency: code,
178
+ minimumFractionDigits: whole ? 0 : 2,
179
+ maximumFractionDigits: whole ? 0 : 2
180
+ }).format(amount);
181
+ } catch {
182
+ return `${code} ${whole ? amount.toString() : amount.toFixed(2)}`;
183
+ }
184
+ }
185
+ function plansForBillingCycle(plans, cycle) {
186
+ if (cycle === "monthly") {
187
+ return plans.filter((p) => p.duration === "monthly");
188
+ }
189
+ const yearly = plans.filter((p) => p.duration === "yearly");
190
+ if (yearly.some((p) => p.priceRaw === 0)) return yearly;
191
+ const free = plans.filter(
192
+ (p) => p.duration === "monthly" && p.priceRaw === 0
193
+ );
194
+ return [...free, ...yearly];
195
+ }
196
+
35
197
  // src/client/security.ts
36
198
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
37
199
  var RESERVED_HEADERS = /* @__PURE__ */ new Set([
@@ -255,6 +417,25 @@ function useWebSDKConfig() {
255
417
  const client = useWebSDK();
256
418
  return client.getConfig();
257
419
  }
420
+
421
+ // src/utils/product-slug.ts
422
+ var PRODUCT_SLUG_RENAMES = Object.freeze(
423
+ [Object.freeze({ legacy: "botlit", current: "botmanor" })]
424
+ );
425
+ Object.freeze(
426
+ Object.fromEntries(PRODUCT_SLUG_RENAMES.map((r) => [r.legacy, r.current]))
427
+ );
428
+ var ProductContext = React.createContext({
429
+ product: null,
430
+ loading: true,
431
+ error: null,
432
+ hasProvider: false,
433
+ refetch: () => {
434
+ }
435
+ });
436
+ function useProduct() {
437
+ return React.useContext(ProductContext);
438
+ }
258
439
  function canonicalFromLocation() {
259
440
  if (typeof window === "undefined" || !window.location) return void 0;
260
441
  const { origin, pathname } = window.location;
@@ -394,18 +575,49 @@ var GET_PLANS_QUERY = `
394
575
  }
395
576
  }
396
577
  `;
578
+ var PUBLIC_PRODUCT_KEYS_QUERY = `
579
+ query PricingProductKeys {
580
+ publicProducts(limit: 200) {
581
+ items {
582
+ id
583
+ slug
584
+ }
585
+ }
586
+ }
587
+ `;
588
+ var CURRENCY_STORAGE_KEY = "burdenoff.pricing.currency";
589
+ function readStoredCurrency() {
590
+ try {
591
+ if (typeof window === "undefined") return null;
592
+ return window.localStorage.getItem(CURRENCY_STORAGE_KEY);
593
+ } catch {
594
+ return null;
595
+ }
596
+ }
597
+ function storeCurrency(code) {
598
+ try {
599
+ if (typeof window === "undefined") return;
600
+ window.localStorage.setItem(CURRENCY_STORAGE_KEY, code);
601
+ } catch {
602
+ }
603
+ }
604
+ function readVisitorLocale() {
605
+ let timeZone = null;
606
+ try {
607
+ timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? null;
608
+ } catch {
609
+ timeZone = null;
610
+ }
611
+ const languages = typeof navigator === "undefined" ? [] : [...navigator.languages ?? [], navigator.language].filter(
612
+ (l) => typeof l === "string" && l.length > 0
613
+ );
614
+ return { timeZone, languages };
615
+ }
397
616
  function formatPrice(price, currencyCode = "USD", currencies) {
398
- const defaultCurrencies = {
399
- USD: { code: "USD", symbol: "$", position: "prefix" },
400
- EUR: { code: "EUR", symbol: "\u20AC", position: "prefix" },
401
- GBP: { code: "GBP", symbol: "\xA3", position: "prefix" },
402
- INR: { code: "INR", symbol: "\u20B9", position: "prefix" }
403
- };
404
- const currencyMap = currencies || defaultCurrencies;
405
- const currency = currencyMap[currencyCode] || {
406
- symbol: currencyCode,
407
- position: "prefix"
408
- };
617
+ const currency = currencies?.[currencyCode];
618
+ if (!currency) {
619
+ return formatCatalogPrice(price, currencyCode);
620
+ }
409
621
  const formattedNumber = price % 1 === 0 ? price.toString() : price.toFixed(2);
410
622
  return currency.position === "prefix" ? `${currency.symbol}${formattedNumber}` : `${formattedNumber}${currency.symbol}`;
411
623
  }
@@ -466,11 +678,12 @@ function getFallbackFeaturesForTier(tier, fallbackFeatures) {
466
678
  }
467
679
  function getMetadataForPlan(planName, priceRaw, planMetadata) {
468
680
  const normalizedName = planName.toLowerCase();
469
- if (planMetadata?.[normalizedName]) {
470
- const meta = planMetadata[normalizedName];
681
+ const cleanName = cleanPlanDisplayName(planName);
682
+ const meta = planMetadata?.[normalizedName] ?? planMetadata?.[cleanName.toLowerCase()];
683
+ if (meta) {
471
684
  return {
472
685
  tier: meta.tier || "basic",
473
- displayName: meta.displayName || planName,
686
+ displayName: meta.displayName || cleanName,
474
687
  defaultDescription: meta.defaultDescription || "Choose this plan",
475
688
  popular: meta.popular ?? false,
476
689
  ctaText: meta.ctaText || "Get Started",
@@ -484,7 +697,7 @@ function getMetadataForPlan(planName, priceRaw, planMetadata) {
484
697
  if (isPaidEnterpriseName) {
485
698
  return {
486
699
  tier: "professional",
487
- displayName: planName,
700
+ displayName: cleanName,
488
701
  defaultDescription: "Choose this plan",
489
702
  popular: false,
490
703
  ctaText: "Get Started",
@@ -493,7 +706,7 @@ function getMetadataForPlan(planName, priceRaw, planMetadata) {
493
706
  }
494
707
  return {
495
708
  tier,
496
- displayName: planName,
709
+ displayName: cleanName,
497
710
  defaultDescription: "Choose this plan",
498
711
  popular: tier === "professional",
499
712
  ctaText: tier === "enterprise" ? "Contact Sales" : "Get Started",
@@ -502,7 +715,7 @@ function getMetadataForPlan(planName, priceRaw, planMetadata) {
502
715
  }
503
716
  return {
504
717
  tier: "basic",
505
- displayName: planName,
718
+ displayName: cleanName,
506
719
  defaultDescription: "Choose this plan",
507
720
  popular: false,
508
721
  ctaText: "Get Started",
@@ -559,17 +772,14 @@ function extractDisplayFeatures(features, planName, fallbackFeatures) {
559
772
  }
560
773
  return [];
561
774
  }
562
- function transformToDisplayCard(plan, props) {
775
+ function transformToDisplayCard(plan, props, currency) {
563
776
  const metadata = getMetadataForPlan(
564
777
  plan.name,
565
778
  plan.price,
566
779
  props.planMetadata
567
780
  );
568
- const formattedPrice = formatPrice(
569
- plan.price,
570
- plan.currency,
571
- props.currencies
572
- );
781
+ const selectedPrice = currency ? planPriceIn(plan, currency) : null;
782
+ const formattedPrice = currency && selectedPrice !== null ? formatPrice(selectedPrice, currency.toUpperCase(), props.currencies) : formatPrice(plan.price, plan.currency, props.currencies);
573
783
  const interval = formatInterval(plan.duration, props.billingIntervals);
574
784
  const features = metadata.features && metadata.features.length > 0 ? metadata.features : extractDisplayFeatures(
575
785
  plan.features,
@@ -578,13 +788,14 @@ function transformToDisplayCard(plan, props) {
578
788
  );
579
789
  const contactUrl = props.contactUrl || "/contact";
580
790
  const ctaLink = metadata.ctaType === "contact" ? contactUrl : props.appLoginUrl;
791
+ const isQuoteBased = metadata.ctaType === "contact" && plan.price === 0;
581
792
  return {
582
793
  id: plan.id,
583
- name: plan.name,
794
+ name: metadata.displayName,
584
795
  description: metadata.defaultDescription,
585
- price: formattedPrice,
796
+ price: isQuoteBased ? "Custom" : formattedPrice,
586
797
  priceRaw: plan.price,
587
- interval,
798
+ interval: isQuoteBased ? "" : interval,
588
799
  duration: plan.duration,
589
800
  features,
590
801
  popular: metadata.popular,
@@ -602,8 +813,13 @@ function PricingSection(props) {
602
813
  } = props;
603
814
  const client = useWebSDK();
604
815
  const config = useWebSDKConfig();
605
- const resolvedProductId = props.productId || config.productId;
606
- const [allPlans, setAllPlans] = React.useState([]);
816
+ const { product: contextProduct } = useProduct();
817
+ const requestedProductId = (props.productId || config.productId || "").trim();
818
+ const contextKey = resolvePricingProductKey(requestedProductId, [
819
+ contextProduct
820
+ ]);
821
+ const productKey = contextKey ?? requestedProductId;
822
+ const [catalogPlans, setCatalogPlans] = React.useState([]);
607
823
  const [loading, setLoading] = React.useState(true);
608
824
  const [error, setError] = React.useState(null);
609
825
  const [billingCycle, setBillingCycle] = React.useState(
@@ -612,13 +828,38 @@ function PricingSection(props) {
612
828
  const [audience, setAudience] = React.useState(
613
829
  "individual"
614
830
  );
831
+ const [selectedCurrency, setSelectedCurrency] = React.useState(
832
+ props.defaultCurrency ? props.defaultCurrency.toUpperCase() : null
833
+ );
615
834
  React.useEffect(() => {
616
835
  let cancelled = false;
836
+ async function resolveKey() {
837
+ if (!productKey) {
838
+ throw new Error(
839
+ "PricingPage needs a product slug: pass productId or set WebSDKConfig.productId."
840
+ );
841
+ }
842
+ if (!isUuidLike(productKey)) return productKey;
843
+ const response = await client.query(
844
+ PUBLIC_PRODUCT_KEYS_QUERY
845
+ );
846
+ if (response.errors && response.errors.length > 0) {
847
+ throw new Error(response.errors[0].message);
848
+ }
849
+ const resolved = resolvePricingProductKey(
850
+ productKey,
851
+ response.data?.publicProducts?.items ?? []
852
+ );
853
+ if (!resolved) throw new PricingProductKeyError(productKey);
854
+ return resolved;
855
+ }
617
856
  async function loadPlans() {
618
857
  try {
619
858
  setLoading(true);
859
+ const key = await resolveKey();
860
+ if (cancelled) return;
620
861
  const response = await client.query(GET_PLANS_QUERY, {
621
- productId: resolvedProductId
862
+ productId: key
622
863
  });
623
864
  if (cancelled) return;
624
865
  if (response.errors && response.errors.length > 0) {
@@ -627,12 +868,20 @@ function PricingSection(props) {
627
868
  if (!response.data?.publicPlans) {
628
869
  throw new Error("No plans data received");
629
870
  }
630
- const cards = response.data.publicPlans.filter((plan) => plan.isActive).map((plan) => transformToDisplayCard(plan, props)).sort((a, b) => a.priceRaw - b.priceRaw);
631
- setAllPlans(cards);
871
+ const active = response.data.publicPlans.filter(
872
+ (plan) => plan.isActive
873
+ );
874
+ if (active.length === 0) {
875
+ throw new Error(
876
+ `publicPlans returned no active plans for product "${key}".`
877
+ );
878
+ }
879
+ setCatalogPlans(active);
632
880
  setError(null);
633
881
  } catch (err) {
634
882
  if (cancelled) return;
635
883
  console.error("Failed to load plans:", err);
884
+ setCatalogPlans([]);
636
885
  setError("Unable to load pricing information. Please try again later.");
637
886
  } finally {
638
887
  if (!cancelled) {
@@ -644,13 +893,38 @@ function PricingSection(props) {
644
893
  return () => {
645
894
  cancelled = true;
646
895
  };
647
- }, [client, resolvedProductId]);
896
+ }, [client, productKey]);
897
+ const currencyOptions = React.useMemo(
898
+ () => availablePlanCurrencies(catalogPlans),
899
+ [catalogPlans]
900
+ );
901
+ React.useEffect(() => {
902
+ if (selectedCurrency || currencyOptions.length < 2) return;
903
+ const stored = readStoredCurrency()?.toUpperCase();
904
+ if (stored && currencyOptions.includes(stored)) {
905
+ setSelectedCurrency(stored);
906
+ return;
907
+ }
908
+ const regional = detectRegionalCurrency(
909
+ currencyOptions,
910
+ readVisitorLocale()
911
+ );
912
+ if (regional) setSelectedCurrency(regional);
913
+ }, [currencyOptions, selectedCurrency]);
914
+ const activeCurrency = selectedCurrency && currencyOptions.includes(selectedCurrency) ? selectedCurrency : currencyOptions[0] ?? null;
915
+ const allPlans = catalogPlans.map((plan) => transformToDisplayCard(plan, props, activeCurrency)).sort((a, b) => {
916
+ const aCustom = a.price === "Custom" ? 1 : 0;
917
+ const bCustom = b.price === "Custom" ? 1 : 0;
918
+ return aCustom - bCustom || a.priceRaw - b.priceRaw;
919
+ });
920
+ const showCurrencySelector = props.showCurrencySelector !== false && currencyOptions.length > 1;
648
921
  const {
649
922
  individualPlans: individualAllPlans,
650
923
  enterprisePlans: enterpriseApiPlans
651
924
  } = splitPlansByAudience(allPlans, props.showAudienceTabs);
652
- const individualPlans = individualAllPlans.filter(
653
- (plan) => billingCycle === "monthly" ? plan.duration === "monthly" : plan.duration === "yearly"
925
+ const individualPlans = plansForBillingCycle(
926
+ individualAllPlans,
927
+ billingCycle
654
928
  );
655
929
  const enterpriseCards = enterpriseApiPlans.map((p) => {
656
930
  if (p.priceRaw === 0 && p.name.toLowerCase().includes("enterprise")) {
@@ -768,6 +1042,23 @@ function PricingSection(props) {
768
1042
  children: "Yearly"
769
1043
  }
770
1044
  )
1045
+ ] }) }),
1046
+ showCurrencySelector && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center mt-4", children: /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "inline-flex items-center gap-2 text-sm text-muted-foreground", children: [
1047
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Currency" }),
1048
+ /* @__PURE__ */ jsxRuntime.jsx(
1049
+ "select",
1050
+ {
1051
+ value: activeCurrency ?? "",
1052
+ onChange: (event) => {
1053
+ const code = event.target.value;
1054
+ setSelectedCurrency(code);
1055
+ storeCurrency(code);
1056
+ },
1057
+ className: "h-9 rounded-md border border-input bg-background px-3 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring",
1058
+ "data-testid": "pricing-currency-select",
1059
+ children: currencyOptions.map((code) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: code, children: code }, code))
1060
+ }
1061
+ )
771
1062
  ] }) })
772
1063
  ] }),
773
1064
  props.showAudienceTabs && audience === "enterprise" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-3xl text-center mb-8", children: [
@@ -953,8 +1244,14 @@ function PricingPage(props) {
953
1244
  }
954
1245
 
955
1246
  exports.PricingPage = PricingPage;
1247
+ exports.PricingProductKeyError = PricingProductKeyError;
956
1248
  exports.PricingSection = PricingSection;
1249
+ exports.availablePlanCurrencies = availablePlanCurrencies;
1250
+ exports.cleanPlanDisplayName = cleanPlanDisplayName;
1251
+ exports.isUuidLike = isUuidLike;
1252
+ exports.planPriceIn = planPriceIn;
957
1253
  exports.resolveEnterprisePlans = resolveEnterprisePlans;
1254
+ exports.resolvePricingProductKey = resolvePricingProductKey;
958
1255
  exports.splitPlansByAudience = splitPlansByAudience;
959
1256
  exports.transformFeatureToLabel = transformFeatureToLabel;
960
1257
  //# sourceMappingURL=pricing.js.map