@growth-operations/ui-shared 1.0.2 → 1.0.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/BillingTab.jsx +74 -45
package/package.json
CHANGED
|
@@ -5,8 +5,7 @@ import {
|
|
|
5
5
|
Heading,
|
|
6
6
|
Text,
|
|
7
7
|
Tile,
|
|
8
|
-
|
|
9
|
-
LoadingSpinner,
|
|
8
|
+
LoadingButton,
|
|
10
9
|
Alert,
|
|
11
10
|
} from "@hubspot/ui-extensions";
|
|
12
11
|
import { useStrictModeEffect } from "../lib/useStrictModeEffect";
|
|
@@ -41,6 +40,10 @@ function StatusPanel({ entitlement }) {
|
|
|
41
40
|
cancel_at_period_end,
|
|
42
41
|
} = entitlement;
|
|
43
42
|
|
|
43
|
+
// HubSpot's Alert renders `title` beside the body in a narrow column, which
|
|
44
|
+
// squeezes multi-line status detail into awkward wraps. Put the headline as a
|
|
45
|
+
// bold first line INSIDE the body (full width) and leave the Alert title unset
|
|
46
|
+
// so the detail flows across the panel.
|
|
44
47
|
if (status === "trialing") {
|
|
45
48
|
const trialEnd = trial_ends_at || current_period_end || sandbox_trial_expires_at;
|
|
46
49
|
const left = daysUntil(trialEnd);
|
|
@@ -51,21 +54,27 @@ function StatusPanel({ entitlement }) {
|
|
|
51
54
|
? "Your free trial ends today"
|
|
52
55
|
: `${left} day${left === 1 ? "" : "s"} left in your free trial`;
|
|
53
56
|
return (
|
|
54
|
-
<Alert
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
<Alert variant="warning">
|
|
58
|
+
<Flex direction="column" gap="extra-small">
|
|
59
|
+
<Text format={{ fontWeight: "bold" }}>{leftLabel}</Text>
|
|
60
|
+
<Text>
|
|
61
|
+
Trial ends {fmtDate(trialEnd)}. Add a plan in Stripe before then to
|
|
62
|
+
keep syncing.
|
|
63
|
+
</Text>
|
|
64
|
+
</Flex>
|
|
59
65
|
</Alert>
|
|
60
66
|
);
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
if (status === "active") {
|
|
64
70
|
return (
|
|
65
|
-
<Alert
|
|
71
|
+
<Alert variant="success">
|
|
66
72
|
<Flex direction="column" gap="extra-small">
|
|
67
|
-
<Text>
|
|
68
|
-
<Text>
|
|
73
|
+
<Text format={{ fontWeight: "bold" }}>You're all set</Text>
|
|
74
|
+
<Text>
|
|
75
|
+
Your subscription is active. Next renewal:{" "}
|
|
76
|
+
{fmtDate(current_period_end)}.
|
|
77
|
+
</Text>
|
|
69
78
|
{cancel_at_period_end && (
|
|
70
79
|
<Text format={{ fontStyle: "italic" }}>
|
|
71
80
|
Cancels at the end of the current period.
|
|
@@ -78,19 +87,27 @@ function StatusPanel({ entitlement }) {
|
|
|
78
87
|
|
|
79
88
|
if (status === "past_due") {
|
|
80
89
|
return (
|
|
81
|
-
<Alert
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
<Alert variant="error">
|
|
91
|
+
<Flex direction="column" gap="extra-small">
|
|
92
|
+
<Text format={{ fontWeight: "bold" }}>
|
|
93
|
+
Your subscription is past due
|
|
94
|
+
</Text>
|
|
95
|
+
<Text>
|
|
96
|
+
We couldn't process your latest payment. Update your payment method
|
|
97
|
+
in Stripe to avoid losing access.
|
|
98
|
+
</Text>
|
|
99
|
+
</Flex>
|
|
86
100
|
</Alert>
|
|
87
101
|
);
|
|
88
102
|
}
|
|
89
103
|
|
|
90
104
|
if (status === "pending_purchase") {
|
|
91
105
|
return (
|
|
92
|
-
<Alert
|
|
93
|
-
<
|
|
106
|
+
<Alert variant="warning">
|
|
107
|
+
<Flex direction="column" gap="extra-small">
|
|
108
|
+
<Text format={{ fontWeight: "bold" }}>Your free trial has ended</Text>
|
|
109
|
+
<Text>Choose a plan in Stripe to resume syncing your data.</Text>
|
|
110
|
+
</Flex>
|
|
94
111
|
</Alert>
|
|
95
112
|
);
|
|
96
113
|
}
|
|
@@ -114,12 +131,18 @@ function usePortalSession({ context, state, appKey }) {
|
|
|
114
131
|
// UI Extensions have no open-URL action — only Link, whose href must exist
|
|
115
132
|
// before render).
|
|
116
133
|
useStrictModeEffect(
|
|
117
|
-
async ({ mounted }) => {
|
|
134
|
+
async ({ mounted, resetOnUnmount }) => {
|
|
118
135
|
// `state` is the /v1/home payload, fetched async by the host page — it's
|
|
119
136
|
// null on the first render(s). Don't mistake "not loaded yet" for
|
|
120
|
-
// "misconfigured": stay in loading until state arrives, then decide.
|
|
121
|
-
//
|
|
122
|
-
|
|
137
|
+
// "misconfigured": stay in loading until state arrives, then decide.
|
|
138
|
+
// IMPORTANT: useStrictModeEffect latches an internal fetchedRef on any
|
|
139
|
+
// normal completion, so a bare `return` here would block the re-run when
|
|
140
|
+
// state later arrives (→ spinner forever). resetOnUnmount() clears that
|
|
141
|
+
// ref so the deps-change re-run (state?.billing_base_url) actually fires.
|
|
142
|
+
if (!state) {
|
|
143
|
+
resetOnUnmount();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
123
146
|
try {
|
|
124
147
|
const base = state.billing_base_url;
|
|
125
148
|
if (!base) throw new Error("Billing service not configured");
|
|
@@ -167,19 +190,23 @@ function TrialSubscriptionBilling({ context, state, appKey }) {
|
|
|
167
190
|
|
|
168
191
|
return (
|
|
169
192
|
<Flex direction="column" gap="medium">
|
|
170
|
-
{/* PRIMARY CTA at the very top, large and obvious (per billing feedback).
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
193
|
+
{/* PRIMARY CTA at the very top, large and obvious (per billing feedback).
|
|
194
|
+
A Button with href navigates to the portal (Button supports href +
|
|
195
|
+
external) so it reads as a real button, not a text link; LoadingButton
|
|
196
|
+
shows the prepared/loading state inline. */}
|
|
197
|
+
{error ? (
|
|
198
|
+
<Alert title="Couldn't open billing" variant="danger">
|
|
199
|
+
<Text>{error}</Text>
|
|
200
|
+
</Alert>
|
|
177
201
|
) : (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
202
|
+
<LoadingButton
|
|
203
|
+
href={portalUrl ? { url: portalUrl, external: true } : undefined}
|
|
204
|
+
loading={loading}
|
|
205
|
+
disabled={!portalUrl}
|
|
206
|
+
variant="primary"
|
|
207
|
+
>
|
|
208
|
+
{portalUrl ? "Manage billing in Stripe" : "Preparing billing…"}
|
|
209
|
+
</LoadingButton>
|
|
183
210
|
)}
|
|
184
211
|
|
|
185
212
|
<StatusPanel entitlement={state?.entitlement} />
|
|
@@ -206,19 +233,21 @@ function CreditsBilling({ context, state, appKey }) {
|
|
|
206
233
|
creditMeter={state?.credit_meter}
|
|
207
234
|
/>
|
|
208
235
|
|
|
209
|
-
{/* PRIMARY CTA at top: pick a plan / buy credits.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
<LoadingSpinner showLabel label="Preparing checkout…" />
|
|
236
|
+
{/* PRIMARY CTA at top: pick a plan / buy credits. Button-with-href so it
|
|
237
|
+
reads as a real button; LoadingButton shows the prepared/loading state. */}
|
|
238
|
+
{error ? (
|
|
239
|
+
<Alert title="Couldn't open checkout" variant="danger">
|
|
240
|
+
<Text>{error}</Text>
|
|
241
|
+
</Alert>
|
|
216
242
|
) : (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
243
|
+
<LoadingButton
|
|
244
|
+
href={portalUrl ? { url: portalUrl, external: true } : undefined}
|
|
245
|
+
loading={loading}
|
|
246
|
+
disabled={!portalUrl}
|
|
247
|
+
variant="primary"
|
|
248
|
+
>
|
|
249
|
+
{portalUrl ? "Pick a plan or buy credits" : "Preparing checkout…"}
|
|
250
|
+
</LoadingButton>
|
|
222
251
|
)}
|
|
223
252
|
|
|
224
253
|
<Text format={{ fontStyle: "italic" }}>
|