@growth-operations/ui-shared 1.21.0 → 1.21.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-operations/ui-shared",
3
- "version": "1.21.0",
3
+ "version": "1.21.2",
4
4
  "description": "Shared HubSpot UI-extension modules (Home, Billing) for Growth Operations apps.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -27,7 +27,10 @@ function MostUsedTile({ mostUsed }) {
27
27
  <Flex direction="column" gap="small">
28
28
  <Text format={{ fontWeight: "bold" }}>Most used features</Text>
29
29
  {mostUsed.map((f) => (
30
- <Flex key={f.key} direction="row" gap="small">
30
+ // justify=between pins the count column flush-right so counts align
31
+ // across rows regardless of feature-name length (Jasper 2026-10-13:
32
+ // ragged gap-after-name read as broken alignment).
33
+ <Flex key={f.key} direction="row" justify="between" align="baseline">
31
34
  <Text>{f.name}</Text>
32
35
  <Text variant="microcopy">
33
36
  {f.count} {unitLabel(f.unit, f.count)}
@@ -26,8 +26,11 @@ export function HealthHero({ appName, state }) {
26
26
  // Plan-fit reinforcement: a healthy verdict on the right plan gets the
27
27
  // extra reassurance line; upgrade_recommended is carried by health
28
28
  // v2's "attention" reasons + the PlanFitBanner, not here.
29
+ // Dual-read: base nests plan_fit under payload.state; self-hosted
30
+ // backends MAY emit it top-level (same pattern as planFitOf).
31
+ const planFit = state.state?.plan_fit ?? state.plan_fit;
29
32
  const onRightPlan =
30
- health.level === "healthy" && state.plan_fit?.verdict === "right_plan";
33
+ health.level === "healthy" && planFit?.verdict === "right_plan";
31
34
  return (
32
35
  <Flex direction="column" gap="small">
33
36
  {name && <Heading>{name}</Heading>}
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { Flex, Tile, Text, ProgressBar } from "@hubspot/ui-extensions";
3
- import { resolvePeriodComparison } from "../lib/homeBlocks";
3
+ import { periodComparisonOf } from "../lib/homeBlocks";
4
4
 
5
5
  // The post-checklist Home usage block: the last full billing period vs the
6
6
  // current period-to-date, side by side — one bar per period (shared
@@ -41,7 +41,9 @@ function PeriodColumn({ period, maxTotal }) {
41
41
  }
42
42
 
43
43
  export function PeriodComparison({ state }) {
44
- const view = resolvePeriodComparison(state?.period_comparison);
44
+ // periodComparisonOf dual-reads payload.state.period_comparison (base) ??
45
+ // payload.period_comparison (self-hosted).
46
+ const view = periodComparisonOf(state);
45
47
  if (!view) return null;
46
48
 
47
49
  return (
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { Text, Alert } from "@hubspot/ui-extensions";
3
- import { resolvePlanFit } from "../lib/homeBlocks";
3
+ import { planFitOf } from "../lib/homeBlocks";
4
4
 
5
5
  // The plan-fit savings banner: when state.plan_fit.verdict is
6
6
  // "upgrade_recommended", tell the customer what staying on their current tier
@@ -14,7 +14,10 @@ import { resolvePlanFit } from "../lib/homeBlocks";
14
14
  // Renders nothing when the block is absent or the verdict isn't
15
15
  // upgrade_recommended.
16
16
  export function PlanFitBanner({ state }) {
17
- const fit = resolvePlanFit(state?.plan_fit);
17
+ // planFitOf dual-reads payload.state.plan_fit (base) ?? payload.plan_fit
18
+ // (self-hosted) — a top-level-only read left this banner dead on every
19
+ // base-hosted app (found 2026-10-13).
20
+ const fit = planFitOf(state);
18
21
  if (!fit || fit.kind !== "upgrade_recommended") return null;
19
22
 
20
23
  return (
package/src/index.js CHANGED
@@ -53,6 +53,7 @@ export { useStrictModeEffect } from "./lib/useStrictModeEffect";
53
53
  export { resolveFeatureUsage, featureUsageOf } from "./lib/featureUsage";
54
54
  export {
55
55
  hasTopUpBank,
56
+ creditsBlockOf,
56
57
  resolveCreditMeter,
57
58
  resolveBillingBanner,
58
59
  parseLimitInput,
@@ -61,7 +62,9 @@ export {
61
62
  TRIAL_BANNER_FOOTER,
62
63
  resolveTrialBanner,
63
64
  resolvePlanFit,
65
+ planFitOf,
64
66
  resolvePeriodComparison,
67
+ periodComparisonOf,
65
68
  } from "./lib/homeBlocks";
66
69
  export {
67
70
  AUTO_PROVISION_TITLE,
@@ -98,6 +98,15 @@ function bankBarVariantFor(bankRemaining, bankGranted) {
98
98
  if (bankRemaining * 100 <= 50 * bankGranted) return "warning";
99
99
  return "success";
100
100
  }
101
+ // The credits block lives at payload.state.credits on base-hosted backends; a
102
+ // self-hosted backend MAY emit it top-level. Dual-read (nested wins), same as
103
+ // featureUsageOf/planFitOf — without this the meter falls back to the legacy
104
+ // single-bar path (no monthly/additional split, no "renews on" line).
105
+ export function creditsBlockOf(state) {
106
+ if (!state || typeof state !== "object") return null;
107
+ return state.state?.credits ?? state.credits ?? null;
108
+ }
109
+
101
110
  export function resolveCreditMeter(entitlement, creditMeter, credits = null) {
102
111
  const ent = entitlement?.mode === "credits" ? entitlement : null;
103
112
  const meter = creditMeter ?? null;
@@ -114,6 +114,15 @@ export function resolvePlanFit(planFit) {
114
114
  return null;
115
115
  }
116
116
 
117
+ // The plan_fit block lives at payload.state.plan_fit on base-hosted backends;
118
+ // a self-hosted backend MAY emit it top-level instead. Dual-read (nested
119
+ // wins), same as featureUsageOf — without this the banner/hero line silently
120
+ // never render (shipped dead 2026-10-12, found 2026-10-13).
121
+ export function planFitOf(state) {
122
+ if (!state || typeof state !== "object") return null;
123
+ return resolvePlanFit(state.state?.plan_fit ?? state.plan_fit);
124
+ }
125
+
117
126
  // --- Period comparison ------------------------------------------------------
118
127
 
119
128
  // Resolve the period_comparison block (null until the install has one full
@@ -144,3 +153,12 @@ export function resolvePeriodComparison(periodComparison) {
144
153
  maxTotal: Math.max(last.total, current.total, 1),
145
154
  };
146
155
  }
156
+
157
+ // Same nested-vs-top-level duality as planFitOf: base serves
158
+ // payload.state.period_comparison; self-hosted backends MAY go top-level.
159
+ export function periodComparisonOf(state) {
160
+ if (!state || typeof state !== "object") return null;
161
+ return resolvePeriodComparison(
162
+ state.state?.period_comparison ?? state.period_comparison
163
+ );
164
+ }
@@ -27,6 +27,7 @@ import {
27
27
  } from "@hubspot/ui-extensions";
28
28
 
29
29
  import { useStrictModeEffect } from "../lib/useStrictModeEffect";
30
+ import { creditsBlockOf } from "../lib/creditDisplay";
30
31
  import { CreditMeter } from "../home/CreditMeter";
31
32
 
32
33
  const DEFAULT_BASE_PATH = "/api/v1/usage";
@@ -56,10 +57,13 @@ export function UsageTab({
56
57
  return (
57
58
  <Flex direction="column" gap="medium">
58
59
  {state && (
60
+ // creditsBlockOf dual-reads payload.state.credits (base) ??
61
+ // payload.credits (self-hosted) — a top-level-only read silently
62
+ // dropped the two-pool split + "renews on" line on base-hosted apps.
59
63
  <CreditMeter
60
64
  entitlement={state.entitlement}
61
65
  creditMeter={state.credit_meter}
62
- credits={state.credits}
66
+ credits={creditsBlockOf(state)}
63
67
  />
64
68
  )}
65
69