@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-operations/ui-shared",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Shared HubSpot UI-extension modules (Home, Billing) for Growth Operations apps.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -5,8 +5,7 @@ import {
5
5
  Heading,
6
6
  Text,
7
7
  Tile,
8
- Link,
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 title={leftLabel} variant="warning">
55
- <Text>
56
- Trial ends {fmtDate(trialEnd)}. Add a plan in Stripe before then to
57
- keep syncing.
58
- </Text>
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 title="You're all set" variant="success">
71
+ <Alert variant="success">
66
72
  <Flex direction="column" gap="extra-small">
67
- <Text>Your subscription is active.</Text>
68
- <Text>Next renewal: {fmtDate(current_period_end)}</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 title="Your subscription is past due" variant="error">
82
- <Text>
83
- We couldn't process your latest payment. Update your payment method in
84
- Stripe to avoid losing access.
85
- </Text>
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 title="Your free trial has ended" variant="warning">
93
- <Text>Choose a plan in Stripe to resume syncing your data.</Text>
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. The
121
- // effect re-runs when state?.billing_base_url changes (see deps).
122
- if (!state) return;
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
- {portalUrl ? (
172
- <Link href={{ url: portalUrl, external: true }} variant="primary">
173
- Manage billing in Stripe
174
- </Link>
175
- ) : loading ? (
176
- <LoadingSpinner showLabel label="Preparing billing…" />
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
- error && (
179
- <Alert title="Couldn't open billing" variant="danger">
180
- <Text>{error}</Text>
181
- </Alert>
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
- {portalUrl ? (
211
- <Link href={{ url: portalUrl, external: true }} variant="primary">
212
- Pick a plan or buy credits
213
- </Link>
214
- ) : loading ? (
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
- error && (
218
- <Alert title="Couldn't open checkout" variant="danger">
219
- <Text>{error}</Text>
220
- </Alert>
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" }}>