@growth-operations/ui-shared 1.6.4 → 1.6.5

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.6.4",
3
+ "version": "1.6.5",
4
4
  "description": "Shared HubSpot UI-extension modules (Home, Billing) for Growth Operations apps.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -180,21 +180,22 @@ function TrialSubscriptionBilling({ context, state, appKey }) {
180
180
  `&return_url=${encodeURIComponent(returnUrl)}`
181
181
  : null;
182
182
 
183
- // Upgrade-only tier picker — shown ONLY while trialing (the lifecycle: trial
184
- // starts on the entry tier at install; while trialing the customer may upgrade
185
- // to a higher tier; once active, plan changes go through support, so no picker).
186
- // The cards target /v1/billing/upgrade/start (swap the trialing sub's item, no
187
- // new sub). "Higher tiers" = plans ranked above the current tier by tier_order;
188
- // the backend stamps Plan.current on the account's tier.
183
+ // Tier picker — shown ONLY while trialing (the lifecycle: trial starts on the
184
+ // entry tier at install; while trialing the customer may upgrade to a higher
185
+ // tier in-app; once active, plan changes go through support). We show the FULL
186
+ // ladder for context so the customer sees where they sit:
187
+ // current tier -> "Current plan" (disabled marker; PlanCard keys off
188
+ // plan.current, stamped server-side on the account's tier).
189
+ // higher tiers -> the actionable upgrade (CTA targets /v1/billing/upgrade/
190
+ // start, swapping the trialing sub's item — no new sub).
191
+ // lower tiers -> shown disabled with a Talk-to-sales CTA (downgrades aren't
192
+ // self-serve). PlanCard derives this from currentOrder.
189
193
  const isTrialing = ent.status === "trialing";
190
194
  const plans = state?.plans ?? [];
191
195
  const currentOrder = plans.find((p) => p.current)?.tier_order;
192
- const upgradePlans =
193
- isTrialing && currentOrder != null
194
- ? plans.filter(
195
- (p) => !p.talk_to_sales && p.tier_order != null && p.tier_order > currentOrder
196
- )
197
- : [];
196
+ // Render the picker only while trialing AND once we know the current tier
197
+ // (currentOrder drives upgrade-vs-downgrade per card).
198
+ const showPicker = isTrialing && currentOrder != null && plans.length > 0;
198
199
 
199
200
  return (
200
201
  <Flex direction="column" gap="medium">
@@ -215,19 +216,22 @@ function TrialSubscriptionBilling({ context, state, appKey }) {
215
216
  link above to update your plan, payment method, or view invoices.
216
217
  </Text>
217
218
 
218
- {/* Trial-only upgrade path. Hidden once active (no plans pass the filter
219
- when not trialing). Upgrading swaps the trialing sub onto the higher
220
- tier, keeping the trial end date. */}
221
- {upgradePlans.length > 0 && (
219
+ {/* Trial-only tier ladder: current (marked), higher (upgradeable), lower
220
+ (disabled, Talk-to-sales). Hidden once active. Upgrading swaps the
221
+ trialing sub onto the higher tier, keeping the trial end date. The full
222
+ `plans` list is passed; PlanCard decides each card's state from
223
+ plan.current + currentOrder. */}
224
+ {showPicker && (
222
225
  <PlanGrid
223
226
  context={context}
224
227
  state={state}
225
228
  appKey={appKey}
226
- plans={upgradePlans}
229
+ plans={plans}
230
+ currentOrder={currentOrder}
227
231
  endpoint="upgrade/start"
228
232
  ctaLabel="Upgrade to"
229
- heading="Upgrade your plan"
230
- footnote="Upgrade any time during your trial — your trial end date stays the same, and the new tier applies when it converts."
233
+ heading="Your plan"
234
+ footnote="Upgrade any time during your trial — your trial end date stays the same, and the new tier applies when it converts. To move to a lower tier, talk to sales."
231
235
  />
232
236
  )}
233
237
  </Flex>
@@ -54,12 +54,29 @@ function PlanCard({
54
54
  // to this tier (the trial-archetype in-app upgrade) — no new sub.
55
55
  endpoint = "checkout/start",
56
56
  ctaLabel = "Choose",
57
+ // Trial-picker only: tier_order of the customer's current tier. When set, a
58
+ // card ranked BELOW it is a downgrade — shown for context but not self-serve
59
+ // (downgrades go through support), so it renders disabled with a Talk-to-sales
60
+ // CTA. Cards ranked ABOVE are upgrades (the normal CTA). null/undefined => the
61
+ // credit first-purchase grid, where this relation doesn't apply.
62
+ currentOrder = null,
57
63
  }) {
58
64
  // Default this card to annual when it offers annual (cheaper-per-month story);
59
65
  // monthly-only tiers default to monthly. Both legs → show the toggle.
60
66
  const hasMonthly = !!plan.monthly;
61
67
  const hasAnnual = !!plan.annual;
62
- const canToggle = hasMonthly && hasAnnual;
68
+ // A lower-ranked tier than the customer's current one: a downgrade. Not
69
+ // self-serve (changes go through support) — show it for context, disabled,
70
+ // with a Talk-to-sales CTA. Only meaningful in the trial picker (currentOrder
71
+ // set); never flag the current tier itself as a downgrade.
72
+ const isDowngrade =
73
+ currentOrder != null &&
74
+ !plan.current &&
75
+ plan.tier_order != null &&
76
+ plan.tier_order < currentOrder;
77
+ // The interval toggle is only useful when the card can be acted on; a current
78
+ // or downgrade card is read-only, so hide its toggle.
79
+ const canToggle = hasMonthly && hasAnnual && !plan.current && !isDowngrade;
63
80
  const [interval, setInterval] = useState(hasAnnual ? "annual" : "monthly");
64
81
  const leg = interval === "annual" ? plan.annual : plan.monthly;
65
82
  const periodLabel = interval === "annual" ? "/yr" : "/mo";
@@ -152,10 +169,20 @@ function PlanCard({
152
169
  <Text key={`pad-${i}`}>{" "}</Text>
153
170
  ))}
154
171
 
155
- {/* CTA. Current tier => disabled marker. talk_to_sales => contact link.
156
- Otherwise pre-create-then-link checkout for the selected interval. */}
172
+ {/* CTA. Current tier => disabled marker. Downgrade (lower than current,
173
+ trial picker) => disabled + Talk-to-sales (not self-serve).
174
+ talk_to_sales tier => contact link. Otherwise pre-create-then-link
175
+ checkout/upgrade for the selected interval. */}
157
176
  {plan.current ? (
158
177
  <Button disabled>Current plan</Button>
178
+ ) : isDowngrade ? (
179
+ <Button
180
+ href={supportUrl ? { url: supportUrl, external: true } : undefined}
181
+ disabled={!supportUrl}
182
+ variant="secondary"
183
+ >
184
+ Talk to sales to switch
185
+ </Button>
159
186
  ) : plan.talk_to_sales ? (
160
187
  <Button
161
188
  href={supportUrl ? { url: supportUrl, external: true } : undefined}
@@ -202,6 +229,9 @@ export function PlanGrid({
202
229
  ctaLabel = "Choose",
203
230
  heading = "Plans",
204
231
  footnote = "Billing is managed in Stripe across all Growth Operations apps. Your new credits are added as soon as payment completes.",
232
+ // Trial-picker only: tier_order of the current tier, so cards below it render
233
+ // as disabled downgrades (Talk-to-sales). Omitted for the credit grid.
234
+ currentOrder = null,
205
235
  }) {
206
236
  const plans = plansOverride ?? state?.plans ?? [];
207
237
 
@@ -242,6 +272,7 @@ export function PlanGrid({
242
272
  maxFeatures={maxFeatures}
243
273
  endpoint={endpoint}
244
274
  ctaLabel={ctaLabel}
275
+ currentOrder={currentOrder}
245
276
  />
246
277
  ))}
247
278
  </AutoGrid>