@growth-operations/ui-shared 1.5.2 → 1.5.4

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.5.2",
3
+ "version": "1.5.4",
4
4
  "description": "Shared HubSpot UI-extension modules (Home, Billing) for Growth Operations apps.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -2,7 +2,7 @@ import React, { useState } from "react";
2
2
  import {
3
3
  hubspot,
4
4
  Flex,
5
- Box,
5
+ AutoGrid,
6
6
  Tile,
7
7
  Text,
8
8
  Heading,
@@ -38,7 +38,15 @@ import { fmtMoney } from "../lib/format";
38
38
  // feature bullets, and a CTA: "Current" (disabled) for the active tier,
39
39
  // "Contact sales" for talk_to_sales tiers, else "Choose" which pre-creates a
40
40
  // checkout session for that tier's price in the selected interval.
41
- function PlanCard({ plan, interval, onChoose, choosing, checkoutUrl, supportUrl }) {
41
+ function PlanCard({
42
+ plan,
43
+ interval,
44
+ onChoose,
45
+ choosing,
46
+ checkoutUrl,
47
+ supportUrl,
48
+ maxFeatures = 0,
49
+ }) {
42
50
  const leg = interval === "annual" ? plan.annual : plan.monthly;
43
51
  const periodLabel = interval === "annual" ? "/yr" : "/mo";
44
52
 
@@ -48,35 +56,38 @@ function PlanCard({ plan, interval, onChoose, choosing, checkoutUrl, supportUrl
48
56
  ? `${fmtMoney(leg.unit_amount, leg.currency)}${periodLabel}`
49
57
  : "Free";
50
58
 
59
+ const features = plan.features ?? [];
60
+ // Align the CTA buttons across cards. HubSpot's column Flex does NOT stretch
61
+ // to the Tile height (AutoGrid equalizes the card height but flex={1}/justify
62
+ // had no vertical room to consume — verified in QA), so we can't push the CTA
63
+ // down with flex. Instead make the cards' CONTENT equal height: pad each
64
+ // feature list to maxFeatures with non-breaking-space spacer lines (a plain
65
+ // space collapses to zero height;   renders a real line). The CTAs then
66
+ // naturally land at the same y.
67
+ const padCount = Math.max(0, maxFeatures - features.length);
68
+
51
69
  return (
52
70
  <Tile>
53
- {/* Full-height column: the content Box grows to fill so the CTA always
54
- pins to the bottom, aligned across cards (the row stretches them to
55
- equal height). Otherwise the button floats up under shorter feature
56
- lists and the cards look ragged. */}
57
- <Flex direction="column" gap="small" align="stretch">
58
- <Box flex={1}>
59
- <Flex direction="column" gap="small">
60
- <Flex direction="row" gap="small" align="center">
61
- <Heading>{plan.name ?? plan.tier}</Heading>
62
- {plan.current && (
63
- <StatusTag variant="success">Current plan</StatusTag>
64
- )}
65
- </Flex>
66
-
67
- <Text format={{ fontWeight: "bold", fontSize: "lg" }}>{priceText}</Text>
68
-
69
- {plan.credits_per_period != null && (
70
- <Text>
71
- {plan.credits_per_period.toLocaleString()} credits / month
72
- </Text>
73
- )}
74
-
75
- {(plan.features ?? []).map((f, i) => (
76
- <Text key={i}>• {f}</Text>
77
- ))}
78
- </Flex>
79
- </Box>
71
+ <Flex direction="column" gap="small">
72
+ <Flex direction="row" gap="small" align="center">
73
+ <Heading>{plan.name ?? plan.tier}</Heading>
74
+ {plan.current && (
75
+ <StatusTag variant="success">Current plan</StatusTag>
76
+ )}
77
+ </Flex>
78
+
79
+ <Text format={{ fontWeight: "bold", fontSize: "lg" }}>{priceText}</Text>
80
+
81
+ {plan.credits_per_period != null && (
82
+ <Text>{plan.credits_per_period.toLocaleString()} credits / month</Text>
83
+ )}
84
+
85
+ {features.map((f, i) => (
86
+ <Text key={i}>• {f}</Text>
87
+ ))}
88
+ {Array.from({ length: padCount }).map((_, i) => (
89
+ <Text key={`pad-${i}`}>{" "}</Text>
90
+ ))}
80
91
 
81
92
  {/* CTA. Current tier => disabled marker. talk_to_sales => contact link.
82
93
  Otherwise pre-create-then-link checkout for the selected interval. */}
@@ -130,6 +141,12 @@ export function PlanGrid({ context, state, appKey }) {
130
141
  if (plans.length === 0) return null;
131
142
 
132
143
  const supportUrl = state?.helpful_links?.supportUrl ?? null;
144
+ // Max feature count across cards — each card pads to this so all cards share
145
+ // content height and the CTAs align (see PlanCard comment).
146
+ const maxFeatures = plans.reduce(
147
+ (m, p) => Math.max(m, (p.features ?? []).length),
148
+ 0
149
+ );
133
150
 
134
151
  async function choose(plan, leg) {
135
152
  setError(null);
@@ -146,12 +163,17 @@ export function PlanGrid({ context, state, appKey }) {
146
163
 
147
164
  // hubspot.fetch: body is a plain OBJECT (not a JSON string); only
148
165
  // Authorization survives as a header — don't set Content-Type.
166
+ // presentation:"hosted" — a UI extension runs in a sandboxed iframe and
167
+ // can't mount Stripe's EMBEDDED checkout (no Stripe.js, nested-iframe 3DS
168
+ // fails), so request the HOSTED page and open its URL externally. The
169
+ // embedded default returns a client_secret (no url) → "No checkout URL".
149
170
  const res = await hubspot.fetch(`${base}/v1/billing/checkout`, {
150
171
  method: "POST",
151
172
  body: {
152
173
  app_key: appKey,
153
174
  portal_id: String(portalId),
154
175
  price_id: leg.price_id,
176
+ presentation: "hosted",
155
177
  return_url: returnUrl,
156
178
  },
157
179
  });
@@ -194,7 +216,9 @@ export function PlanGrid({ context, state, appKey }) {
194
216
  </Alert>
195
217
  )}
196
218
 
197
- <Flex direction="row" gap="medium" wrap="wrap">
219
+ {/* AutoGrid(flexible): equal-width columns that wrap responsively. Card
220
+ height is equalized via the maxFeatures padding in PlanCard. */}
221
+ <AutoGrid columnWidth={240} flexible={true} gap="medium">
198
222
  {plans.map((plan) => (
199
223
  <PlanCard
200
224
  key={plan.tier}
@@ -204,9 +228,10 @@ export function PlanGrid({ context, state, appKey }) {
204
228
  choosing={choosingTier === plan.tier}
205
229
  checkoutUrl={checkoutUrls[`${plan.tier}:${interval}`]}
206
230
  supportUrl={supportUrl}
231
+ maxFeatures={maxFeatures}
207
232
  />
208
233
  ))}
209
- </Flex>
234
+ </AutoGrid>
210
235
 
211
236
  <Text format={{ fontStyle: "italic" }}>
212
237
  Billing is managed in Stripe across all Growth Operations apps. Your new