@burdenoff/website-sdk 2026.1014.1 → 2026.1021.2

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.
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { createContext, useState, useEffect, useContext } from 'react';
2
+ import { createContext, useState, useEffect, useMemo, useContext } from 'react';
3
3
  import { jsx, jsxs } from 'react/jsx-runtime';
4
4
  import { Helmet } from 'react-helmet-async';
5
5
  import { Slot } from '@radix-ui/react-slot';
@@ -11,6 +11,168 @@ var __defProp = Object.defineProperty;
11
11
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
12
12
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
13
13
 
14
+ // src/components/pricing-catalog.ts
15
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16
+ function isUuidLike(value) {
17
+ return UUID_PATTERN.test(value.trim());
18
+ }
19
+ var PricingProductKeyError = class extends Error {
20
+ constructor(productId) {
21
+ super(
22
+ `PricingPage was given the product id "${productId}", which matches no public product. publicPlans(productId) expects the product slug (for example "vibecontrols"); pass product.slug.`
23
+ );
24
+ this.name = "PricingProductKeyError";
25
+ }
26
+ };
27
+ function resolvePricingProductKey(productId, candidates) {
28
+ const trimmed = productId.trim();
29
+ if (!isUuidLike(trimmed)) return trimmed;
30
+ const wanted = trimmed.toLowerCase();
31
+ const match = candidates.find(
32
+ (c) => !!c && c.id.toLowerCase() === wanted && c.slug.trim().length > 0
33
+ );
34
+ return match ? match.slug.trim() : null;
35
+ }
36
+ var PERIOD_SUFFIX = /\s*[([]\s*(monthly|month|annual|annually|yearly|year)\s*[)\]]\s*$/i;
37
+ var PRODUCT_PREFIX = /^\S.*?\s[-–—]\s+(?=\S)/;
38
+ function cleanPlanDisplayName(name) {
39
+ const withoutPeriod = name.replace(PERIOD_SUFFIX, "").trim();
40
+ const withoutProduct = withoutPeriod.replace(PRODUCT_PREFIX, "").trim();
41
+ if (withoutProduct.length > 0) return withoutProduct;
42
+ if (withoutPeriod.length > 0) return withoutPeriod;
43
+ return name.trim();
44
+ }
45
+ function isFiniteNumber(value) {
46
+ return typeof value === "number" && Number.isFinite(value);
47
+ }
48
+ function planPriceIn(plan, currency) {
49
+ const code = currency.toUpperCase();
50
+ if (plan.price === 0) return 0;
51
+ if (plan.currency.toUpperCase() === code) return plan.price;
52
+ const prices = plan.currencyPrices;
53
+ if (!prices) return null;
54
+ const direct = prices[code];
55
+ if (isFiniteNumber(direct)) return direct;
56
+ const key = Object.keys(prices).find((k) => k.toUpperCase() === code);
57
+ const value = key === void 0 ? void 0 : prices[key];
58
+ return isFiniteNumber(value) ? value : null;
59
+ }
60
+ function availablePlanCurrencies(plans) {
61
+ if (plans.length === 0) return [];
62
+ const all = /* @__PURE__ */ new Set();
63
+ for (const plan of plans) {
64
+ all.add(plan.currency.toUpperCase());
65
+ for (const key of Object.keys(plan.currencyPrices ?? {})) {
66
+ all.add(key.toUpperCase());
67
+ }
68
+ }
69
+ const usable = [...all].filter(
70
+ (code) => plans.every((plan) => planPriceIn(plan, code) !== null)
71
+ );
72
+ const base = plans.find((p) => p.price > 0)?.currency.toUpperCase();
73
+ return usable.sort((a, b) => {
74
+ if (a === base) return -1;
75
+ if (b === base) return 1;
76
+ return a.localeCompare(b);
77
+ });
78
+ }
79
+ var EURO_REGIONS = [
80
+ "AT",
81
+ "BE",
82
+ "CY",
83
+ "DE",
84
+ "EE",
85
+ "ES",
86
+ "FI",
87
+ "FR",
88
+ "GR",
89
+ "HR",
90
+ "IE",
91
+ "IT",
92
+ "LT",
93
+ "LU",
94
+ "LV",
95
+ "MT",
96
+ "NL",
97
+ "PT",
98
+ "SI",
99
+ "SK"
100
+ ];
101
+ var REGION_CURRENCY = Object.freeze({
102
+ AU: "AUD",
103
+ BR: "BRL",
104
+ CA: "CAD",
105
+ CH: "CHF",
106
+ CN: "CNY",
107
+ DK: "DKK",
108
+ GB: "GBP",
109
+ HK: "HKD",
110
+ IN: "INR",
111
+ JP: "JPY",
112
+ KR: "KRW",
113
+ MX: "MXN",
114
+ MY: "MYR",
115
+ NO: "NOK",
116
+ NZ: "NZD",
117
+ PL: "PLN",
118
+ SE: "SEK",
119
+ SG: "SGD",
120
+ TH: "THB",
121
+ US: "USD",
122
+ ...Object.fromEntries(EURO_REGIONS.map((r) => [r, "EUR"]))
123
+ });
124
+ var TIME_ZONE_REGION = Object.freeze({
125
+ "Asia/Kolkata": "IN",
126
+ "Asia/Calcutta": "IN"
127
+ });
128
+ function regionOfLanguage(tag) {
129
+ const parts = tag.replace(/_/g, "-").split("-");
130
+ for (const part of parts.slice(1)) {
131
+ if (/^[A-Za-z]{2}$/.test(part)) return part.toUpperCase();
132
+ }
133
+ return null;
134
+ }
135
+ function detectRegionalCurrency(available, locale) {
136
+ const sellable = new Set(available.map((c) => c.toUpperCase()));
137
+ const regions = [];
138
+ const tzRegion = locale.timeZone ? TIME_ZONE_REGION[locale.timeZone] : null;
139
+ if (tzRegion) regions.push(tzRegion);
140
+ for (const tag of locale.languages ?? []) {
141
+ const region = regionOfLanguage(tag);
142
+ if (region) regions.push(region);
143
+ }
144
+ for (const region of regions) {
145
+ const currency = REGION_CURRENCY[region];
146
+ if (currency && sellable.has(currency)) return currency;
147
+ }
148
+ return null;
149
+ }
150
+ function formatCatalogPrice(amount, currency) {
151
+ const code = currency.toUpperCase();
152
+ const whole = amount % 1 === 0;
153
+ try {
154
+ return new Intl.NumberFormat("en-US", {
155
+ style: "currency",
156
+ currency: code,
157
+ minimumFractionDigits: whole ? 0 : 2,
158
+ maximumFractionDigits: whole ? 0 : 2
159
+ }).format(amount);
160
+ } catch {
161
+ return `${code} ${whole ? amount.toString() : amount.toFixed(2)}`;
162
+ }
163
+ }
164
+ function plansForBillingCycle(plans, cycle) {
165
+ if (cycle === "monthly") {
166
+ return plans.filter((p) => p.duration === "monthly");
167
+ }
168
+ const yearly = plans.filter((p) => p.duration === "yearly");
169
+ if (yearly.some((p) => p.priceRaw === 0)) return yearly;
170
+ const free = plans.filter(
171
+ (p) => p.duration === "monthly" && p.priceRaw === 0
172
+ );
173
+ return [...free, ...yearly];
174
+ }
175
+
14
176
  // src/client/security.ts
15
177
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
16
178
  var RESERVED_HEADERS = /* @__PURE__ */ new Set([
@@ -234,6 +396,25 @@ function useWebSDKConfig() {
234
396
  const client = useWebSDK();
235
397
  return client.getConfig();
236
398
  }
399
+
400
+ // src/utils/product-slug.ts
401
+ var PRODUCT_SLUG_RENAMES = Object.freeze(
402
+ [Object.freeze({ legacy: "botlit", current: "botmanor" })]
403
+ );
404
+ Object.freeze(
405
+ Object.fromEntries(PRODUCT_SLUG_RENAMES.map((r) => [r.legacy, r.current]))
406
+ );
407
+ var ProductContext = createContext({
408
+ product: null,
409
+ loading: true,
410
+ error: null,
411
+ hasProvider: false,
412
+ refetch: () => {
413
+ }
414
+ });
415
+ function useProduct() {
416
+ return useContext(ProductContext);
417
+ }
237
418
  function canonicalFromLocation() {
238
419
  if (typeof window === "undefined" || !window.location) return void 0;
239
420
  const { origin, pathname } = window.location;
@@ -373,18 +554,49 @@ var GET_PLANS_QUERY = `
373
554
  }
374
555
  }
375
556
  `;
557
+ var PUBLIC_PRODUCT_KEYS_QUERY = `
558
+ query PricingProductKeys {
559
+ publicProducts(limit: 200) {
560
+ items {
561
+ id
562
+ slug
563
+ }
564
+ }
565
+ }
566
+ `;
567
+ var CURRENCY_STORAGE_KEY = "burdenoff.pricing.currency";
568
+ function readStoredCurrency() {
569
+ try {
570
+ if (typeof window === "undefined") return null;
571
+ return window.localStorage.getItem(CURRENCY_STORAGE_KEY);
572
+ } catch {
573
+ return null;
574
+ }
575
+ }
576
+ function storeCurrency(code) {
577
+ try {
578
+ if (typeof window === "undefined") return;
579
+ window.localStorage.setItem(CURRENCY_STORAGE_KEY, code);
580
+ } catch {
581
+ }
582
+ }
583
+ function readVisitorLocale() {
584
+ let timeZone = null;
585
+ try {
586
+ timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? null;
587
+ } catch {
588
+ timeZone = null;
589
+ }
590
+ const languages = typeof navigator === "undefined" ? [] : [...navigator.languages ?? [], navigator.language].filter(
591
+ (l) => typeof l === "string" && l.length > 0
592
+ );
593
+ return { timeZone, languages };
594
+ }
376
595
  function formatPrice(price, currencyCode = "USD", currencies) {
377
- const defaultCurrencies = {
378
- USD: { code: "USD", symbol: "$", position: "prefix" },
379
- EUR: { code: "EUR", symbol: "\u20AC", position: "prefix" },
380
- GBP: { code: "GBP", symbol: "\xA3", position: "prefix" },
381
- INR: { code: "INR", symbol: "\u20B9", position: "prefix" }
382
- };
383
- const currencyMap = currencies || defaultCurrencies;
384
- const currency = currencyMap[currencyCode] || {
385
- symbol: currencyCode,
386
- position: "prefix"
387
- };
596
+ const currency = currencies?.[currencyCode];
597
+ if (!currency) {
598
+ return formatCatalogPrice(price, currencyCode);
599
+ }
388
600
  const formattedNumber = price % 1 === 0 ? price.toString() : price.toFixed(2);
389
601
  return currency.position === "prefix" ? `${currency.symbol}${formattedNumber}` : `${formattedNumber}${currency.symbol}`;
390
602
  }
@@ -445,11 +657,12 @@ function getFallbackFeaturesForTier(tier, fallbackFeatures) {
445
657
  }
446
658
  function getMetadataForPlan(planName, priceRaw, planMetadata) {
447
659
  const normalizedName = planName.toLowerCase();
448
- if (planMetadata?.[normalizedName]) {
449
- const meta = planMetadata[normalizedName];
660
+ const cleanName = cleanPlanDisplayName(planName);
661
+ const meta = planMetadata?.[normalizedName] ?? planMetadata?.[cleanName.toLowerCase()];
662
+ if (meta) {
450
663
  return {
451
664
  tier: meta.tier || "basic",
452
- displayName: meta.displayName || planName,
665
+ displayName: meta.displayName || cleanName,
453
666
  defaultDescription: meta.defaultDescription || "Choose this plan",
454
667
  popular: meta.popular ?? false,
455
668
  ctaText: meta.ctaText || "Get Started",
@@ -463,7 +676,7 @@ function getMetadataForPlan(planName, priceRaw, planMetadata) {
463
676
  if (isPaidEnterpriseName) {
464
677
  return {
465
678
  tier: "professional",
466
- displayName: planName,
679
+ displayName: cleanName,
467
680
  defaultDescription: "Choose this plan",
468
681
  popular: false,
469
682
  ctaText: "Get Started",
@@ -472,7 +685,7 @@ function getMetadataForPlan(planName, priceRaw, planMetadata) {
472
685
  }
473
686
  return {
474
687
  tier,
475
- displayName: planName,
688
+ displayName: cleanName,
476
689
  defaultDescription: "Choose this plan",
477
690
  popular: tier === "professional",
478
691
  ctaText: tier === "enterprise" ? "Contact Sales" : "Get Started",
@@ -481,7 +694,7 @@ function getMetadataForPlan(planName, priceRaw, planMetadata) {
481
694
  }
482
695
  return {
483
696
  tier: "basic",
484
- displayName: planName,
697
+ displayName: cleanName,
485
698
  defaultDescription: "Choose this plan",
486
699
  popular: false,
487
700
  ctaText: "Get Started",
@@ -538,17 +751,14 @@ function extractDisplayFeatures(features, planName, fallbackFeatures) {
538
751
  }
539
752
  return [];
540
753
  }
541
- function transformToDisplayCard(plan, props) {
754
+ function transformToDisplayCard(plan, props, currency) {
542
755
  const metadata = getMetadataForPlan(
543
756
  plan.name,
544
757
  plan.price,
545
758
  props.planMetadata
546
759
  );
547
- const formattedPrice = formatPrice(
548
- plan.price,
549
- plan.currency,
550
- props.currencies
551
- );
760
+ const selectedPrice = currency ? planPriceIn(plan, currency) : null;
761
+ const formattedPrice = currency && selectedPrice !== null ? formatPrice(selectedPrice, currency.toUpperCase(), props.currencies) : formatPrice(plan.price, plan.currency, props.currencies);
552
762
  const interval = formatInterval(plan.duration, props.billingIntervals);
553
763
  const features = metadata.features && metadata.features.length > 0 ? metadata.features : extractDisplayFeatures(
554
764
  plan.features,
@@ -557,13 +767,14 @@ function transformToDisplayCard(plan, props) {
557
767
  );
558
768
  const contactUrl = props.contactUrl || "/contact";
559
769
  const ctaLink = metadata.ctaType === "contact" ? contactUrl : props.appLoginUrl;
770
+ const isQuoteBased = metadata.ctaType === "contact" && plan.price === 0;
560
771
  return {
561
772
  id: plan.id,
562
- name: plan.name,
773
+ name: metadata.displayName,
563
774
  description: metadata.defaultDescription,
564
- price: formattedPrice,
775
+ price: isQuoteBased ? "Custom" : formattedPrice,
565
776
  priceRaw: plan.price,
566
- interval,
777
+ interval: isQuoteBased ? "" : interval,
567
778
  duration: plan.duration,
568
779
  features,
569
780
  popular: metadata.popular,
@@ -581,8 +792,13 @@ function PricingSection(props) {
581
792
  } = props;
582
793
  const client = useWebSDK();
583
794
  const config = useWebSDKConfig();
584
- const resolvedProductId = props.productId || config.productId;
585
- const [allPlans, setAllPlans] = useState([]);
795
+ const { product: contextProduct } = useProduct();
796
+ const requestedProductId = (props.productId || config.productId || "").trim();
797
+ const contextKey = resolvePricingProductKey(requestedProductId, [
798
+ contextProduct
799
+ ]);
800
+ const productKey = contextKey ?? requestedProductId;
801
+ const [catalogPlans, setCatalogPlans] = useState([]);
586
802
  const [loading, setLoading] = useState(true);
587
803
  const [error, setError] = useState(null);
588
804
  const [billingCycle, setBillingCycle] = useState(
@@ -591,13 +807,38 @@ function PricingSection(props) {
591
807
  const [audience, setAudience] = useState(
592
808
  "individual"
593
809
  );
810
+ const [selectedCurrency, setSelectedCurrency] = useState(
811
+ props.defaultCurrency ? props.defaultCurrency.toUpperCase() : null
812
+ );
594
813
  useEffect(() => {
595
814
  let cancelled = false;
815
+ async function resolveKey() {
816
+ if (!productKey) {
817
+ throw new Error(
818
+ "PricingPage needs a product slug: pass productId or set WebSDKConfig.productId."
819
+ );
820
+ }
821
+ if (!isUuidLike(productKey)) return productKey;
822
+ const response = await client.query(
823
+ PUBLIC_PRODUCT_KEYS_QUERY
824
+ );
825
+ if (response.errors && response.errors.length > 0) {
826
+ throw new Error(response.errors[0].message);
827
+ }
828
+ const resolved = resolvePricingProductKey(
829
+ productKey,
830
+ response.data?.publicProducts?.items ?? []
831
+ );
832
+ if (!resolved) throw new PricingProductKeyError(productKey);
833
+ return resolved;
834
+ }
596
835
  async function loadPlans() {
597
836
  try {
598
837
  setLoading(true);
838
+ const key = await resolveKey();
839
+ if (cancelled) return;
599
840
  const response = await client.query(GET_PLANS_QUERY, {
600
- productId: resolvedProductId
841
+ productId: key
601
842
  });
602
843
  if (cancelled) return;
603
844
  if (response.errors && response.errors.length > 0) {
@@ -606,12 +847,20 @@ function PricingSection(props) {
606
847
  if (!response.data?.publicPlans) {
607
848
  throw new Error("No plans data received");
608
849
  }
609
- const cards = response.data.publicPlans.filter((plan) => plan.isActive).map((plan) => transformToDisplayCard(plan, props)).sort((a, b) => a.priceRaw - b.priceRaw);
610
- setAllPlans(cards);
850
+ const active = response.data.publicPlans.filter(
851
+ (plan) => plan.isActive
852
+ );
853
+ if (active.length === 0) {
854
+ throw new Error(
855
+ `publicPlans returned no active plans for product "${key}".`
856
+ );
857
+ }
858
+ setCatalogPlans(active);
611
859
  setError(null);
612
860
  } catch (err) {
613
861
  if (cancelled) return;
614
862
  console.error("Failed to load plans:", err);
863
+ setCatalogPlans([]);
615
864
  setError("Unable to load pricing information. Please try again later.");
616
865
  } finally {
617
866
  if (!cancelled) {
@@ -623,13 +872,38 @@ function PricingSection(props) {
623
872
  return () => {
624
873
  cancelled = true;
625
874
  };
626
- }, [client, resolvedProductId]);
875
+ }, [client, productKey]);
876
+ const currencyOptions = useMemo(
877
+ () => availablePlanCurrencies(catalogPlans),
878
+ [catalogPlans]
879
+ );
880
+ useEffect(() => {
881
+ if (selectedCurrency || currencyOptions.length < 2) return;
882
+ const stored = readStoredCurrency()?.toUpperCase();
883
+ if (stored && currencyOptions.includes(stored)) {
884
+ setSelectedCurrency(stored);
885
+ return;
886
+ }
887
+ const regional = detectRegionalCurrency(
888
+ currencyOptions,
889
+ readVisitorLocale()
890
+ );
891
+ if (regional) setSelectedCurrency(regional);
892
+ }, [currencyOptions, selectedCurrency]);
893
+ const activeCurrency = selectedCurrency && currencyOptions.includes(selectedCurrency) ? selectedCurrency : currencyOptions[0] ?? null;
894
+ const allPlans = catalogPlans.map((plan) => transformToDisplayCard(plan, props, activeCurrency)).sort((a, b) => {
895
+ const aCustom = a.price === "Custom" ? 1 : 0;
896
+ const bCustom = b.price === "Custom" ? 1 : 0;
897
+ return aCustom - bCustom || a.priceRaw - b.priceRaw;
898
+ });
899
+ const showCurrencySelector = props.showCurrencySelector !== false && currencyOptions.length > 1;
627
900
  const {
628
901
  individualPlans: individualAllPlans,
629
902
  enterprisePlans: enterpriseApiPlans
630
903
  } = splitPlansByAudience(allPlans, props.showAudienceTabs);
631
- const individualPlans = individualAllPlans.filter(
632
- (plan) => billingCycle === "monthly" ? plan.duration === "monthly" : plan.duration === "yearly"
904
+ const individualPlans = plansForBillingCycle(
905
+ individualAllPlans,
906
+ billingCycle
633
907
  );
634
908
  const enterpriseCards = enterpriseApiPlans.map((p) => {
635
909
  if (p.priceRaw === 0 && p.name.toLowerCase().includes("enterprise")) {
@@ -747,6 +1021,23 @@ function PricingSection(props) {
747
1021
  children: "Yearly"
748
1022
  }
749
1023
  )
1024
+ ] }) }),
1025
+ showCurrencySelector && /* @__PURE__ */ jsx("div", { className: "flex justify-center mt-4", children: /* @__PURE__ */ jsxs("label", { className: "inline-flex items-center gap-2 text-sm text-muted-foreground", children: [
1026
+ /* @__PURE__ */ jsx("span", { children: "Currency" }),
1027
+ /* @__PURE__ */ jsx(
1028
+ "select",
1029
+ {
1030
+ value: activeCurrency ?? "",
1031
+ onChange: (event) => {
1032
+ const code = event.target.value;
1033
+ setSelectedCurrency(code);
1034
+ storeCurrency(code);
1035
+ },
1036
+ 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",
1037
+ "data-testid": "pricing-currency-select",
1038
+ children: currencyOptions.map((code) => /* @__PURE__ */ jsx("option", { value: code, children: code }, code))
1039
+ }
1040
+ )
750
1041
  ] }) })
751
1042
  ] }),
752
1043
  props.showAudienceTabs && audience === "enterprise" && /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-3xl text-center mb-8", children: [
@@ -931,6 +1222,6 @@ function PricingPage(props) {
931
1222
  ] });
932
1223
  }
933
1224
 
934
- export { PricingPage, PricingSection, resolveEnterprisePlans, splitPlansByAudience, transformFeatureToLabel };
1225
+ export { PricingPage, PricingProductKeyError, PricingSection, availablePlanCurrencies, cleanPlanDisplayName, isUuidLike, planPriceIn, resolveEnterprisePlans, resolvePricingProductKey, splitPlansByAudience, transformFeatureToLabel };
935
1226
  //# sourceMappingURL=pricing.mjs.map
936
1227
  //# sourceMappingURL=pricing.mjs.map