@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-operations/ui-shared",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "Shared HubSpot UI-extension modules (Home, Billing) for Growth Operations apps.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -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";