@growth-operations/ui-shared 1.17.2 → 1.19.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.17.2",
3
+ "version": "1.19.0",
4
4
  "description": "Shared HubSpot UI-extension modules (Home, Billing) for Growth Operations apps.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -8,6 +8,7 @@ import {
8
8
  Button,
9
9
  Alert,
10
10
  Input,
11
+ Link,
11
12
  LoadingSpinner,
12
13
  Table,
13
14
  TableHead,
@@ -22,7 +23,7 @@ import { useStrictModeEffect } from "../lib/useStrictModeEffect";
22
23
  import { CreditMeter } from "../home/CreditMeter";
23
24
  import { PlanGrid } from "./PlanGrid";
24
25
  import { refreshBillingActionTokens } from "../sdk/billing";
25
- import { getTopUps, setBillingLimit } from "../sdk/app/topUps";
26
+ import { getInvoices, getTopUps, setBillingLimit } from "../sdk/app/topUps";
26
27
 
27
28
  // Billing action tokens (see common.billing.action_token) are signed with a
28
29
  // 5-minute TTL — short enough that a customer who opens the tab and comes
@@ -477,6 +478,124 @@ function TopUpHistory({ context, appKey }) {
477
478
  );
478
479
  }
479
480
 
481
+ // Invoice status -> label (Stripe's vocabulary: paid / open / void /
482
+ // uncollectible; drafts and $0 voids are filtered server-side).
483
+ const INVOICE_STATUS_LABELS = {
484
+ paid: "Paid",
485
+ open: "Open",
486
+ void: "Void",
487
+ uncollectible: "Uncollectible",
488
+ };
489
+
490
+ // The invoice-history table itself — pure rendering (exported for tests);
491
+ // fetch/loading/error live in InvoiceHistory below.
492
+ export function InvoiceHistoryTable({ invoices }) {
493
+ return (
494
+ <Table>
495
+ <TableHead>
496
+ <TableRow>
497
+ <TableHeader>Date</TableHeader>
498
+ <TableHeader>Description</TableHeader>
499
+ <TableHeader>Amount</TableHeader>
500
+ <TableHeader>Status</TableHeader>
501
+ <TableHeader>Receipt</TableHeader>
502
+ </TableRow>
503
+ </TableHead>
504
+ <TableBody>
505
+ {invoices.map((invoice, i) => (
506
+ <TableRow key={`${invoice.id ?? invoice.date}-${i}`}>
507
+ <TableCell>{fmtDate(invoice.date)}</TableCell>
508
+ <TableCell>{invoice.description}</TableCell>
509
+ <TableCell>{fmtMoney(invoice.amount_cents, invoice.currency)}</TableCell>
510
+ <TableCell>
511
+ {INVOICE_STATUS_LABELS[invoice.status] ?? invoice.status ?? "—"}
512
+ </TableCell>
513
+ <TableCell>
514
+ {invoice.hosted_invoice_url ? (
515
+ <Link href={{ url: invoice.hosted_invoice_url, external: true }}>
516
+ Receipt
517
+ </Link>
518
+ ) : (
519
+ "—"
520
+ )}
521
+ </TableCell>
522
+ </TableRow>
523
+ ))}
524
+ </TableBody>
525
+ </Table>
526
+ );
527
+ }
528
+
529
+ // "Invoice history" — every Stripe invoice on the portal's customer
530
+ // (subscription renewals, one-off charges, top-up bands — including VOIDED
531
+ // top-ups, which tell the decline story), from the billing service's
532
+ // GET /v1/billing/invoices (newest first, capped at 24). Loading, error, and
533
+ // empty states mirror TopUpHistory.
534
+ //
535
+ // Auth is the billing action token (the claim scopes the response to THIS
536
+ // portal's Stripe customer). BillingTab re-mints the token set every 3
537
+ // minutes; depending on the token means the list also quietly refreshes on
538
+ // that cadence while the tab stays open — a top-up that completes mid-session
539
+ // shows up without a reload.
540
+ //
541
+ // Two "render nothing" degradations: no billing_base_url / token yet (an
542
+ // older backend that doesn't mint them — same "hidden until token exists"
543
+ // posture as the restart link), and a 404 from a billing service that
544
+ // predates the invoices route (hide the section instead of erroring forever
545
+ // until the service's next deploy).
546
+ function InvoiceHistory({ billingBaseUrl, billingActionToken }) {
547
+ const [status, setStatus] = useState("loading"); // loading | ready | error | unsupported
548
+ const [invoices, setInvoices] = useState([]);
549
+
550
+ useStrictModeEffect(
551
+ async ({ mounted }) => {
552
+ try {
553
+ const res = await getInvoices({
554
+ billingBaseUrl,
555
+ token: billingActionToken,
556
+ });
557
+ if (mounted.current) {
558
+ setInvoices(res?.invoices ?? []);
559
+ setStatus("ready");
560
+ }
561
+ } catch (e) {
562
+ if (mounted.current) {
563
+ setStatus(e?.statusCode === 404 ? "unsupported" : "error");
564
+ }
565
+ }
566
+ },
567
+ [billingBaseUrl, billingActionToken]
568
+ );
569
+
570
+ if (!billingBaseUrl || !billingActionToken || status === "unsupported") {
571
+ return null;
572
+ }
573
+ if (status === "loading") {
574
+ return <LoadingSpinner showLabel label="Loading invoices…" />;
575
+ }
576
+ if (status === "error") {
577
+ return (
578
+ <Alert title="Couldn't load invoice history" variant="warning">
579
+ <Text>Refresh the page to try again.</Text>
580
+ </Alert>
581
+ );
582
+ }
583
+
584
+ return (
585
+ <Flex direction="column" gap="small">
586
+ <Heading>Invoice history</Heading>
587
+ {invoices.length === 0 ? (
588
+ <Text format={{ fontStyle: "italic" }}>
589
+ No invoices yet. Subscription renewals, one-off charges, and credit
590
+ top-ups are listed here.
591
+ </Text>
592
+ ) : (
593
+ <InvoiceHistoryTable invoices={invoices} />
594
+ )}
595
+ </Flex>
596
+ );
597
+ }
598
+
480
599
  // "Billing limit" — the per-period spend cap on automatic top-ups. Shows the
481
600
  // current limit (when the entitlement arm carries billing_limit_cents), sets
482
601
  // a new one (USD input -> cents), or clears it, via the base service's
@@ -699,6 +818,14 @@ export function CreditsBilling({ context, state, appKey, openIframe = null }) {
699
818
  {state?.entitlement?.billing_model === "topup" && (
700
819
  <>
701
820
  <TopUpHistory context={context} appKey={appKey} />
821
+ {/* Directly below Top-up history (Andrew's ask). `state` here is
822
+ BillingTab's interval-refreshed copy, so the token is re-minted
823
+ every 3 minutes; InvoiceHistory hides itself when the backend
824
+ or billing service predates the pieces it needs. */}
825
+ <InvoiceHistory
826
+ billingBaseUrl={base}
827
+ billingActionToken={state?.billing_action_tokens?.portal ?? null}
828
+ />
702
829
  <BillingLimitControl
703
830
  context={context}
704
831
  appKey={appKey}
@@ -9,16 +9,30 @@ import { resolveCreditMeter } from "../lib/creditDisplay";
9
9
  // `credit_meter` block (CONTRACT.md). Renders nothing when there is no credit
10
10
  // data to show.
11
11
  //
12
- // All branch math (number precedence, threshold_state → tag/alert, the
13
- // pool+bank split) lives in ../lib/creditDisplay so it's unit-testable — this
14
- // component is pure rendering.
12
+ // All branch math (number precedence, threshold_state → tag/alert/bar
13
+ // variants, the pool+bank split) lives in ../lib/creditDisplay so it's
14
+ // unit-testable — this component is pure rendering.
15
15
  //
16
- // Two-segment bank display: accounts with a top-up bank (topup billing model)
17
- // get the combined marker bar — `${used} of ${granted}` over the combined
18
- // pool+bank denominator, so after a period reset with bank left it reads e.g.
19
- // "0 of 1,700" while the bank segment shows what's left — plus the
20
- // pool/bank breakdown lines. Accounts with NO bank (top_up_bank_remaining 0
21
- // AND never a top-up) render exactly the legacy single-segment bar.
16
+ // Stacked-bars bank display: an account with top-up bank credits left (topup
17
+ // billing model) gets TWO bars, one per bucket, and NO combined headline (the
18
+ // combined pool+bank denominator read as one big confusing allowance):
19
+ // 1. Monthly plan credits — a DEPLETION meter (value = pool used of the
20
+ // period grant), colored by the POOL'S OWN fill level: success under
21
+ // 80% used, warning at >=80%, danger at >=95% (view.poolBarVariant —
22
+ // mirrors the ~80/95 low-credit alert ladder).
23
+ // 2. Additional credits — a LEVEL gauge (value = bank remaining of the
24
+ // bank's granted total: the bank is cumulative and never expires, so a
25
+ // used-of-granted bar would drift to permanently-near-full over multiple
26
+ // purchases; the remaining level refills on purchase and drains on
27
+ // spend), colored by ITS OWN level: success above 50% full, warning at
28
+ // 20–50%, danger at <=20% (view.bankBarVariant).
29
+ // Each bar colors by its own fill, NOT the account's threshold state — the
30
+ // StatusTag keeps the account-state colors. Hosts whose backend predates
31
+ // top_up_bank_granted degrade gracefully: the bank renders as a text-only
32
+ // line (no bar, never "of 0"). An account with NO bank credits left (never
33
+ // topped up, or the bank is spent) renders exactly the legacy view — the
34
+ // "{remaining} of {granted} credits left" headline plus the single
35
+ // fill-colored bar.
22
36
  //
23
37
  // Props:
24
38
  // entitlement — the /v1/home entitlement union (uses the credits arm).
@@ -27,7 +41,7 @@ export function CreditMeter({ entitlement, creditMeter }) {
27
41
  const view = resolveCreditMeter(entitlement, creditMeter);
28
42
  if (!view) return null;
29
43
 
30
- const { granted, used, remaining, bank, tagVariant, tagLabel, alert } = view;
44
+ const { granted, used, remaining, bank, tagVariant, tagLabel, poolBarVariant, bankBarVariant, alert } = view;
31
45
  const grantDaysLeft = view.grantExpiresAt ? daysUntil(view.grantExpiresAt) : null;
32
46
 
33
47
  return (
@@ -38,42 +52,64 @@ export function CreditMeter({ entitlement, creditMeter }) {
38
52
  <StatusTag variant={tagVariant}>{tagLabel}</StatusTag>
39
53
  </Flex>
40
54
 
41
- {/* PRIMARY signal — large and obvious. */}
42
- <Text format={{ fontWeight: "bold", fontSize: "lg" }}>
43
- {remaining} of {granted} credits left
44
- </Text>
45
-
46
55
  {bank ? (
47
56
  <>
48
- {/* Current-position marker over the COMBINED pool+bank
49
- denominator (used = granted - remaining). The HubSpot
50
- ProgressBar is single-value, so the two segments are the
51
- labeled breakdown below; this bar is the position within the
52
- whole. */}
53
- <ProgressBar
54
- title={`${used} of ${granted} used`}
55
- value={used}
56
- maxValue={granted > 0 ? granted : 1}
57
- showPercentage={true}
58
- />
57
+ {/* Pool — depletion meter over the period grant. */}
59
58
  <Flex direction="column" gap="extra-small">
60
59
  <Text>
61
60
  Monthly plan credits: {bank.poolRemaining} of {bank.poolGrant}{" "}
62
61
  left
63
62
  </Text>
64
- <Text>Additional credits: {bank.bankRemaining} left</Text>
65
- <Text format={{ fontStyle: "italic" }}>
66
- Additional credits don't expire.
67
- </Text>
63
+ <ProgressBar
64
+ title={`${bank.poolUsed} of ${bank.poolGrant} used`}
65
+ value={bank.poolUsed}
66
+ maxValue={bank.poolGrant > 0 ? bank.poolGrant : 1}
67
+ showPercentage={true}
68
+ variant={poolBarVariant}
69
+ />
68
70
  </Flex>
71
+ {bank.bankGranted > 0 ? (
72
+ // Bank — level gauge over the granted total (refills on
73
+ // purchase, drains on spend; never expires).
74
+ <Flex direction="column" gap="extra-small">
75
+ <Text>
76
+ Additional credits (never expire): {bank.bankRemaining} of{" "}
77
+ {bank.bankGranted} left
78
+ </Text>
79
+ <ProgressBar
80
+ title={`${bank.bankRemaining} of ${bank.bankGranted} left`}
81
+ value={bank.bankRemaining}
82
+ maxValue={bank.bankGranted > 0 ? bank.bankGranted : 1}
83
+ showPercentage={true}
84
+ variant={bankBarVariant}
85
+ />
86
+ </Flex>
87
+ ) : (
88
+ // Graceful degradation: the host's backend predates
89
+ // top_up_bank_granted, so the bank total is unknown — text
90
+ // only, no bar (never divide by zero, never "of 0").
91
+ <Flex direction="column" gap="extra-small">
92
+ <Text>Additional credits: {bank.bankRemaining} left</Text>
93
+ <Text format={{ fontStyle: "italic" }}>
94
+ Additional credits don't expire.
95
+ </Text>
96
+ </Flex>
97
+ )}
69
98
  </>
70
99
  ) : (
71
- <ProgressBar
72
- title={`${used} used`}
73
- value={used}
74
- maxValue={granted > 0 ? granted : 1}
75
- showPercentage={true}
76
- />
100
+ <>
101
+ {/* PRIMARY signal — large and obvious. */}
102
+ <Text format={{ fontWeight: "bold", fontSize: "lg" }}>
103
+ {remaining} of {granted} credits left
104
+ </Text>
105
+ <ProgressBar
106
+ title={`${used} used`}
107
+ value={used}
108
+ maxValue={granted > 0 ? granted : 1}
109
+ showPercentage={true}
110
+ variant={poolBarVariant}
111
+ />
112
+ </>
77
113
  )}
78
114
 
79
115
  {/* Free-grant countdown: "100 free credits — N days left". */}
package/src/index.js CHANGED
@@ -72,6 +72,7 @@ export {
72
72
  getAlerts,
73
73
  dismissAlert,
74
74
  getTopUps,
75
+ getInvoices,
75
76
  setBillingLimit,
76
77
  getSettings,
77
78
  updateSettings,
@@ -26,9 +26,10 @@
26
26
  // incident where a paying, overage account was shown a blocking
27
27
  // "Depleted"/"out of credits" panel).
28
28
 
29
- // True when the account has (or ever had) a top-up bank: the two-segment
30
- // meter renders only then — a zero bank with NO top-up ever (last_top_up_at
31
- // null) renders exactly the legacy single-segment bar.
29
+ // True when the account has (or ever had) a top-up bank. Note the CreditMeter
30
+ // bank section renders only while the bank HAS credits left (bankRemaining >
31
+ // 0 — see resolveCreditMeter); this predicate stays the looser "ever had one"
32
+ // for consumers that render history/summary surfaces.
32
33
  export function hasTopUpBank(ent) {
33
34
  if (!ent) return false;
34
35
  return (ent.top_up_bank_remaining ?? 0) > 0 || ent.last_top_up_at != null;
@@ -38,14 +39,53 @@ export function hasTopUpBank(ent) {
38
39
  // Returns null when there is no credit data to show (component renders
39
40
  // nothing). Otherwise:
40
41
  // granted/used/remaining — combined (topup) or raw (legacy) display numbers
41
- // bank — { bankRemaining, poolGrant, poolRemaining, poolUsed }
42
- // when the account has a bank, else null
42
+ // bank — { bankRemaining, bankGranted, poolGrant,
43
+ // poolRemaining, poolUsed } when the account has bank
44
+ // credits LEFT (bankRemaining > 0), else null — a
45
+ // zero bank renders the plain single-pool bar (with
46
+ // bankRemaining 0 the combined numbers ARE the pool
47
+ // numbers). bankGranted is the bank's lifetime
48
+ // granted total (the bank bar's denominator); 0 on
49
+ // backends that predate top_up_bank_granted — the
50
+ // component degrades to a text-only bank line.
43
51
  // thresholdState — the arm's threshold_state, or null on a backend
44
52
  // that predates it
45
53
  // depleted/overIncluded/low — mutually-exclusive urgency flags
46
- // tagVariant/tagLabel — StatusTag rendering
54
+ // tagVariant/tagLabel — StatusTag rendering (ACCOUNT state — unchanged)
55
+ // poolBarVariant — ProgressBar sentiment for the pool bar (and the
56
+ // no-bank legacy single bar), colored by the POOL'S
57
+ // OWN fill level, not the account state
58
+ // bankBarVariant — same for the bank level gauge (null when there is
59
+ // no bank section)
47
60
  // alert — { variant, title, message } or null
48
61
  // grantExpiresAt — pass-through for the free-grant countdown
62
+ //
63
+ // BAR COLORS ARE PER-BAR FILL LEVELS (Jasper 2026-10): a pool at 99% used
64
+ // with a full bank must NOT render green just because the account is healthy
65
+ // — each bar colors by its own level; the StatusTag keeps the account-state
66
+ // colors. Thresholds mirror the backend's ~80/95 low-credit alert ladder
67
+ // (common.entitlements LOW/DEPLETED banding). Comparisons are integer
68
+ // cross-multiplications — no float fuzz at the 80/95/50/20 boundaries.
69
+ //
70
+ // pool (DEPLETION meter, value = poolUsed of poolGrant):
71
+ // success when used < 80%, warning at >= 80%, danger at >= 95%.
72
+ // bank (LEVEL gauge, value = bankRemaining of bankGranted):
73
+ // success above 50% full, warning at 20–50%, danger at <= 20%
74
+ // ("running out") — a draining bank is warning-worthy even though it
75
+ // never expires.
76
+ function poolBarVariantFor(poolUsed, poolGrant) {
77
+ if (!(poolGrant > 0)) return "success"; // no grant: nothing to deplete
78
+ if (poolUsed * 100 >= 95 * poolGrant) return "danger";
79
+ if (poolUsed * 100 >= 80 * poolGrant) return "warning";
80
+ return "success";
81
+ }
82
+
83
+ function bankBarVariantFor(bankRemaining, bankGranted) {
84
+ if (!(bankGranted > 0)) return "success"; // degraded text-only line renders no bar
85
+ if (bankRemaining * 100 <= 20 * bankGranted) return "danger";
86
+ if (bankRemaining * 100 <= 50 * bankGranted) return "warning";
87
+ return "success";
88
+ }
49
89
  export function resolveCreditMeter(entitlement, creditMeter) {
50
90
  const ent = entitlement?.mode === "credits" ? entitlement : null;
51
91
  const meter = creditMeter ?? null;
@@ -82,13 +122,23 @@ export function resolveCreditMeter(entitlement, creditMeter) {
82
122
  // Bank split. Drawdown is monthly-first, so of the combined `remaining` the
83
123
  // bank portion is bankRemaining and the rest is pool remaining; the pool
84
124
  // segment's full size is the combined denominator minus the bank.
125
+ //
126
+ // The bank section renders ONLY while the bank has credits left
127
+ // (bankRemaining > 0): a zero bank — never topped up, or fully burned —
128
+ // renders the plain single-pool bar, and with bankRemaining 0 the combined
129
+ // numbers ARE the pool numbers (granted == poolGrant, remaining ==
130
+ // poolRemaining), so nothing is lost.
85
131
  let bank = null;
86
- if (hasTopUpBank(ent)) {
87
- const bankRemaining = Math.max(0, ent?.top_up_bank_remaining ?? 0);
132
+ const bankRemaining = Math.max(0, ent?.top_up_bank_remaining ?? 0);
133
+ if (bankRemaining > 0) {
88
134
  const poolGrant = Math.max(granted - bankRemaining, 0);
89
135
  const poolRemaining = Math.min(Math.max(remaining - bankRemaining, 0), poolGrant);
90
136
  bank = {
91
137
  bankRemaining,
138
+ // The bank's lifetime granted total — the bank bar's denominator. 0 on
139
+ // backends that predate top_up_bank_granted (the component degrades to
140
+ // a text-only bank line; never divide by zero, never "of 0").
141
+ bankGranted: Math.max(0, ent?.top_up_bank_granted ?? 0),
92
142
  poolGrant,
93
143
  poolRemaining,
94
144
  poolUsed: poolGrant - poolRemaining,
@@ -120,6 +170,17 @@ export function resolveCreditMeter(entitlement, creditMeter) {
120
170
  tagLabel = "Healthy";
121
171
  }
122
172
 
173
+ // ProgressBar sentiments — each bar colored by ITS OWN fill level (see the
174
+ // header block). The pool bar (and the no-bank legacy single bar) reads the
175
+ // pool's depletion ratio; the bank bar reads the bank's remaining level.
176
+ const poolBarVariant = poolBarVariantFor(
177
+ bank ? bank.poolUsed : used,
178
+ bank ? bank.poolGrant : granted
179
+ );
180
+ const bankBarVariant = bank
181
+ ? bankBarVariantFor(bank.bankRemaining, bank.bankGranted)
182
+ : null;
183
+
123
184
  // The inline meter alert mirrors the banner's messaging at the point of
124
185
  // glance. Legacy branches keep their pre-top-up copy byte-for-byte.
125
186
  let alert = null;
@@ -171,6 +232,8 @@ export function resolveCreditMeter(entitlement, creditMeter) {
171
232
  low,
172
233
  tagVariant,
173
234
  tagLabel,
235
+ poolBarVariant,
236
+ bankBarVariant,
174
237
  alert,
175
238
  grantExpiresAt: ent?.grant_expires_at ?? null,
176
239
  };
@@ -4,7 +4,13 @@
4
4
  // headers). Base-hosted credit apps reach base at context.variables.BASE_URL;
5
5
  // `portalId` rides as a query param — the same pattern as the base-hosted
6
6
  // alerts routes (see ./alerts).
7
- import { callAppApi } from "./base";
7
+ //
8
+ // EXCEPTION: getInvoices below hits the BILLING service
9
+ // (growth-ops-apps-app at state.billing_base_url), not base — it holds the
10
+ // Stripe keys and serves every app's billing. Token-auth bare hubspot.fetch,
11
+ // same as refreshBillingActionTokens (../billing).
12
+ import { hubspot } from "@hubspot/ui-extensions";
13
+ import { callAppApi, AppApiError } from "./base";
8
14
 
9
15
  function billingPath(appKey, suffix, portalId, { token } = {}) {
10
16
  const path = `/v1/hubspot/app_pages/${encodeURIComponent(appKey)}/billing/${suffix}`;
@@ -22,6 +28,42 @@ export async function getTopUps(context, { appKey, portalId } = {}) {
22
28
  return callAppApi(context, billingPath(appKey, "top-ups", portalId), "GET");
23
29
  }
24
30
 
31
+ // GET {billingBaseUrl}/v1/billing/invoices?token=... ->
32
+ // { invoices: [{ id, date, description, amount_cents, currency, status,
33
+ // hosted_invoice_url, kind }] } (newest first, capped at 24)
34
+ //
35
+ // The billing service (growth-ops-apps-app), NOT base — see the header note.
36
+ // Auth is the signed billing-action `token` query param
37
+ // (common.billing.action_token): the endpoint is read-only and accepts any
38
+ // action; BillingTab passes the (interval-refreshed)
39
+ // state.billing_action_tokens.portal. The claim carries app_key + portal_id,
40
+ // so the response is scoped to THIS portal's Stripe customer — never a
41
+ // stranger's invoices.
42
+ //
43
+ // Throws AppApiError on failure, with the FastAPI {"detail": ...} string as
44
+ // the message — a 404 means the billing service predates the invoices route
45
+ // (the caller hides the section rather than erroring forever).
46
+ export async function getInvoices({ billingBaseUrl, token } = {}) {
47
+ const url = `${billingBaseUrl}/v1/billing/invoices?token=${encodeURIComponent(token)}`;
48
+ const response = await hubspot.fetch(url, { timeout: 10000, method: "GET" });
49
+ if (!response.ok) {
50
+ const errorText = await response.text();
51
+ // FastAPI errors come back as {"detail": "..."} — surface just the detail
52
+ // string (same convention as callAppApi).
53
+ let message = errorText;
54
+ try {
55
+ const parsed = JSON.parse(errorText);
56
+ if (parsed && typeof parsed.detail === "string") {
57
+ message = parsed.detail;
58
+ }
59
+ } catch {
60
+ // body wasn't JSON; keep the raw text
61
+ }
62
+ throw new AppApiError(message, response.status, errorText);
63
+ }
64
+ return response.json();
65
+ }
66
+
25
67
  // POST /v1/hubspot/app_pages/{app}/billing/limit, body { limit_cents: int|null }
26
68
  // — set (or clear, with null) the per-period auto top-up spend cap. Body is a
27
69
  // plain object, never JSON.stringify'd (the hubspot.fetch rule callAppApi
package/src/sdk/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // App backend (context.variables.BASE_URL) via hubspot.fetch.
5
5
  export { callAppApi, AppApiError } from "./app/base";
6
6
  export { getAlerts, dismissAlert } from "./app/alerts";
7
- export { getTopUps, setBillingLimit } from "./app/topUps";
7
+ export { getTopUps, getInvoices, setBillingLimit } from "./app/topUps";
8
8
  export { getSettings, updateSettings, makeSettingsApi } from "./app/settings";
9
9
  export {
10
10
  strToBase64,