@growth-operations/ui-shared 1.15.0 → 1.16.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/SubscriptionGate.jsx +96 -0
- package/src/index.js +1 -0
package/package.json
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Alert,
|
|
4
|
+
Flex,
|
|
5
|
+
Link,
|
|
6
|
+
LoadingSpinner,
|
|
7
|
+
Text,
|
|
8
|
+
} from "@hubspot/ui-extensions";
|
|
9
|
+
|
|
10
|
+
import { useStrictModeEffect } from "../lib/useStrictModeEffect";
|
|
11
|
+
import { callAppApi } from "../sdk/app/base";
|
|
12
|
+
|
|
13
|
+
// Full-surface "no active subscription" gate for CRM cards (and any other
|
|
14
|
+
// extension surface that should hard-block a canceled install).
|
|
15
|
+
//
|
|
16
|
+
// Wrap a card's whole tree at the hubspot.extend level:
|
|
17
|
+
//
|
|
18
|
+
// hubspot.extend(({ context, actions }) => (
|
|
19
|
+
// <SubscriptionGate context={context} appName="Applicant Tracking System">
|
|
20
|
+
// <MyCard ... />
|
|
21
|
+
// </SubscriptionGate>
|
|
22
|
+
// ));
|
|
23
|
+
//
|
|
24
|
+
// On mount it calls the app's `GET {checkPath}?portalId=…` (default
|
|
25
|
+
// /v1/entitlement — see CONTRACT.md "Subscription-canceled gate"), which
|
|
26
|
+
// returns { entitled, status, billing_url }. Canceled/uninstalled installs get
|
|
27
|
+
// a blocking Alert with a Go-to-Billing link; everyone else renders children.
|
|
28
|
+
//
|
|
29
|
+
// Deliberate behaviors (match the backend gate in common.entitlements.fastapi):
|
|
30
|
+
// - FAILS OPEN on fetch error: a transient backend/network failure must not
|
|
31
|
+
// blank the card for paying customers. Enforcement still happens per-request
|
|
32
|
+
// — the backend returns the same 402 contract from every gated endpoint.
|
|
33
|
+
// - Only canceled/uninstalled is blocked. past_due / pending_purchase / paused
|
|
34
|
+
// keep their softer banner UX (billingBannerFor / BillingBanner) — dunning
|
|
35
|
+
// and trial expiry must not hard-disable the product.
|
|
36
|
+
// - Mid-session cancellation (after a passing mount check) surfaces through
|
|
37
|
+
// the card's existing error handling, since the gated endpoints 402 with the
|
|
38
|
+
// same { detail, code: "subscription_canceled", billing_url } body.
|
|
39
|
+
//
|
|
40
|
+
// Props:
|
|
41
|
+
// context — the hubspot.extend context (portal.id, app.id, variables).
|
|
42
|
+
// checkPath — backend entitlement-check path; default "/v1/entitlement".
|
|
43
|
+
// appName — display name for the copy, e.g. "Applicant Tracking System".
|
|
44
|
+
export function SubscriptionGate({
|
|
45
|
+
context,
|
|
46
|
+
children,
|
|
47
|
+
checkPath = "/v1/entitlement",
|
|
48
|
+
appName = "this app",
|
|
49
|
+
}) {
|
|
50
|
+
// null = still loading; { entitled, status, billing_url } once resolved.
|
|
51
|
+
const [check, setCheck] = useState(null);
|
|
52
|
+
const [failedOpen, setFailedOpen] = useState(false);
|
|
53
|
+
|
|
54
|
+
useStrictModeEffect(
|
|
55
|
+
async ({ mounted }) => {
|
|
56
|
+
try {
|
|
57
|
+
const result = await callAppApi(
|
|
58
|
+
context,
|
|
59
|
+
`${checkPath}?portalId=${context.portal.id}`
|
|
60
|
+
);
|
|
61
|
+
if (mounted.current) setCheck(result);
|
|
62
|
+
} catch (_e) {
|
|
63
|
+
if (mounted.current) setFailedOpen(true);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
[context, checkPath]
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (failedOpen) return children;
|
|
70
|
+
if (!check) {
|
|
71
|
+
return <LoadingSpinner label="Checking subscription…" showLabel />;
|
|
72
|
+
}
|
|
73
|
+
if (check.entitled) return children;
|
|
74
|
+
|
|
75
|
+
const billingUrl =
|
|
76
|
+
check.billing_url ||
|
|
77
|
+
(context?.portal?.id && context?.app?.id
|
|
78
|
+
? `https://app.hubspot.com/app/${context.portal.id}/${context.app.id}/billing`
|
|
79
|
+
: null);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Alert title="No active subscription" variant="error">
|
|
83
|
+
<Flex direction="column" gap="extra-small">
|
|
84
|
+
<Text>
|
|
85
|
+
Your {appName} subscription was canceled. Your data is still here —
|
|
86
|
+
manage billing to pick up where you left off.
|
|
87
|
+
</Text>
|
|
88
|
+
{billingUrl && (
|
|
89
|
+
<Link href={{ url: billingUrl, external: true }}>Go to Billing</Link>
|
|
90
|
+
)}
|
|
91
|
+
</Flex>
|
|
92
|
+
</Alert>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default SubscriptionGate;
|
package/src/index.js
CHANGED
|
@@ -18,6 +18,7 @@ export { HelpfulLinks } from "./home/HelpfulLinks";
|
|
|
18
18
|
export { BillingTab } from "./billing/BillingTab";
|
|
19
19
|
export { PlanGrid } from "./billing/PlanGrid";
|
|
20
20
|
export { BillingBanner } from "./billing/BillingBanner";
|
|
21
|
+
export { SubscriptionGate } from "./billing/SubscriptionGate";
|
|
21
22
|
|
|
22
23
|
// Alerts — the shared customer alerts table (level filter, sort, dismiss).
|
|
23
24
|
export { AlertsTab } from "./alerts/AlertsTab";
|