@burdenoff/website-sdk 2026.802.1 → 2026.7203.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.
@@ -35,7 +35,6 @@ interface PlanCard {
35
35
  features: Array<{
36
36
  id: string;
37
37
  quotaId?: string | null;
38
- context?: unknown;
39
38
  quota?: {
40
39
  id: string;
41
40
  name: string;
@@ -183,95 +182,6 @@ interface PricingProps {
183
182
  */
184
183
  seoUrl?: string;
185
184
  }
186
- /**
187
- * Split the full active plan list into the individual (self-serve) grid vs.
188
- * the enterprise-audience-tab bucket.
189
- *
190
- * When `showAudienceTabs` is false (the default — @see `PricingProps.showAudienceTabs`),
191
- * there is no "Enterprise" tab for a name-matched plan to render under, so
192
- * every active plan must stay in the individual grid regardless of its name.
193
- *
194
- * When `showAudienceTabs` is true, only custom-priced (priceRaw === 0) plans
195
- * whose names contain "team"/"business"/"enterprise" are moved to the
196
- * Enterprise tab. Paid self-serve tiers with those same names (e.g.
197
- * "$999/mo Business") stay in the individual grid so they actually render —
198
- * this is the BOFF-5652 fix.
199
- *
200
- * Exported (pure, no React/hooks) so this exact branching can be unit-tested
201
- * without a DOM/render harness.
202
- */
203
- declare function splitPlansByAudience<T extends {
204
- name: string;
205
- priceRaw?: number;
206
- }>(plans: T[], showAudienceTabs: boolean | undefined): {
207
- individualPlans: T[];
208
- enterprisePlans: T[];
209
- };
210
- /**
211
- * Resolve the final set of cards to show under the Enterprise audience tab.
212
- *
213
- * Historically this was a strict either/or: any API-fetched, name-matched
214
- * "enterprise audience" plan (see `isEnterprisePlan` — matches "team",
215
- * "business", or "enterprise" in the plan name) completely replaced the
216
- * consumer's static `enterprisePlans` prop, on the assumption that a product
217
- * either sells enterprise tiers self-serve through the billing catalog OR
218
- * lists them as static "Contact Sales" cards, never both.
219
- *
220
- * That assumption breaks for a product that self-serve-sells a mid tier
221
- * whose name happens to match the enterprise heuristic (e.g. a "Business"
222
- * plan sold through the catalog, $399/mo) while ALSO wanting a distinct,
223
- * uncatalogued, custom-priced "Enterprise / Contact Sales" tier above it
224
- * (sales-negotiated, not backed by a billing plan). Found live on
225
- * HousingVista's /pricing (BOFF-5007): the API's "Business (Monthly/Yearly)"
226
- * plans satisfied `hasApiEnterprisePlans`, so the static `enterprisePlans`
227
- * prop carrying the real "Enterprise" Contact-Sales card was silently
228
- * dropped in full — the tab showed Business but no Enterprise card at all.
229
- *
230
- * Fix: union the two sources instead of picking one. API plans always win a
231
- * same-name collision (case-insensitive) so a static entry can't shadow a
232
- * live catalog plan; anything in the static list that doesn't collide is
233
- * appended after the API cards. Only fall back to the single built-in
234
- * default card when NEITHER source has anything to show.
235
- *
236
- * Exported (pure, no React/hooks) so this merge/dedup logic can be
237
- * unit-tested without a DOM/render harness.
238
- */
239
- declare function resolveEnterprisePlans<T extends {
240
- name: string;
241
- }>(apiEnterprisePlans: T[], staticEnterprisePlans: Array<{
242
- name: string;
243
- }> | undefined, defaultEnterpriseCard: {
244
- name: string;
245
- }): Array<T | {
246
- name: string;
247
- }>;
248
- /**
249
- * Transform a plan feature (quota link) into a human-readable string.
250
- *
251
- * BOFF-4206: the per-PLAN quantity lives on the SubscriptionFeature's own
252
- * `context` (e.g. `{ quantity: 250, unit: 'profiles' }`) — `quota.limits` is
253
- * the SHARED Quota row's value, set once at quota-creation time and
254
- * IDENTICAL for every plan that links that quota. Reading `quota.limits`
255
- * here made every tier's card show the same numbers: live on TimeCampus's
256
- * /pricing, Starter/Growth/Business all rendered Free's "1 Projects" and "2
257
- * Admin Seats" instead of their real, higher, per-tier limits. Prefer the
258
- * per-feature `context.quantity`; fall back to `quota.limits.value` only
259
- * when a feature genuinely carries no context (legacy rows seeded before
260
- * this convention, or a quota linked without a context payload).
261
- *
262
- * Exported (pure, no React/hooks) so this exact precedence is unit-testable
263
- * without a DOM/render harness, matching `splitPlansByAudience` /
264
- * `resolveEnterprisePlans` above.
265
- */
266
- declare function transformFeatureToLabel(feature: {
267
- quotaId?: string | null;
268
- context?: unknown;
269
- quota?: {
270
- id: string;
271
- name: string;
272
- limits?: unknown;
273
- } | null;
274
- }): string;
275
185
  /**
276
186
  * Pricing Section Component - Embeddable
277
187
  */
@@ -281,4 +191,4 @@ declare function PricingSection(props: PricingProps): react_jsx_runtime.JSX.Elem
281
191
  */
282
192
  declare function PricingPage(props: PricingProps): react_jsx_runtime.JSX.Element;
283
193
 
284
- export { type PlanCard, PricingPage, type PricingProps, PricingSection, resolveEnterprisePlans, splitPlansByAudience, transformFeatureToLabel };
194
+ export { type PlanCard, PricingPage, type PricingProps, PricingSection };
@@ -35,7 +35,6 @@ interface PlanCard {
35
35
  features: Array<{
36
36
  id: string;
37
37
  quotaId?: string | null;
38
- context?: unknown;
39
38
  quota?: {
40
39
  id: string;
41
40
  name: string;
@@ -183,95 +182,6 @@ interface PricingProps {
183
182
  */
184
183
  seoUrl?: string;
185
184
  }
186
- /**
187
- * Split the full active plan list into the individual (self-serve) grid vs.
188
- * the enterprise-audience-tab bucket.
189
- *
190
- * When `showAudienceTabs` is false (the default — @see `PricingProps.showAudienceTabs`),
191
- * there is no "Enterprise" tab for a name-matched plan to render under, so
192
- * every active plan must stay in the individual grid regardless of its name.
193
- *
194
- * When `showAudienceTabs` is true, only custom-priced (priceRaw === 0) plans
195
- * whose names contain "team"/"business"/"enterprise" are moved to the
196
- * Enterprise tab. Paid self-serve tiers with those same names (e.g.
197
- * "$999/mo Business") stay in the individual grid so they actually render —
198
- * this is the BOFF-5652 fix.
199
- *
200
- * Exported (pure, no React/hooks) so this exact branching can be unit-tested
201
- * without a DOM/render harness.
202
- */
203
- declare function splitPlansByAudience<T extends {
204
- name: string;
205
- priceRaw?: number;
206
- }>(plans: T[], showAudienceTabs: boolean | undefined): {
207
- individualPlans: T[];
208
- enterprisePlans: T[];
209
- };
210
- /**
211
- * Resolve the final set of cards to show under the Enterprise audience tab.
212
- *
213
- * Historically this was a strict either/or: any API-fetched, name-matched
214
- * "enterprise audience" plan (see `isEnterprisePlan` — matches "team",
215
- * "business", or "enterprise" in the plan name) completely replaced the
216
- * consumer's static `enterprisePlans` prop, on the assumption that a product
217
- * either sells enterprise tiers self-serve through the billing catalog OR
218
- * lists them as static "Contact Sales" cards, never both.
219
- *
220
- * That assumption breaks for a product that self-serve-sells a mid tier
221
- * whose name happens to match the enterprise heuristic (e.g. a "Business"
222
- * plan sold through the catalog, $399/mo) while ALSO wanting a distinct,
223
- * uncatalogued, custom-priced "Enterprise / Contact Sales" tier above it
224
- * (sales-negotiated, not backed by a billing plan). Found live on
225
- * HousingVista's /pricing (BOFF-5007): the API's "Business (Monthly/Yearly)"
226
- * plans satisfied `hasApiEnterprisePlans`, so the static `enterprisePlans`
227
- * prop carrying the real "Enterprise" Contact-Sales card was silently
228
- * dropped in full — the tab showed Business but no Enterprise card at all.
229
- *
230
- * Fix: union the two sources instead of picking one. API plans always win a
231
- * same-name collision (case-insensitive) so a static entry can't shadow a
232
- * live catalog plan; anything in the static list that doesn't collide is
233
- * appended after the API cards. Only fall back to the single built-in
234
- * default card when NEITHER source has anything to show.
235
- *
236
- * Exported (pure, no React/hooks) so this merge/dedup logic can be
237
- * unit-tested without a DOM/render harness.
238
- */
239
- declare function resolveEnterprisePlans<T extends {
240
- name: string;
241
- }>(apiEnterprisePlans: T[], staticEnterprisePlans: Array<{
242
- name: string;
243
- }> | undefined, defaultEnterpriseCard: {
244
- name: string;
245
- }): Array<T | {
246
- name: string;
247
- }>;
248
- /**
249
- * Transform a plan feature (quota link) into a human-readable string.
250
- *
251
- * BOFF-4206: the per-PLAN quantity lives on the SubscriptionFeature's own
252
- * `context` (e.g. `{ quantity: 250, unit: 'profiles' }`) — `quota.limits` is
253
- * the SHARED Quota row's value, set once at quota-creation time and
254
- * IDENTICAL for every plan that links that quota. Reading `quota.limits`
255
- * here made every tier's card show the same numbers: live on TimeCampus's
256
- * /pricing, Starter/Growth/Business all rendered Free's "1 Projects" and "2
257
- * Admin Seats" instead of their real, higher, per-tier limits. Prefer the
258
- * per-feature `context.quantity`; fall back to `quota.limits.value` only
259
- * when a feature genuinely carries no context (legacy rows seeded before
260
- * this convention, or a quota linked without a context payload).
261
- *
262
- * Exported (pure, no React/hooks) so this exact precedence is unit-testable
263
- * without a DOM/render harness, matching `splitPlansByAudience` /
264
- * `resolveEnterprisePlans` above.
265
- */
266
- declare function transformFeatureToLabel(feature: {
267
- quotaId?: string | null;
268
- context?: unknown;
269
- quota?: {
270
- id: string;
271
- name: string;
272
- limits?: unknown;
273
- } | null;
274
- }): string;
275
185
  /**
276
186
  * Pricing Section Component - Embeddable
277
187
  */
@@ -281,4 +191,4 @@ declare function PricingSection(props: PricingProps): react_jsx_runtime.JSX.Elem
281
191
  */
282
192
  declare function PricingPage(props: PricingProps): react_jsx_runtime.JSX.Element;
283
193
 
284
- export { type PlanCard, PricingPage, type PricingProps, PricingSection, resolveEnterprisePlans, splitPlansByAudience, transformFeatureToLabel };
194
+ export { type PlanCard, PricingPage, type PricingProps, PricingSection };
@@ -334,7 +334,6 @@ var GET_PLANS_QUERY = `
334
334
  features {
335
335
  id
336
336
  quotaId
337
- context
338
337
  quota {
339
338
  id
340
339
  name
@@ -374,35 +373,6 @@ function isEnterprisePlan(planName) {
374
373
  const name = planName.toLowerCase();
375
374
  return name.includes("team") || name.includes("business") || name.includes("enterprise");
376
375
  }
377
- function isEnterpriseAudiencePlan(plan) {
378
- if (!isEnterprisePlan(plan.name)) {
379
- return false;
380
- }
381
- if (typeof plan.priceRaw === "number") {
382
- return plan.priceRaw === 0;
383
- }
384
- return true;
385
- }
386
- function splitPlansByAudience(plans, showAudienceTabs) {
387
- if (!showAudienceTabs) {
388
- return { individualPlans: plans, enterprisePlans: [] };
389
- }
390
- return {
391
- individualPlans: plans.filter((p) => !isEnterpriseAudiencePlan(p)),
392
- enterprisePlans: plans.filter((p) => isEnterpriseAudiencePlan(p))
393
- };
394
- }
395
- function resolveEnterprisePlans(apiEnterprisePlans, staticEnterprisePlans, defaultEnterpriseCard) {
396
- const staticPlans = staticEnterprisePlans || [];
397
- if (apiEnterprisePlans.length === 0 && staticPlans.length === 0) {
398
- return [defaultEnterpriseCard];
399
- }
400
- const apiNames = new Set(apiEnterprisePlans.map((p) => p.name.toLowerCase()));
401
- const dedupedStaticPlans = staticPlans.filter(
402
- (p) => !apiNames.has(p.name.toLowerCase())
403
- );
404
- return [...apiEnterprisePlans, ...dedupedStaticPlans];
405
- }
406
376
  function detectPlanTier(planName) {
407
377
  const name = planName.toLowerCase();
408
378
  if (name.includes("free") || name.includes("basic") || name.includes("starter") || name.includes("hobby")) {
@@ -417,7 +387,7 @@ function detectPlanTier(planName) {
417
387
  function getFallbackFeaturesForTier(tier, fallbackFeatures) {
418
388
  return [...fallbackFeatures?.[tier] || []];
419
389
  }
420
- function getMetadataForPlan(planName, priceRaw, planMetadata) {
390
+ function getMetadataForPlan(planName, planMetadata) {
421
391
  const normalizedName = planName.toLowerCase();
422
392
  if (planMetadata?.[normalizedName]) {
423
393
  const meta = planMetadata[normalizedName];
@@ -433,17 +403,6 @@ function getMetadataForPlan(planName, priceRaw, planMetadata) {
433
403
  }
434
404
  const tier = detectPlanTier(planName);
435
405
  if (tier) {
436
- const isPaidEnterpriseName = tier === "enterprise" && typeof priceRaw === "number" && priceRaw > 0;
437
- if (isPaidEnterpriseName) {
438
- return {
439
- tier: "professional",
440
- displayName: planName,
441
- defaultDescription: "Choose this plan",
442
- popular: false,
443
- ctaText: "Get Started",
444
- ctaType: "signup"
445
- };
446
- }
447
406
  return {
448
407
  tier,
449
408
  displayName: planName,
@@ -467,27 +426,27 @@ function humanizeQuotaName(name) {
467
426
  const resource = parts.length >= 3 ? parts[parts.length - 2] : parts[parts.length - 1];
468
427
  return resource.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
469
428
  }
470
- function transformFeatureToLabel(feature) {
471
- const quota = feature.quota;
472
- if (!quota) return "";
429
+ function transformQuotaToFeature(quota) {
473
430
  const { name, limits } = quota;
474
- const featureContext = feature.context && typeof feature.context === "object" ? feature.context : null;
475
- const limitsObj = limits && typeof limits === "object" ? limits : null;
476
- const value = typeof featureContext?.quantity === "number" ? featureContext.quantity : typeof limitsObj?.value === "number" ? limitsObj.value : void 0;
477
- const unit = typeof featureContext?.unit === "string" && featureContext.unit || typeof limitsObj?.unit === "string" && limitsObj.unit || void 0;
478
- const description = typeof featureContext?.description === "string" && featureContext.description || typeof limitsObj?.description === "string" && limitsObj.description || void 0;
479
- const type = limitsObj?.type;
480
- if (description) {
431
+ if (!limits || typeof limits !== "object") {
432
+ return humanizeQuotaName(name);
433
+ }
434
+ const limitsObj = limits;
435
+ const value = limitsObj.value;
436
+ const unit = limitsObj.unit;
437
+ const type = limitsObj.type;
438
+ const description = limitsObj.description;
439
+ if (typeof description === "string" && description) {
481
440
  if (type === "number" && typeof value === "number") {
482
441
  return `${value.toLocaleString()} ${description}`;
483
442
  }
484
443
  return description;
485
444
  }
486
- if (unit) {
445
+ if (typeof unit === "string" && unit) {
487
446
  if (typeof value === "number") {
488
447
  return `${value.toLocaleString()} ${unit}`;
489
448
  }
490
- return unit;
449
+ return `${unit}`;
491
450
  }
492
451
  const label = humanizeQuotaName(name);
493
452
  if (type === "boolean") {
@@ -506,18 +465,14 @@ function extractDisplayFeatures(features, planName, fallbackFeatures) {
506
465
  return tierFeatures;
507
466
  }
508
467
  }
509
- const quotaFeatures = features.filter((f) => f.quota).map((f) => transformFeatureToLabel(f));
468
+ const quotaFeatures = features.filter((f) => f.quota).map((f) => transformQuotaToFeature(f.quota));
510
469
  if (quotaFeatures.length > 0) {
511
470
  return quotaFeatures;
512
471
  }
513
472
  return [];
514
473
  }
515
474
  function transformToDisplayCard(plan, props) {
516
- const metadata = getMetadataForPlan(
517
- plan.name,
518
- plan.price,
519
- props.planMetadata
520
- );
475
+ const metadata = getMetadataForPlan(plan.name, props.planMetadata);
521
476
  const formattedPrice = formatPrice(
522
477
  plan.price,
523
478
  plan.currency,
@@ -598,10 +553,8 @@ function PricingSection(props) {
598
553
  cancelled = true;
599
554
  };
600
555
  }, [client, resolvedProductId]);
601
- const {
602
- individualPlans: individualAllPlans,
603
- enterprisePlans: enterpriseApiPlans
604
- } = splitPlansByAudience(allPlans, props.showAudienceTabs);
556
+ const individualAllPlans = allPlans.filter((p) => !isEnterprisePlan(p.name));
557
+ const enterpriseApiPlans = allPlans.filter((p) => isEnterprisePlan(p.name));
605
558
  const individualPlans = individualAllPlans.filter(
606
559
  (plan) => billingCycle === "monthly" ? plan.duration === "monthly" : plan.duration === "yearly"
607
560
  );
@@ -617,6 +570,7 @@ function PricingSection(props) {
617
570
  }
618
571
  return p;
619
572
  });
573
+ const hasApiEnterprisePlans = enterpriseCards.length > 0;
620
574
  const defaultEnterpriseCard = {
621
575
  name: "Enterprise",
622
576
  subtitle: "For organizations that need more",
@@ -637,11 +591,7 @@ function PricingSection(props) {
637
591
  "Self-hosted option"
638
592
  ]
639
593
  };
640
- const resolvedEnterprisePlans = resolveEnterprisePlans(
641
- enterpriseCards,
642
- props.enterprisePlans,
643
- defaultEnterpriseCard
644
- );
594
+ const resolvedEnterprisePlans = hasApiEnterprisePlans ? enterpriseCards : props.enterprisePlans ? props.enterprisePlans : [defaultEnterpriseCard];
645
595
  const plans = props.showAudienceTabs && audience === "enterprise" ? [] : individualPlans;
646
596
  if (loading) {
647
597
  return /* @__PURE__ */ jsxRuntime.jsx("section", { id, className: `py-12 sm:py-16 md:py-20 ${className}`, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "container mx-auto px-4 sm:px-6", children: [
@@ -907,8 +857,5 @@ function PricingPage(props) {
907
857
 
908
858
  exports.PricingPage = PricingPage;
909
859
  exports.PricingSection = PricingSection;
910
- exports.resolveEnterprisePlans = resolveEnterprisePlans;
911
- exports.splitPlansByAudience = splitPlansByAudience;
912
- exports.transformFeatureToLabel = transformFeatureToLabel;
913
860
  //# sourceMappingURL=pricing.js.map
914
861
  //# sourceMappingURL=pricing.js.map