@growth-operations/ui-shared 1.25.0 → 1.26.1
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/alerts/AlertsTab.jsx +2 -1
- package/src/billing/AutoProvisionToggle.jsx +25 -21
- package/src/billing/BillingBanner.jsx +21 -19
- package/src/billing/BillingTab.jsx +85 -62
- package/src/billing/SubscriptionGate.jsx +12 -9
- package/src/forms/OptionSelect.jsx +4 -2
- package/src/home/CreditMeter.jsx +4 -2
- package/src/home/FeatureUsage.jsx +28 -22
- package/src/home/HealthHero.jsx +29 -21
- package/src/home/InstallProgress.jsx +24 -19
- package/src/home/PlanFitBanner.jsx +4 -2
- package/src/lib/featureUsage.js +17 -7
- package/src/sandbox/SandboxInstall.jsx +7 -2
- package/src/usage/UsageTab.jsx +4 -1
package/package.json
CHANGED
package/src/alerts/AlertsTab.jsx
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import React, { useState, useEffect } from "react";
|
|
24
24
|
import {
|
|
25
25
|
Alert,
|
|
26
|
+
Box,
|
|
26
27
|
EmptyState,
|
|
27
28
|
Text,
|
|
28
29
|
Link,
|
|
@@ -207,7 +208,7 @@ export function AlertsTab({
|
|
|
207
208
|
if (error) {
|
|
208
209
|
return (
|
|
209
210
|
<Alert title="Couldn't load alerts" variant="error">
|
|
210
|
-
{error}
|
|
211
|
+
<Box flex="auto">{error}</Box>
|
|
211
212
|
</Alert>
|
|
212
213
|
);
|
|
213
214
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useReducer, useState } from "react";
|
|
2
|
-
import { Alert, Button, Flex, Heading, Text, Toggle } from "@hubspot/ui-extensions";
|
|
2
|
+
import { Alert, Box, Button, Flex, Heading, Text, Toggle } from "@hubspot/ui-extensions";
|
|
3
3
|
import {
|
|
4
4
|
AUTO_PROVISION_CONFIRM_NO,
|
|
5
5
|
AUTO_PROVISION_CONFIRM_YES,
|
|
@@ -101,26 +101,28 @@ export function AutoProvisionToggle({
|
|
|
101
101
|
cancel snaps back to idle with the flag untouched). */}
|
|
102
102
|
{ui.phase === "confirming" && (
|
|
103
103
|
<Alert variant="warning" title="Disable automatic credit provisioning?">
|
|
104
|
-
<
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
104
|
+
<Box flex="auto">
|
|
105
|
+
<Flex direction="column" gap="small">
|
|
106
|
+
<Text>{autoProvisionConfirmBody(appName)}</Text>
|
|
107
|
+
<Flex direction="row" gap="small">
|
|
108
|
+
<Button
|
|
109
|
+
variant="destructive"
|
|
110
|
+
onClick={() => {
|
|
111
|
+
dispatch({ type: "confirm" });
|
|
112
|
+
save(false);
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{AUTO_PROVISION_CONFIRM_YES}
|
|
116
|
+
</Button>
|
|
117
|
+
<Button
|
|
118
|
+
variant="secondary"
|
|
119
|
+
onClick={() => dispatch({ type: "cancel" })}
|
|
120
|
+
>
|
|
121
|
+
{AUTO_PROVISION_CONFIRM_NO}
|
|
122
|
+
</Button>
|
|
123
|
+
</Flex>
|
|
122
124
|
</Flex>
|
|
123
|
-
</
|
|
125
|
+
</Box>
|
|
124
126
|
</Alert>
|
|
125
127
|
)}
|
|
126
128
|
|
|
@@ -129,7 +131,9 @@ export function AutoProvisionToggle({
|
|
|
129
131
|
variant={ui.notice.variant}
|
|
130
132
|
title={ui.notice.variant === "success" ? "Saved" : "Couldn't save"}
|
|
131
133
|
>
|
|
132
|
-
<
|
|
134
|
+
<Box flex="auto">
|
|
135
|
+
<Text>{ui.notice.text}</Text>
|
|
136
|
+
</Box>
|
|
133
137
|
</Alert>
|
|
134
138
|
)}
|
|
135
139
|
</Flex>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Flex, Text, Alert, Button } from "@hubspot/ui-extensions";
|
|
2
|
+
import { Box, Flex, Text, Alert, Button } from "@hubspot/ui-extensions";
|
|
3
3
|
import { resolveBillingBanner } from "../lib/creditDisplay";
|
|
4
4
|
|
|
5
5
|
// Shared app-wide billing banner for the credit archetype (CreditState.
|
|
@@ -94,24 +94,26 @@ export function BillingBanner({ state, actionLabel, actionsNoun, appName, onNavi
|
|
|
94
94
|
|
|
95
95
|
return (
|
|
96
96
|
<Alert title={banner.title} variant={banner.variant}>
|
|
97
|
-
<
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
97
|
+
<Box flex="auto">
|
|
98
|
+
<Flex direction="column" gap="extra-small">
|
|
99
|
+
<Text>{banner.message}</Text>
|
|
100
|
+
{/* The support footer line rides the trial-state banners (the same
|
|
101
|
+
microcopy the retired Home TrialBanner rendered). */}
|
|
102
|
+
{banner.footer && <Text variant="microcopy">{banner.footer}</Text>}
|
|
103
|
+
{banner.cta?.kind === "portal" && portalStartUrl ? (
|
|
104
|
+
<Button
|
|
105
|
+
variant="secondary"
|
|
106
|
+
href={{ url: portalStartUrl, external: true }}
|
|
107
|
+
>
|
|
108
|
+
{banner.cta.label}
|
|
109
|
+
</Button>
|
|
110
|
+
) : !suppressInAppCta && banner.cta && onNavigate ? (
|
|
111
|
+
<Button variant="secondary" onClick={() => onNavigate("/billing")}>
|
|
112
|
+
{banner.cta.kind === "portal" ? "Go to Billing →" : banner.cta.label}
|
|
113
|
+
</Button>
|
|
114
|
+
) : null}
|
|
115
|
+
</Flex>
|
|
116
|
+
</Box>
|
|
115
117
|
</Alert>
|
|
116
118
|
);
|
|
117
119
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useState } from "react";
|
|
2
2
|
import {
|
|
3
|
+
Box,
|
|
3
4
|
Flex,
|
|
4
5
|
Heading,
|
|
5
6
|
Text,
|
|
@@ -130,12 +131,14 @@ export function StatusPanel({ entitlement, plansAvailable = true, restartAvailab
|
|
|
130
131
|
: "Add a payment method in Stripe before then so your subscription continues.";
|
|
131
132
|
return (
|
|
132
133
|
<Alert variant="warning">
|
|
133
|
-
<
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
<Box flex="auto">
|
|
135
|
+
<Flex direction="column" gap="extra-small">
|
|
136
|
+
<Text format={{ fontWeight: "bold" }}>{leftLabel}</Text>
|
|
137
|
+
<Text>
|
|
138
|
+
Trial ends {fmtDate(trialEnd)}. {trialCta}
|
|
139
|
+
</Text>
|
|
140
|
+
</Flex>
|
|
141
|
+
</Box>
|
|
139
142
|
</Alert>
|
|
140
143
|
);
|
|
141
144
|
}
|
|
@@ -149,28 +152,32 @@ export function StatusPanel({ entitlement, plansAvailable = true, restartAvailab
|
|
|
149
152
|
if (cancel_at_period_end) {
|
|
150
153
|
return (
|
|
151
154
|
<Alert variant="warning">
|
|
152
|
-
<
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
<Box flex="auto">
|
|
156
|
+
<Flex direction="column" gap="extra-small">
|
|
157
|
+
<Text format={{ fontWeight: "bold" }}>
|
|
158
|
+
Your subscription is set to cancel
|
|
159
|
+
</Text>
|
|
160
|
+
<Text>
|
|
161
|
+
It stays active until {fmtDate(current_period_end)}, then your
|
|
162
|
+
data stops syncing. Changed your mind? Keep it below — no
|
|
163
|
+
re-checkout needed.
|
|
164
|
+
</Text>
|
|
165
|
+
</Flex>
|
|
166
|
+
</Box>
|
|
162
167
|
</Alert>
|
|
163
168
|
);
|
|
164
169
|
}
|
|
165
170
|
return (
|
|
166
171
|
<Alert variant="success">
|
|
167
|
-
<
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
172
|
+
<Box flex="auto">
|
|
173
|
+
<Flex direction="column" gap="extra-small">
|
|
174
|
+
<Text format={{ fontWeight: "bold" }}>You're all set</Text>
|
|
175
|
+
<Text>
|
|
176
|
+
Your subscription is active. Next renewal:{" "}
|
|
177
|
+
{fmtDate(current_period_end)}.
|
|
178
|
+
</Text>
|
|
179
|
+
</Flex>
|
|
180
|
+
</Box>
|
|
174
181
|
</Alert>
|
|
175
182
|
);
|
|
176
183
|
}
|
|
@@ -178,15 +185,17 @@ export function StatusPanel({ entitlement, plansAvailable = true, restartAvailab
|
|
|
178
185
|
if (status === "past_due") {
|
|
179
186
|
return (
|
|
180
187
|
<Alert variant="error">
|
|
181
|
-
<
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
<Box flex="auto">
|
|
189
|
+
<Flex direction="column" gap="extra-small">
|
|
190
|
+
<Text format={{ fontWeight: "bold" }}>
|
|
191
|
+
Your subscription is past due
|
|
192
|
+
</Text>
|
|
193
|
+
<Text>
|
|
194
|
+
We couldn't process your latest payment. Update your payment method
|
|
195
|
+
in Stripe to avoid losing access.
|
|
196
|
+
</Text>
|
|
197
|
+
</Flex>
|
|
198
|
+
</Box>
|
|
190
199
|
</Alert>
|
|
191
200
|
);
|
|
192
201
|
}
|
|
@@ -205,10 +214,12 @@ export function StatusPanel({ entitlement, plansAvailable = true, restartAvailab
|
|
|
205
214
|
: "Choose a plan below to resume syncing your data.";
|
|
206
215
|
return (
|
|
207
216
|
<Alert variant="warning">
|
|
208
|
-
<
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
|
|
217
|
+
<Box flex="auto">
|
|
218
|
+
<Flex direction="column" gap="extra-small">
|
|
219
|
+
<Text format={{ fontWeight: "bold" }}>Your free trial has ended</Text>
|
|
220
|
+
<Text>{resumeCta}</Text>
|
|
221
|
+
</Flex>
|
|
222
|
+
</Box>
|
|
212
223
|
</Alert>
|
|
213
224
|
);
|
|
214
225
|
}
|
|
@@ -227,13 +238,15 @@ export function StatusPanel({ entitlement, plansAvailable = true, restartAvailab
|
|
|
227
238
|
: "Restart your previous plan below in one click, or choose a different one — either way your data starts syncing again.";
|
|
228
239
|
return (
|
|
229
240
|
<Alert variant="warning">
|
|
230
|
-
<
|
|
231
|
-
<
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
241
|
+
<Box flex="auto">
|
|
242
|
+
<Flex direction="column" gap="extra-small">
|
|
243
|
+
<Text format={{ fontWeight: "bold" }}>Your subscription was canceled</Text>
|
|
244
|
+
<Text>{resumeCta}</Text>
|
|
245
|
+
{!plansAvailable && (
|
|
246
|
+
<Text variant="microcopy">{SUPPORT_FOOTER}</Text>
|
|
247
|
+
)}
|
|
248
|
+
</Flex>
|
|
249
|
+
</Box>
|
|
237
250
|
</Alert>
|
|
238
251
|
);
|
|
239
252
|
}
|
|
@@ -258,12 +271,14 @@ export function TrialSubscriptionBilling({ context, state, appKey, openIframe =
|
|
|
258
271
|
return (
|
|
259
272
|
<Flex direction="column" gap="medium">
|
|
260
273
|
<Alert title="Billing isn't set up yet" variant="info">
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
274
|
+
<Box flex="auto">
|
|
275
|
+
<Text>
|
|
276
|
+
Your trial and billing are still being set up for this install. Once
|
|
277
|
+
setup completes, manage your plan and payment method here. If this
|
|
278
|
+
persists, reinstall the app from the HubSpot marketplace to complete
|
|
279
|
+
sign-in.
|
|
280
|
+
</Text>
|
|
281
|
+
</Box>
|
|
267
282
|
</Alert>
|
|
268
283
|
</Flex>
|
|
269
284
|
);
|
|
@@ -545,7 +560,9 @@ function InvoiceHistory({ billingBaseUrl, billingActionToken }) {
|
|
|
545
560
|
if (status === "error") {
|
|
546
561
|
return (
|
|
547
562
|
<Alert title="Couldn't load invoice history" variant="warning">
|
|
548
|
-
<
|
|
563
|
+
<Box flex="auto">
|
|
564
|
+
<Text>Refresh the page to try again.</Text>
|
|
565
|
+
</Box>
|
|
549
566
|
</Alert>
|
|
550
567
|
);
|
|
551
568
|
}
|
|
@@ -719,10 +736,12 @@ function LegacyBilling({ state }) {
|
|
|
719
736
|
return (
|
|
720
737
|
<Flex direction="column" gap="medium">
|
|
721
738
|
<Alert title="Managed through your existing subscription" variant="info">
|
|
722
|
-
<
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
739
|
+
<Box flex="auto">
|
|
740
|
+
<Text>
|
|
741
|
+
Your plan and payment method are billed through your existing
|
|
742
|
+
subscription. There's nothing to set up here.
|
|
743
|
+
</Text>
|
|
744
|
+
</Box>
|
|
726
745
|
</Alert>
|
|
727
746
|
{manageUrl && (
|
|
728
747
|
<Button href={{ url: manageUrl, external: true }} variant="secondary">
|
|
@@ -736,13 +755,15 @@ function LegacyBilling({ state }) {
|
|
|
736
755
|
function CompedBilling() {
|
|
737
756
|
return (
|
|
738
757
|
<Alert variant="success">
|
|
739
|
-
<
|
|
740
|
-
<
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
758
|
+
<Box flex="auto">
|
|
759
|
+
<Flex direction="column" gap="extra-small">
|
|
760
|
+
<Text format={{ fontWeight: "bold" }}>Free access</Text>
|
|
761
|
+
<Text>
|
|
762
|
+
Your account has been granted complimentary access as a Growth Operations partner or
|
|
763
|
+
team member. No payment method or subscription is required.
|
|
764
|
+
</Text>
|
|
765
|
+
</Flex>
|
|
766
|
+
</Box>
|
|
746
767
|
</Alert>
|
|
747
768
|
);
|
|
748
769
|
}
|
|
@@ -770,7 +791,9 @@ export function BillingTab({ context, state, appKey, openIframe = null }) {
|
|
|
770
791
|
<Heading>Billing</Heading>
|
|
771
792
|
{!isSuperAdmin ? (
|
|
772
793
|
<Alert variant="warning" title="Access restricted">
|
|
773
|
-
<
|
|
794
|
+
<Box flex="auto">
|
|
795
|
+
<Text>Billing settings are only available to super admins.</Text>
|
|
796
|
+
</Box>
|
|
774
797
|
</Alert>
|
|
775
798
|
) : mode === "legacy" ? (
|
|
776
799
|
<LegacyBilling state={billingState} />
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Alert,
|
|
4
|
+
Box,
|
|
4
5
|
Flex,
|
|
5
6
|
Link,
|
|
6
7
|
LoadingSpinner,
|
|
@@ -80,15 +81,17 @@ export function SubscriptionGate({
|
|
|
80
81
|
|
|
81
82
|
return (
|
|
82
83
|
<Alert title="No active subscription" variant="error">
|
|
83
|
-
<
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
<Box flex="auto">
|
|
85
|
+
<Flex direction="column" gap="extra-small">
|
|
86
|
+
<Text>
|
|
87
|
+
Your {appName} subscription was canceled. Your data is still here —
|
|
88
|
+
manage billing to pick up where you left off.
|
|
89
|
+
</Text>
|
|
90
|
+
{billingUrl && (
|
|
91
|
+
<Link href={{ url: billingUrl, external: true }}>Go to Billing</Link>
|
|
92
|
+
)}
|
|
93
|
+
</Flex>
|
|
94
|
+
</Box>
|
|
92
95
|
</Alert>
|
|
93
96
|
);
|
|
94
97
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
-
import { Select, LoadingSpinner, Alert, Flex, Text, Link } from "@hubspot/ui-extensions";
|
|
2
|
+
import { Box, Select, LoadingSpinner, Alert, Flex, Text, Link } from "@hubspot/ui-extensions";
|
|
3
3
|
import { useToken } from "../sdk/index";
|
|
4
4
|
import { getForms } from "../sdk/hubspot/forms";
|
|
5
5
|
import { getLists } from "../sdk/hubspot/lists";
|
|
@@ -137,7 +137,9 @@ export function OptionSelect({
|
|
|
137
137
|
if (error) {
|
|
138
138
|
return (
|
|
139
139
|
<Alert title={`Couldn't load ${label.toLowerCase()}`} variant="danger">
|
|
140
|
-
<
|
|
140
|
+
<Box flex="auto">
|
|
141
|
+
<Text>{error}</Text>
|
|
142
|
+
</Box>
|
|
141
143
|
</Alert>
|
|
142
144
|
);
|
|
143
145
|
}
|
package/src/home/CreditMeter.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Flex, Tile, Text, ProgressBar, StatusTag, Alert } from "@hubspot/ui-extensions";
|
|
2
|
+
import { Box, Flex, Tile, Text, ProgressBar, StatusTag, Alert } from "@hubspot/ui-extensions";
|
|
3
3
|
import { daysUntil, fmtDate } from "../lib/format";
|
|
4
4
|
import { resolveCreditMeter } from "../lib/creditDisplay";
|
|
5
5
|
|
|
@@ -142,7 +142,9 @@ export function CreditMeter({ entitlement, creditMeter, credits = null }) {
|
|
|
142
142
|
|
|
143
143
|
{alert && (
|
|
144
144
|
<Alert title={alert.title} variant={alert.variant}>
|
|
145
|
-
<
|
|
145
|
+
<Box flex="auto">
|
|
146
|
+
<Text>{alert.message}</Text>
|
|
147
|
+
</Box>
|
|
146
148
|
</Alert>
|
|
147
149
|
)}
|
|
148
150
|
</Flex>
|
|
@@ -12,9 +12,10 @@ import {
|
|
|
12
12
|
//
|
|
13
13
|
// Composition: most-used first (what this install actually runs on), then
|
|
14
14
|
// the not-in-use catalog as "Recommended to explore" — each with its
|
|
15
|
-
// one-line rationale and
|
|
16
|
-
// has rows, and the whole thing renders NOTHING
|
|
17
|
-
// (older backends, legacy installs) — absence is
|
|
15
|
+
// one-line rationale and (for super admins) its setup CTA links. Each
|
|
16
|
+
// section renders only when it has rows, and the whole thing renders NOTHING
|
|
17
|
+
// when the block is absent (older backends, legacy installs) — absence is
|
|
18
|
+
// never an empty box.
|
|
18
19
|
|
|
19
20
|
// Singular/plural label for a row's unit ("credits" | "events") — the wire
|
|
20
21
|
// field is credits_or_events, so an event-denominated feature must not be
|
|
@@ -61,29 +62,34 @@ function RecommendedTile({ notInUse, isSuperAdmin, onNavigate }) {
|
|
|
61
62
|
<Text format={{ fontWeight: "bold" }}>Recommended to explore</Text>
|
|
62
63
|
{picks.map((f) => {
|
|
63
64
|
// Setup CTAs point at admin-only surfaces (the layout editor above
|
|
64
|
-
// all) — show them to super admins only; everyone else
|
|
65
|
-
//
|
|
66
|
-
// worse than no link).
|
|
67
|
-
|
|
65
|
+
// all) — show them to super admins only; everyone else gets name +
|
|
66
|
+
// rationale alone (a non-admin clicking into a HubSpot permission
|
|
67
|
+
// wall is worse than no link).
|
|
68
|
+
//
|
|
69
|
+
// "Learn more →" (docs_url) intentionally NOT rendered (Jasper
|
|
70
|
+
// 2026-10-14): every feature's docs_url points at the same single
|
|
71
|
+
// setup guide — noise until per-feature articles exist. docs_url
|
|
72
|
+
// stays in the wire contract; re-enabling is a one-line revert.
|
|
73
|
+
const ctas = isSuperAdmin ? f.ctas : [];
|
|
68
74
|
return (
|
|
69
75
|
<Flex key={f.key} direction="column" gap="extra-small">
|
|
70
76
|
<Text format={{ fontWeight: "demibold" }}>{f.name}</Text>
|
|
71
77
|
{f.rationale ? <Text>{f.rationale}</Text> : null}
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
)
|
|
78
|
+
{ctas.map((cta) =>
|
|
79
|
+
cta.url ? (
|
|
80
|
+
<Link key={cta.label} href={{ url: cta.url, external: true }}>
|
|
81
|
+
{cta.label} →
|
|
82
|
+
</Link>
|
|
83
|
+
) : onNavigate ? (
|
|
84
|
+
<Button
|
|
85
|
+
key={cta.label}
|
|
86
|
+
variant="secondary"
|
|
87
|
+
onClick={() => onNavigate(cta.route)}
|
|
88
|
+
>
|
|
89
|
+
{cta.label}
|
|
90
|
+
</Button>
|
|
91
|
+
) : null
|
|
92
|
+
)}
|
|
87
93
|
</Flex>
|
|
88
94
|
);
|
|
89
95
|
})}
|
package/src/home/HealthHero.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Flex, Heading, Alert, Text } from "@hubspot/ui-extensions";
|
|
2
|
+
import { Box, Flex, Heading, Alert, Text } from "@hubspot/ui-extensions";
|
|
3
3
|
import { humanizeStatus, statusVariant } from "../lib/format";
|
|
4
4
|
|
|
5
5
|
const HEALTH_VARIANT = { healthy: "success", attention: "warning", critical: "error" };
|
|
@@ -38,12 +38,14 @@ export function HealthHero({ appName, state }) {
|
|
|
38
38
|
title={HEALTH_TITLE[health.level] ?? "Status"}
|
|
39
39
|
variant={HEALTH_VARIANT[health.level] ?? "info"}
|
|
40
40
|
>
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
<Box flex="auto">
|
|
42
|
+
<Flex direction="column" gap="extra-small">
|
|
43
|
+
{(health.reasons ?? []).map((r, i) => (
|
|
44
|
+
<Text key={i}>{r}</Text>
|
|
45
|
+
))}
|
|
46
|
+
{onRightPlan && <Text>...and you're on the right plan</Text>}
|
|
47
|
+
</Flex>
|
|
48
|
+
</Box>
|
|
47
49
|
</Alert>
|
|
48
50
|
</Flex>
|
|
49
51
|
);
|
|
@@ -64,7 +66,9 @@ export function HealthHero({ appName, state }) {
|
|
|
64
66
|
<Flex direction="column" gap="small">
|
|
65
67
|
{name && <Heading>{name}</Heading>}
|
|
66
68
|
<Alert title="Free access" variant="success">
|
|
67
|
-
<
|
|
69
|
+
<Box flex="auto">
|
|
70
|
+
<Text>Partner or team member access — no subscription required.</Text>
|
|
71
|
+
</Box>
|
|
68
72
|
</Alert>
|
|
69
73
|
</Flex>
|
|
70
74
|
);
|
|
@@ -84,13 +88,15 @@ export function HealthHero({ appName, state }) {
|
|
|
84
88
|
<Flex direction="column" gap="small">
|
|
85
89
|
{name && <Heading>{name}</Heading>}
|
|
86
90
|
<Alert title={title} variant={variant}>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
<Box flex="auto">
|
|
92
|
+
{(depleted || low) && (
|
|
93
|
+
<Text>
|
|
94
|
+
{depleted
|
|
95
|
+
? "Pick a plan in Billing to keep going."
|
|
96
|
+
: "You're getting close to your limit — consider a plan in Billing."}
|
|
97
|
+
</Text>
|
|
98
|
+
)}
|
|
99
|
+
</Box>
|
|
94
100
|
</Alert>
|
|
95
101
|
</Flex>
|
|
96
102
|
);
|
|
@@ -101,12 +107,14 @@ export function HealthHero({ appName, state }) {
|
|
|
101
107
|
<Flex direction="column" gap="small">
|
|
102
108
|
{name && <Heading>{name}</Heading>}
|
|
103
109
|
<Alert title={humanizeStatus(ent.status)} variant={statusVariant(ent.status)}>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
<Box flex="auto">
|
|
111
|
+
{ent.entitled === false && (
|
|
112
|
+
<Text>
|
|
113
|
+
This install isn't on an active plan. Open Billing to choose a plan
|
|
114
|
+
and resume syncing.
|
|
115
|
+
</Text>
|
|
116
|
+
)}
|
|
117
|
+
</Box>
|
|
110
118
|
</Alert>
|
|
111
119
|
</Flex>
|
|
112
120
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useRef } from "react";
|
|
2
2
|
import {
|
|
3
|
+
Box,
|
|
3
4
|
Flex,
|
|
4
5
|
Tile,
|
|
5
6
|
Text,
|
|
@@ -54,20 +55,22 @@ export function InstallProgress({ state, onRetry }) {
|
|
|
54
55
|
if (status === "failed") {
|
|
55
56
|
return (
|
|
56
57
|
<Alert title="Setup didn't finish" variant="error">
|
|
57
|
-
<
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
warnings.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
58
|
+
<Box flex="auto">
|
|
59
|
+
<Flex direction="column" gap="small">
|
|
60
|
+
<Text>
|
|
61
|
+
We hit a problem finishing your install. This often clears itself on
|
|
62
|
+
the next automatic retry; if it persists, retry now or contact
|
|
63
|
+
support.
|
|
64
|
+
</Text>
|
|
65
|
+
{warnings.length > 0 &&
|
|
66
|
+
warnings.map((w, i) => <Text key={i}>• {w}</Text>)}
|
|
67
|
+
{onRetry && (
|
|
68
|
+
<Button variant="primary" onClick={onRetry}>
|
|
69
|
+
Retry setup
|
|
70
|
+
</Button>
|
|
71
|
+
)}
|
|
72
|
+
</Flex>
|
|
73
|
+
</Box>
|
|
71
74
|
</Alert>
|
|
72
75
|
);
|
|
73
76
|
}
|
|
@@ -95,11 +98,13 @@ export function InstallProgress({ state, onRetry }) {
|
|
|
95
98
|
if (complete && warnings.length > 0) {
|
|
96
99
|
return (
|
|
97
100
|
<Alert title="Setup finished with warnings" variant="warning">
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
<Box flex="auto">
|
|
102
|
+
<Flex direction="column" gap="extra-small">
|
|
103
|
+
{warnings.map((w, i) => (
|
|
104
|
+
<Text key={i}>• {w}</Text>
|
|
105
|
+
))}
|
|
106
|
+
</Flex>
|
|
107
|
+
</Box>
|
|
103
108
|
</Alert>
|
|
104
109
|
);
|
|
105
110
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Text, Alert } from "@hubspot/ui-extensions";
|
|
2
|
+
import { Box, Text, Alert } from "@hubspot/ui-extensions";
|
|
3
3
|
import { planFitOf } from "../lib/homeBlocks";
|
|
4
4
|
|
|
5
5
|
// The plan-fit savings banner: when state.plan_fit.verdict is
|
|
@@ -22,7 +22,9 @@ export function PlanFitBanner({ state }) {
|
|
|
22
22
|
|
|
23
23
|
return (
|
|
24
24
|
<Alert title="You could save on a higher plan" variant="warning">
|
|
25
|
-
<
|
|
25
|
+
<Box flex="auto">
|
|
26
|
+
<Text>{fit.text}</Text>
|
|
27
|
+
</Box>
|
|
26
28
|
</Alert>
|
|
27
29
|
);
|
|
28
30
|
}
|
package/src/lib/featureUsage.js
CHANGED
|
@@ -15,9 +15,21 @@
|
|
|
15
15
|
const normalizeUnit = (u) =>
|
|
16
16
|
typeof u === "string" && u.toLowerCase().startsWith("event") ? "events" : "credits";
|
|
17
17
|
|
|
18
|
+
// One well-formed setup CTA is {label: non-empty string, url XOR route:
|
|
19
|
+
// non-empty string}. Anything else (missing label, both targets, neither
|
|
20
|
+
// target, wrong types) is dropped — a malformed item never renders.
|
|
21
|
+
const normalizeCta = (c) => {
|
|
22
|
+
if (!c || typeof c !== "object") return null;
|
|
23
|
+
const label = typeof c.label === "string" && c.label ? c.label : null;
|
|
24
|
+
const url = typeof c.url === "string" && c.url ? c.url : null;
|
|
25
|
+
const route = typeof c.route === "string" && c.route ? c.route : null;
|
|
26
|
+
if (!label || !(url || route) || (url && route)) return null;
|
|
27
|
+
return url ? { label, url } : { label, route };
|
|
28
|
+
};
|
|
29
|
+
|
|
18
30
|
// Resolve the feature_usage block into render-ready shape:
|
|
19
31
|
// { mostUsed: [{ key, name, count, unit }],
|
|
20
|
-
// notInUse: [{ key, name, docsUrl, rationale }] }
|
|
32
|
+
// notInUse: [{ key, name, docsUrl, rationale, ctas }] }
|
|
21
33
|
// Returns null when the block is absent/malformed or BOTH lists are empty
|
|
22
34
|
// (an install using every detectable feature has nothing to show here).
|
|
23
35
|
export function resolveFeatureUsage(featureUsage) {
|
|
@@ -39,12 +51,10 @@ export function resolveFeatureUsage(featureUsage) {
|
|
|
39
51
|
name: f.name,
|
|
40
52
|
docsUrl: typeof f.docs_url === "string" && f.docs_url ? f.docs_url : null,
|
|
41
53
|
rationale: typeof f.rationale === "string" ? f.rationale : "",
|
|
42
|
-
// Optional SETUP
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
ctaUrl: typeof f.cta_url === "string" && f.cta_url ? f.cta_url : null,
|
|
47
|
-
ctaRoute: typeof f.cta_route === "string" && f.cta_route ? f.cta_route : null,
|
|
54
|
+
// Optional SETUP actions (1.26.0 — a LIST per feature: one feature can
|
|
55
|
+
// surface in multiple places, e.g. a card on both contacts and
|
|
56
|
+
// applications). Each item is {label, url|route}; absent/empty -> [].
|
|
57
|
+
ctas: (Array.isArray(f.ctas) ? f.ctas : []).map(normalizeCta).filter(Boolean),
|
|
48
58
|
}));
|
|
49
59
|
|
|
50
60
|
if (mostUsed.length === 0 && notInUse.length === 0) return null;
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
// calm message.
|
|
32
32
|
import React, { useEffect, useState } from "react";
|
|
33
33
|
import {
|
|
34
|
+
Box,
|
|
34
35
|
Flex,
|
|
35
36
|
Tile,
|
|
36
37
|
Heading,
|
|
@@ -141,13 +142,17 @@ export function SandboxInstall({
|
|
|
141
142
|
|
|
142
143
|
{error && (
|
|
143
144
|
<Alert title="Couldn't create a sandbox link" variant="warning">
|
|
144
|
-
<
|
|
145
|
+
<Box flex="auto">
|
|
146
|
+
<Text>{error}</Text>
|
|
147
|
+
</Box>
|
|
145
148
|
</Alert>
|
|
146
149
|
)}
|
|
147
150
|
|
|
148
151
|
{uninstallError && (
|
|
149
152
|
<Alert title="Couldn't uninstall that sandbox" variant="warning">
|
|
150
|
-
<
|
|
153
|
+
<Box flex="auto">
|
|
154
|
+
<Text>{uninstallError}</Text>
|
|
155
|
+
</Box>
|
|
151
156
|
</Alert>
|
|
152
157
|
)}
|
|
153
158
|
|
package/src/usage/UsageTab.jsx
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import React, { useState } from "react";
|
|
14
14
|
import {
|
|
15
15
|
hubspot,
|
|
16
|
+
Box,
|
|
16
17
|
Flex,
|
|
17
18
|
Heading,
|
|
18
19
|
Text,
|
|
@@ -131,7 +132,9 @@ function UsageContent({ context, basePath, portalId, appName, days }) {
|
|
|
131
132
|
if (error) {
|
|
132
133
|
return (
|
|
133
134
|
<Alert title="Couldn't load usage" variant="warning">
|
|
134
|
-
<
|
|
135
|
+
<Box flex="auto">
|
|
136
|
+
<Text>{error}</Text>
|
|
137
|
+
</Box>
|
|
135
138
|
</Alert>
|
|
136
139
|
);
|
|
137
140
|
}
|