@growth-operations/ui-shared 1.0.3 → 1.0.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 +1 -1
- package/src/billing/BillingTab.jsx +64 -41
- package/src/sdk/app/settings.js +10 -2
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
|
}
|
|
@@ -173,19 +190,23 @@ function TrialSubscriptionBilling({ context, state, appKey }) {
|
|
|
173
190
|
|
|
174
191
|
return (
|
|
175
192
|
<Flex direction="column" gap="medium">
|
|
176
|
-
{/* PRIMARY CTA at the very top, large and obvious (per billing feedback).
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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>
|
|
183
201
|
) : (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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>
|
|
189
210
|
)}
|
|
190
211
|
|
|
191
212
|
<StatusPanel entitlement={state?.entitlement} />
|
|
@@ -212,19 +233,21 @@ function CreditsBilling({ context, state, appKey }) {
|
|
|
212
233
|
creditMeter={state?.credit_meter}
|
|
213
234
|
/>
|
|
214
235
|
|
|
215
|
-
{/* PRIMARY CTA at top: pick a plan / buy credits.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
<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>
|
|
222
242
|
) : (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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>
|
|
228
251
|
)}
|
|
229
252
|
|
|
230
253
|
<Text format={{ fontStyle: "italic" }}>
|
package/src/sdk/app/settings.js
CHANGED
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
// shared with other importers.
|
|
9
9
|
//
|
|
10
10
|
// getSettings accepts { force } to bypass the cache (e.g. polling for live sync
|
|
11
|
-
// status, which a stale cache would otherwise mask).
|
|
11
|
+
// status, which a stale cache would otherwise mask). `force` bypasses BOTH the
|
|
12
|
+
// frontend module cache AND the backend account cache: it appends ?fresh=1 so a
|
|
13
|
+
// backend that honors it (e.g. toast's CacheableMixin-backed /settings) reads
|
|
14
|
+
// live state instead of its own ~120s-cached copy. updateSettings invalidates
|
|
12
15
|
// the cache on write.
|
|
13
16
|
//
|
|
14
17
|
// All requests go through callAppApi → hubspot.fetch rules apply (object body,
|
|
@@ -33,9 +36,14 @@ export function makeSettingsApi({ path = "/api/v1/settings" } = {}) {
|
|
|
33
36
|
return settingsPromise;
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
// On a forced read, also ask the backend to bypass its own cache (?fresh=1)
|
|
40
|
+
// so we don't just re-fetch a server-side-stale copy.
|
|
41
|
+
const reqPath = force
|
|
42
|
+
? `${path}${path.includes("?") ? "&" : "?"}fresh=1`
|
|
43
|
+
: path;
|
|
36
44
|
const fetchPromise = (async () => {
|
|
37
45
|
try {
|
|
38
|
-
const settings = await callAppApi(context,
|
|
46
|
+
const settings = await callAppApi(context, reqPath, "GET");
|
|
39
47
|
cachedSettings = settings;
|
|
40
48
|
settingsPromise = null;
|
|
41
49
|
return settings;
|