@growth-operations/ui-shared 1.6.3 → 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.3",
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,6 +180,23 @@ function TrialSubscriptionBilling({ context, state, appKey }) {
180
180
  `&return_url=${encodeURIComponent(returnUrl)}`
181
181
  : null;
182
182
 
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.
193
+ const isTrialing = ent.status === "trialing";
194
+ const plans = state?.plans ?? [];
195
+ const currentOrder = plans.find((p) => p.current)?.tier_order;
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;
199
+
183
200
  return (
184
201
  <Flex direction="column" gap="medium">
185
202
  {/* PRIMARY CTA at the top. Direct external link to the billing-service
@@ -198,6 +215,25 @@ function TrialSubscriptionBilling({ context, state, appKey }) {
198
215
  Billing is managed in Stripe across all Growth Operations apps. Use the
199
216
  link above to update your plan, payment method, or view invoices.
200
217
  </Text>
218
+
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 && (
225
+ <PlanGrid
226
+ context={context}
227
+ state={state}
228
+ appKey={appKey}
229
+ plans={plans}
230
+ currentOrder={currentOrder}
231
+ endpoint="upgrade/start"
232
+ ctaLabel="Upgrade to"
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."
235
+ />
236
+ )}
201
237
  </Flex>
202
238
  );
203
239
  }
@@ -48,12 +48,35 @@ function PlanCard({
48
48
  returnUrl,
49
49
  supportUrl,
50
50
  maxFeatures = 0,
51
+ // Billing-service redirect the "Choose" button targets. "checkout/start" (the
52
+ // default) starts a NEW subscription via hosted Checkout — for free/credit
53
+ // first purchase. "upgrade/start" swaps the EXISTING trialing/active sub's item
54
+ // to this tier (the trial-archetype in-app upgrade) — no new sub.
55
+ endpoint = "checkout/start",
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,
51
63
  }) {
52
64
  // Default this card to annual when it offers annual (cheaper-per-month story);
53
65
  // monthly-only tiers default to monthly. Both legs → show the toggle.
54
66
  const hasMonthly = !!plan.monthly;
55
67
  const hasAnnual = !!plan.annual;
56
- 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;
57
80
  const [interval, setInterval] = useState(hasAnnual ? "annual" : "monthly");
58
81
  const leg = interval === "annual" ? plan.annual : plan.monthly;
59
82
  const periodLabel = interval === "annual" ? "/yr" : "/mo";
@@ -80,7 +103,7 @@ function PlanCard({
80
103
  // price) — the button is disabled until then.
81
104
  const startUrl =
82
105
  billingBaseUrl && appKey && portalId && leg?.price_id
83
- ? `${billingBaseUrl}/v1/billing/checkout/start` +
106
+ ? `${billingBaseUrl}/v1/billing/${endpoint}` +
84
107
  `?app_key=${encodeURIComponent(appKey)}` +
85
108
  `&portal_id=${encodeURIComponent(portalId)}` +
86
109
  `&price_id=${encodeURIComponent(leg.price_id)}` +
@@ -146,10 +169,20 @@ function PlanCard({
146
169
  <Text key={`pad-${i}`}>{" "}</Text>
147
170
  ))}
148
171
 
149
- {/* CTA. Current tier => disabled marker. talk_to_sales => contact link.
150
- 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. */}
151
176
  {plan.current ? (
152
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>
153
186
  ) : plan.talk_to_sales ? (
154
187
  <Button
155
188
  href={supportUrl ? { url: supportUrl, external: true } : undefined}
@@ -175,7 +208,7 @@ function PlanCard({
175
208
  disabled={!startUrl}
176
209
  variant="primary"
177
210
  >
178
- Choose {plan.name ?? plan.tier}
211
+ {ctaLabel} {plan.name ?? plan.tier}
179
212
  </Button>
180
213
  )}
181
214
  </Flex>
@@ -183,8 +216,24 @@ function PlanCard({
183
216
  );
184
217
  }
185
218
 
186
- export function PlanGrid({ context, state, appKey }) {
187
- const plans = state?.plans ?? [];
219
+ export function PlanGrid({
220
+ context,
221
+ state,
222
+ appKey,
223
+ // Customization for non-default uses. The credit-app first-purchase grid uses
224
+ // all defaults; the trial-archetype in-app UPGRADE picker passes:
225
+ // plans={higherTiersOnly} endpoint="upgrade/start" ctaLabel="Upgrade to"
226
+ // heading="Upgrade your plan" footnote={trial copy}
227
+ plans: plansOverride,
228
+ endpoint = "checkout/start",
229
+ ctaLabel = "Choose",
230
+ heading = "Plans",
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,
235
+ }) {
236
+ const plans = plansOverride ?? state?.plans ?? [];
188
237
 
189
238
  if (plans.length === 0) return null;
190
239
 
@@ -205,7 +254,7 @@ export function PlanGrid({ context, state, appKey }) {
205
254
 
206
255
  return (
207
256
  <Flex direction="column" gap="medium">
208
- <Heading>Plans</Heading>
257
+ <Heading>{heading}</Heading>
209
258
 
210
259
  {/* AutoGrid(flexible): equal-width columns that wrap responsively. Card
211
260
  height is equalized via the maxFeatures padding in PlanCard. Each card
@@ -221,14 +270,14 @@ export function PlanGrid({ context, state, appKey }) {
221
270
  returnUrl={returnUrl}
222
271
  supportUrl={supportUrl}
223
272
  maxFeatures={maxFeatures}
273
+ endpoint={endpoint}
274
+ ctaLabel={ctaLabel}
275
+ currentOrder={currentOrder}
224
276
  />
225
277
  ))}
226
278
  </AutoGrid>
227
279
 
228
- <Text format={{ fontStyle: "italic" }}>
229
- Billing is managed in Stripe across all Growth Operations apps. Your new
230
- credits are added as soon as payment completes.
231
- </Text>
280
+ <Text format={{ fontStyle: "italic" }}>{footnote}</Text>
232
281
  </Flex>
233
282
  );
234
283
  }