@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 +1 -1
- package/src/billing/PlanGrid.jsx +56 -31
package/package.json
CHANGED
package/src/billing/PlanGrid.jsx
CHANGED
|
@@ -2,7 +2,7 @@ import React, { useState } from "react";
|
|
|
2
2
|
import {
|
|
3
3
|
hubspot,
|
|
4
4
|
Flex,
|
|
5
|
-
|
|
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({
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
</
|
|
234
|
+
</AutoGrid>
|
|
210
235
|
|
|
211
236
|
<Text format={{ fontStyle: "italic" }}>
|
|
212
237
|
Billing is managed in Stripe across all Growth Operations apps. Your new
|