@growth-operations/ui-shared 1.3.1 → 1.5.0
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 +27 -1
- package/src/index.js +3 -0
- package/src/sandbox/SandboxInstall.jsx +89 -0
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Text,
|
|
7
7
|
Tile,
|
|
8
8
|
LoadingButton,
|
|
9
|
+
Button,
|
|
9
10
|
Alert,
|
|
10
11
|
} from "@hubspot/ui-extensions";
|
|
11
12
|
import { useStrictModeEffect } from "../lib/useStrictModeEffect";
|
|
@@ -339,13 +340,38 @@ function CreditsBilling({ context, state, appKey }) {
|
|
|
339
340
|
);
|
|
340
341
|
}
|
|
341
342
|
|
|
343
|
+
// Legacy (Anvil) installs are billed through their existing subscription, NOT
|
|
344
|
+
// the credit model. Render a calm informational panel — never the credit meter
|
|
345
|
+
// or plan picker, which would falsely tell a paying Anvil customer they're out
|
|
346
|
+
// of credits and push them to buy a credit plan.
|
|
347
|
+
function LegacyBilling({ state }) {
|
|
348
|
+
const manageUrl = state?.legacy_billing_url;
|
|
349
|
+
return (
|
|
350
|
+
<Flex direction="column" gap="medium">
|
|
351
|
+
<Alert title="Managed through your existing subscription" variant="info">
|
|
352
|
+
<Text>
|
|
353
|
+
Your plan and payment method are billed through your existing
|
|
354
|
+
subscription. There's nothing to set up here.
|
|
355
|
+
</Text>
|
|
356
|
+
</Alert>
|
|
357
|
+
{manageUrl && (
|
|
358
|
+
<Button href={{ url: manageUrl, external: true }} variant="secondary">
|
|
359
|
+
Manage subscription
|
|
360
|
+
</Button>
|
|
361
|
+
)}
|
|
362
|
+
</Flex>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
342
366
|
export function BillingTab({ context, state, appKey }) {
|
|
343
367
|
const mode = state?.entitlement?.mode;
|
|
344
368
|
|
|
345
369
|
return (
|
|
346
370
|
<Flex direction="column" gap="medium">
|
|
347
371
|
<Heading>Billing</Heading>
|
|
348
|
-
{mode === "
|
|
372
|
+
{mode === "legacy" ? (
|
|
373
|
+
<LegacyBilling state={state} />
|
|
374
|
+
) : mode === "credits" ? (
|
|
349
375
|
<CreditsBilling context={context} state={state} appKey={appKey} />
|
|
350
376
|
) : (
|
|
351
377
|
<TrialSubscriptionBilling context={context} state={state} appKey={appKey} />
|
package/src/index.js
CHANGED
|
@@ -24,6 +24,9 @@ export { AlertsTab } from "./alerts/AlertsTab";
|
|
|
24
24
|
// Usage — the shared credit usage view (per-day LineChart + window stats).
|
|
25
25
|
export { UsageTab } from "./usage/UsageTab";
|
|
26
26
|
|
|
27
|
+
// Sandbox — the shared "Install on a sandbox" action (mints a signed link).
|
|
28
|
+
export { SandboxInstall } from "./sandbox/SandboxInstall";
|
|
29
|
+
|
|
27
30
|
// Lib helpers.
|
|
28
31
|
export {
|
|
29
32
|
fmtDate,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// SandboxInstall — the shared "Install on a sandbox" action.
|
|
2
|
+
//
|
|
3
|
+
// The one sanctioned path to a sandbox install (a customer can't self-install on
|
|
4
|
+
// a sandbox from the marketplace — the backend rejects that). From a prod
|
|
5
|
+
// portal, this asks the app's mint endpoint for a signed sandbox install link
|
|
6
|
+
// (which carries sandbox=true + the prod customer to link), then surfaces it for
|
|
7
|
+
// the user to open on their sandbox account.
|
|
8
|
+
//
|
|
9
|
+
// Extensible per app: the caller passes the mint `path` on its OWN backend
|
|
10
|
+
// (base-hosted apps -> /v1/hubspot/app_pages/... no; the install mint lives at
|
|
11
|
+
// /v2/hubspot/install/{app}/sandbox-link on base; self-hosted apps serve their
|
|
12
|
+
// own), plus the portalId. callAppApi targets context.variables.BASE_URL, so
|
|
13
|
+
// each app's BASE_URL routes to the right service. Apps not allowlisted get a
|
|
14
|
+
// 403 from the endpoint, surfaced as a calm message.
|
|
15
|
+
import React, { useState } from "react";
|
|
16
|
+
import { Flex, Heading, Text, Button, Link, Alert } from "@hubspot/ui-extensions";
|
|
17
|
+
|
|
18
|
+
import { callAppApi, AppApiError } from "../sdk/app/base";
|
|
19
|
+
|
|
20
|
+
// context: extension context. path: the mint endpoint on the app's backend
|
|
21
|
+
// (e.g. `/v2/hubspot/install/${appKey}/sandbox-link`). portalId: the prod portal
|
|
22
|
+
// minting the link. appName: copy. heading/description: optional overrides.
|
|
23
|
+
export function SandboxInstall({
|
|
24
|
+
context,
|
|
25
|
+
path,
|
|
26
|
+
portalId,
|
|
27
|
+
appName = "this app",
|
|
28
|
+
heading = "Install on a sandbox",
|
|
29
|
+
description,
|
|
30
|
+
}) {
|
|
31
|
+
const [installUrl, setInstallUrl] = useState(null);
|
|
32
|
+
const [loading, setLoading] = useState(false);
|
|
33
|
+
const [error, setError] = useState(null);
|
|
34
|
+
|
|
35
|
+
const mint = async () => {
|
|
36
|
+
setLoading(true);
|
|
37
|
+
setError(null);
|
|
38
|
+
try {
|
|
39
|
+
const pid = portalId ?? context.portal.id;
|
|
40
|
+
const sep = path.includes("?") ? "&" : "?";
|
|
41
|
+
const res = await callAppApi(context, `${path}${sep}portalId=${pid}`, "POST");
|
|
42
|
+
setInstallUrl(res?.install_url ?? null);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
// The backend 403s apps that don't allow sandbox installs — show its
|
|
45
|
+
// message rather than a generic failure.
|
|
46
|
+
setError(err instanceof AppApiError ? err.message : String(err));
|
|
47
|
+
} finally {
|
|
48
|
+
setLoading(false);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Flex direction="column" gap="small">
|
|
54
|
+
<Heading>{heading}</Heading>
|
|
55
|
+
<Text>
|
|
56
|
+
{description ??
|
|
57
|
+
`Spin up ${appName} on a HubSpot sandbox account, linked to your billing. ` +
|
|
58
|
+
`Generate a link, then open it while signed in to your sandbox.`}
|
|
59
|
+
</Text>
|
|
60
|
+
|
|
61
|
+
{error && (
|
|
62
|
+
<Alert title="Couldn't create a sandbox link" variant="warning">
|
|
63
|
+
<Text>{error}</Text>
|
|
64
|
+
</Alert>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
{installUrl ? (
|
|
68
|
+
<Flex direction="column" gap="extra-small">
|
|
69
|
+
<Link href={{ url: installUrl, external: true }}>
|
|
70
|
+
Open the sandbox install →
|
|
71
|
+
</Link>
|
|
72
|
+
<Text variant="microcopy">
|
|
73
|
+
Open this while signed in to the sandbox account you want to install on.
|
|
74
|
+
</Text>
|
|
75
|
+
</Flex>
|
|
76
|
+
) : (
|
|
77
|
+
<Button
|
|
78
|
+
variant="secondary"
|
|
79
|
+
disabled={loading}
|
|
80
|
+
onClick={mint}
|
|
81
|
+
>
|
|
82
|
+
{loading ? "Generating…" : "Generate sandbox install link"}
|
|
83
|
+
</Button>
|
|
84
|
+
)}
|
|
85
|
+
</Flex>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default SandboxInstall;
|