@growth-operations/ui-shared 1.0.7 → 1.0.9
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 +52 -7
- package/src/home/HelpfulLinks.jsx +67 -0
- package/src/index.js +1 -0
package/package.json
CHANGED
|
@@ -145,6 +145,11 @@ function usePortalSession({ context, state, appKey }) {
|
|
|
145
145
|
const [loading, setLoading] = useState(true);
|
|
146
146
|
const [error, setError] = useState(null);
|
|
147
147
|
const [portalUrl, setPortalUrl] = useState(null);
|
|
148
|
+
// "Not ready" = the install hasn't been provisioned with a Stripe customer
|
|
149
|
+
// yet (fresh/old account: no entitlement.status, or the portal endpoint 409s
|
|
150
|
+
// with "no Stripe customer"). That's an expected pre-purchase state, not an
|
|
151
|
+
// error — the UI shows a calm "finish setup" message instead of a red alert.
|
|
152
|
+
const [notReady, setNotReady] = useState(false);
|
|
148
153
|
|
|
149
154
|
// Pre-create on mount so the link is one real click (Stripe can't be iframed;
|
|
150
155
|
// UI Extensions have no open-URL action — only Link, whose href must exist
|
|
@@ -162,6 +167,17 @@ function usePortalSession({ context, state, appKey }) {
|
|
|
162
167
|
resetOnUnmount();
|
|
163
168
|
return;
|
|
164
169
|
}
|
|
170
|
+
// No Stripe customer yet → don't even try to open a portal. An install
|
|
171
|
+
// that hasn't been provisioned (no entitlement, or status null/
|
|
172
|
+
// not_installed) has nothing to manage; show the calm "not ready" state.
|
|
173
|
+
const status = state?.entitlement?.status;
|
|
174
|
+
if (!state.entitlement || status == null || status === "not_installed") {
|
|
175
|
+
if (mounted.current) {
|
|
176
|
+
setNotReady(true);
|
|
177
|
+
setLoading(false);
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
165
181
|
try {
|
|
166
182
|
const base = state.billing_base_url;
|
|
167
183
|
if (!base) throw new Error("Billing service not configured");
|
|
@@ -187,25 +203,54 @@ function usePortalSession({ context, state, appKey }) {
|
|
|
187
203
|
});
|
|
188
204
|
if (!res.ok) {
|
|
189
205
|
const text = await res.text();
|
|
190
|
-
|
|
206
|
+
// 409 "no Stripe customer yet" is the not-provisioned case, not a
|
|
207
|
+
// failure — surface it as the calm not-ready state.
|
|
208
|
+
if (res.status === 409 && /no stripe customer/i.test(text)) {
|
|
209
|
+
if (mounted.current) setNotReady(true);
|
|
210
|
+
} else {
|
|
211
|
+
throw new Error(`Billing portal request failed (${res.status}): ${text}`);
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
const { url } = await res.json();
|
|
215
|
+
if (!url) throw new Error("No billing portal URL returned");
|
|
216
|
+
if (mounted.current) setPortalUrl(url);
|
|
191
217
|
}
|
|
192
|
-
const { url } = await res.json();
|
|
193
|
-
if (!url) throw new Error("No billing portal URL returned");
|
|
194
|
-
if (mounted.current) setPortalUrl(url);
|
|
195
218
|
} catch (perr) {
|
|
196
219
|
if (mounted.current) setError(String(perr));
|
|
197
220
|
} finally {
|
|
198
221
|
if (mounted.current) setLoading(false);
|
|
199
222
|
}
|
|
200
223
|
},
|
|
201
|
-
[context, appKey, state?.billing_base_url, state?.app_id]
|
|
224
|
+
[context, appKey, state?.billing_base_url, state?.app_id, state?.entitlement?.status]
|
|
202
225
|
);
|
|
203
226
|
|
|
204
|
-
return { portalUrl, loading, error };
|
|
227
|
+
return { portalUrl, loading, error, notReady };
|
|
205
228
|
}
|
|
206
229
|
|
|
207
230
|
function TrialSubscriptionBilling({ context, state, appKey }) {
|
|
208
|
-
const { portalUrl, loading, error } = usePortalSession({
|
|
231
|
+
const { portalUrl, loading, error, notReady } = usePortalSession({
|
|
232
|
+
context,
|
|
233
|
+
state,
|
|
234
|
+
appKey,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Install not provisioned with a Stripe customer yet — calm "finish setup"
|
|
238
|
+
// state, not a red error. (Old/stale installs, or before the install flow's
|
|
239
|
+
// provision_trial_install has run.)
|
|
240
|
+
if (notReady) {
|
|
241
|
+
return (
|
|
242
|
+
<Flex direction="column" gap="medium">
|
|
243
|
+
<Alert title="Billing isn't set up yet" variant="info">
|
|
244
|
+
<Text>
|
|
245
|
+
Your trial and billing are still being set up for this install. Once
|
|
246
|
+
setup completes, manage your plan and payment method here. If this
|
|
247
|
+
persists, reinstall the app from the HubSpot marketplace to complete
|
|
248
|
+
sign-in.
|
|
249
|
+
</Text>
|
|
250
|
+
</Alert>
|
|
251
|
+
</Flex>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
209
254
|
|
|
210
255
|
return (
|
|
211
256
|
<Flex direction="column" gap="medium">
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Flex, Text, Link, Divider } from "@hubspot/ui-extensions";
|
|
3
|
+
|
|
4
|
+
// Build the in-app app-pages deep link (the app's Home/tabs live under /app).
|
|
5
|
+
function appPagesUrl(portalId, appId, pagePath = "") {
|
|
6
|
+
const suffix = pagePath ? `/${pagePath.replace(/^\//, "")}` : "";
|
|
7
|
+
return `https://app.hubspot.com/app/${portalId}/${appId}${suffix}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// HelpfulLinks — a standard "Helpful Links" section for every Growth Operations
|
|
11
|
+
// app. Renders a labeled list of links: optionally an "Open <app>" deep link
|
|
12
|
+
// back to the app pages (so the settings extension isn't a dead end), docs +
|
|
13
|
+
// support from the app config, and any app-specific extras.
|
|
14
|
+
//
|
|
15
|
+
// Props:
|
|
16
|
+
// portalId — context.portal.id (number/string).
|
|
17
|
+
// appId — this app's HubSpot app id (per-env). Needed for the app-pages
|
|
18
|
+
// deep link; omit to hide that link.
|
|
19
|
+
// appName — display name, e.g. "Sparkfly" (used in the deep-link label).
|
|
20
|
+
// appPages — when true (default) and appId is set, show "Open <appName> →"
|
|
21
|
+
// linking to the app pages. Set the page path via appPagesPath.
|
|
22
|
+
// appPagesPath — optional pages route to deep-link into (default: Home).
|
|
23
|
+
// docsUrl, supportUrl — external help links (omit to hide).
|
|
24
|
+
// links — app-specific extras: [{ label, url }]. Rendered after the
|
|
25
|
+
// standard links.
|
|
26
|
+
// title — section heading (default "Helpful Links").
|
|
27
|
+
export function HelpfulLinks({
|
|
28
|
+
portalId,
|
|
29
|
+
appId,
|
|
30
|
+
appName = "the app",
|
|
31
|
+
appPages = true,
|
|
32
|
+
appPagesPath = "",
|
|
33
|
+
docsUrl,
|
|
34
|
+
supportUrl,
|
|
35
|
+
links = [],
|
|
36
|
+
title = "Helpful Links",
|
|
37
|
+
}) {
|
|
38
|
+
const items = [];
|
|
39
|
+
|
|
40
|
+
if (appPages && appId && portalId != null) {
|
|
41
|
+
items.push({
|
|
42
|
+
label: `Open ${appName} →`,
|
|
43
|
+
url: appPagesUrl(portalId, appId, appPagesPath),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (docsUrl) items.push({ label: "Documentation", url: docsUrl });
|
|
47
|
+
if (supportUrl) items.push({ label: "Support", url: supportUrl });
|
|
48
|
+
for (const l of links) {
|
|
49
|
+
if (l && l.url && l.label) items.push({ label: l.label, url: l.url });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (items.length === 0) return null;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Flex direction="column" gap="small">
|
|
56
|
+
<Text format={{ fontWeight: "bold" }}>{title}</Text>
|
|
57
|
+
<Divider />
|
|
58
|
+
{items.map((item) => (
|
|
59
|
+
<Link key={item.url} href={{ url: item.url, external: true }}>
|
|
60
|
+
{item.label}
|
|
61
|
+
</Link>
|
|
62
|
+
))}
|
|
63
|
+
</Flex>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default HelpfulLinks;
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export { OnboardingChecklist } from "./home/OnboardingChecklist";
|
|
|
12
12
|
export { SyncPulse } from "./home/SyncPulse";
|
|
13
13
|
export { FeatureNudges } from "./home/FeatureNudges";
|
|
14
14
|
export { CreditMeter } from "./home/CreditMeter";
|
|
15
|
+
export { HelpfulLinks } from "./home/HelpfulLinks";
|
|
15
16
|
|
|
16
17
|
// Billing.
|
|
17
18
|
export { BillingTab } from "./billing/BillingTab";
|