@growth-operations/ui-shared 1.35.0 → 1.37.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
|
@@ -640,6 +640,7 @@ export function CreditsBilling({ context, state, appKey, openIframe = null }) {
|
|
|
640
640
|
<CreditMeter
|
|
641
641
|
entitlement={state?.entitlement}
|
|
642
642
|
creditMeter={state?.credit_meter}
|
|
643
|
+
appName={state?.app?.name}
|
|
643
644
|
/>
|
|
644
645
|
<LoadingButton
|
|
645
646
|
href={
|
|
@@ -719,6 +720,7 @@ export function CreditsBilling({ context, state, appKey, openIframe = null }) {
|
|
|
719
720
|
<CreditMeter
|
|
720
721
|
entitlement={state?.entitlement}
|
|
721
722
|
creditMeter={state?.credit_meter}
|
|
723
|
+
appName={state?.app?.name}
|
|
722
724
|
/>
|
|
723
725
|
<PlanGrid context={context} state={state} appKey={appKey} openIframe={openIframe} />
|
|
724
726
|
{!hasPlans && (
|
package/src/home/CreditMeter.jsx
CHANGED
|
@@ -43,8 +43,10 @@ import { resolveCreditMeter } from "../lib/creditDisplay";
|
|
|
43
43
|
// credits — the /v1/home credits block (optional; the authoritative
|
|
44
44
|
// two-bucket monthly/additional numbers + renews_on when the
|
|
45
45
|
// backend carries it — see CONTRACT.md).
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
// appName — display name for the trial-ended alert copy (optional;
|
|
47
|
+
// hosts pass state.app.name — falls back to "this app").
|
|
48
|
+
export function CreditMeter({ entitlement, creditMeter, credits = null, appName = null }) {
|
|
49
|
+
const view = resolveCreditMeter(entitlement, creditMeter, credits, { appName });
|
|
48
50
|
if (!view) return null;
|
|
49
51
|
|
|
50
52
|
const { granted, used, remaining, bank, tagVariant, tagLabel, poolBarVariant, bankBarVariant, alert } = view;
|
package/src/lib/creditDisplay.js
CHANGED
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
// double-guards so a stale/mixed payload can't regress the 2026-09-02
|
|
26
26
|
// incident where a paying, overage account was shown a blocking
|
|
27
27
|
// "Depleted"/"out of credits" panel).
|
|
28
|
+
//
|
|
29
|
+
// TRIAL ENDED overrides all of the above for FREE accounts (Andrew
|
|
30
|
+
// 2026-11-11): the backend maps an expired grant clock onto depleted even
|
|
31
|
+
// with credits left, so a lapsed clock reads tag "Trial ended" + the
|
|
32
|
+
// trial-ended alert, never "Depleted"/"out of credits".
|
|
28
33
|
import { daysUntil, SUPPORT_FOOTER } from "./format";
|
|
29
34
|
|
|
30
35
|
// True when the account has (or ever had) a top-up bank. Note the CreditMeter
|
|
@@ -108,7 +113,7 @@ export function creditsBlockOf(state) {
|
|
|
108
113
|
return state.state?.credits ?? state.credits ?? null;
|
|
109
114
|
}
|
|
110
115
|
|
|
111
|
-
export function resolveCreditMeter(entitlement, creditMeter, credits = null) {
|
|
116
|
+
export function resolveCreditMeter(entitlement, creditMeter, credits = null, { appName } = {}) {
|
|
112
117
|
const ent = entitlement?.mode === "credits" ? entitlement : null;
|
|
113
118
|
const meter = creditMeter ?? null;
|
|
114
119
|
// The state.credits block stands on its own (it IS the credit data), so it
|
|
@@ -161,6 +166,16 @@ export function resolveCreditMeter(entitlement, creditMeter, credits = null) {
|
|
|
161
166
|
: !onPaidPlan && (ent?.depleted === true || remaining <= 0);
|
|
162
167
|
const low = !depleted && !overIncluded && remaining <= lowThreshold;
|
|
163
168
|
|
|
169
|
+
// Trial ended (free account, grant clock lapsed) outranks every credit
|
|
170
|
+
// reading on the tag/alert — the backend maps an expired grant onto
|
|
171
|
+
// depleted even with credits left, and the lapsed clock is the true story
|
|
172
|
+
// (Andrew 2026-11-11; the same override the top BillingBanner applies).
|
|
173
|
+
const trialDaysLeft =
|
|
174
|
+
!onPaidPlan && ent?.grant_expires_at != null
|
|
175
|
+
? daysUntil(ent.grant_expires_at)
|
|
176
|
+
: null;
|
|
177
|
+
const trialEnded = trialDaysLeft != null && trialDaysLeft <= 0;
|
|
178
|
+
|
|
164
179
|
// Bank split. Drawdown is monthly-first, so of the combined `remaining` the
|
|
165
180
|
// bank portion is bankRemaining and the rest is pool remaining; the pool
|
|
166
181
|
// segment's full size is the combined denominator minus the bank.
|
|
@@ -216,7 +231,10 @@ export function resolveCreditMeter(entitlement, creditMeter, credits = null) {
|
|
|
216
231
|
|
|
217
232
|
let tagVariant;
|
|
218
233
|
let tagLabel;
|
|
219
|
-
if (
|
|
234
|
+
if (trialEnded) {
|
|
235
|
+
tagVariant = "danger";
|
|
236
|
+
tagLabel = "Trial ended";
|
|
237
|
+
} else if (depleted) {
|
|
220
238
|
tagVariant = "danger";
|
|
221
239
|
tagLabel = "Depleted";
|
|
222
240
|
} else if (thresholdState === "top_up_failed") {
|
|
@@ -253,7 +271,13 @@ export function resolveCreditMeter(entitlement, creditMeter, credits = null) {
|
|
|
253
271
|
// The inline meter alert mirrors the banner's messaging at the point of
|
|
254
272
|
// glance. Legacy branches keep their pre-top-up copy byte-for-byte.
|
|
255
273
|
let alert = null;
|
|
256
|
-
if (
|
|
274
|
+
if (trialEnded) {
|
|
275
|
+
alert = {
|
|
276
|
+
variant: "error",
|
|
277
|
+
title: "Your trial has ended",
|
|
278
|
+
message: `Your free trial has ended. Select a plan to continue use of ${appName ?? "this app"}.`,
|
|
279
|
+
};
|
|
280
|
+
} else if (depleted) {
|
|
257
281
|
alert = {
|
|
258
282
|
variant: "error",
|
|
259
283
|
title: "You're out of credits",
|
|
@@ -321,12 +345,19 @@ export function resolveCreditMeter(entitlement, creditMeter, credits = null) {
|
|
|
321
345
|
// owner) is reused VERBATIM here — only the placement and the trial-ended
|
|
322
346
|
// variant (warning -> error: it stops the app) changed.
|
|
323
347
|
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
348
|
+
// Andrew 2026-11-11: the trial CLOCK outranks the credit count. Once the
|
|
349
|
+
// 30-day window lapses, "trial ended" is the ONLY banner — it overrides the
|
|
350
|
+
// credits reading even when the grant is also spent. This ordering matters
|
|
351
|
+
// because the backend maps an EXPIRED grant (credits left) onto depleted /
|
|
352
|
+
// threshold_state "depleted" (CreditState.to_entitlement), so the "explicit"
|
|
353
|
+
// out-of-credits signals often MEAN "trial over", not "credits gone" —
|
|
354
|
+
// portal 52109545 flew "You're out of credits" with 49/100 credits left.
|
|
355
|
+
//
|
|
356
|
+
// First match wins inside trialStateOf:
|
|
357
|
+
// trial ended — the grant's 30-day clock ran out (credits left or not).
|
|
358
|
+
// out of credits — the free grant is spent INSIDE the window (depleted /
|
|
359
|
+
// remaining <= 0 with days left, or no clock on record).
|
|
360
|
+
// trial active — the clock is still running: "[XX] trial days remaining".
|
|
330
361
|
// Returns { kind, title, body } or null (no grant clock on record).
|
|
331
362
|
function trialStateOf(ent, { appName } = {}) {
|
|
332
363
|
const name = appName ?? "this app";
|
|
@@ -336,13 +367,6 @@ function trialStateOf(ent, { appName } = {}) {
|
|
|
336
367
|
const daysLeft =
|
|
337
368
|
ent.grant_expires_at != null ? daysUntil(ent.grant_expires_at) : null;
|
|
338
369
|
|
|
339
|
-
if (ent.depleted === true || remaining <= 0) {
|
|
340
|
-
return {
|
|
341
|
-
kind: "out_of_credits",
|
|
342
|
-
title: "You're out of credits",
|
|
343
|
-
body: `You have used ${used}/${granted} free credits. Select one of the plans below to continue use of ${name}.`,
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
370
|
if (daysLeft != null && daysLeft <= 0) {
|
|
347
371
|
return {
|
|
348
372
|
kind: "trial_ended",
|
|
@@ -350,6 +374,18 @@ function trialStateOf(ent, { appName } = {}) {
|
|
|
350
374
|
body: `Your free trial has ended. Select a plan to continue use of ${name}.`,
|
|
351
375
|
};
|
|
352
376
|
}
|
|
377
|
+
if (ent.depleted === true || remaining <= 0) {
|
|
378
|
+
return {
|
|
379
|
+
kind: "out_of_credits",
|
|
380
|
+
// True when a live trial clock is on record (a lapsed clock returned
|
|
381
|
+
// trial_ended above). The banner's hard-depleted branch keys on this:
|
|
382
|
+
// live-clock accounts fly the slide's used-X/Y copy (Andrew 2026-11-11);
|
|
383
|
+
// clockless free accounts keep the generic pre-top-up line.
|
|
384
|
+
liveClock: daysLeft != null,
|
|
385
|
+
title: "You're out of credits",
|
|
386
|
+
body: `You have used ${used}/${granted} free credits. Select one of the plans below to continue use of ${name}.`,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
353
389
|
if (daysLeft != null) {
|
|
354
390
|
return {
|
|
355
391
|
kind: "trial_active",
|
|
@@ -395,13 +431,18 @@ function trialBannerView(trial) {
|
|
|
395
431
|
// 1. top_up_pending (info) — transient in-flight top-up
|
|
396
432
|
// 2. top_up_failed (error) — card on file failed to process
|
|
397
433
|
// 3. limit_reached (warning)
|
|
398
|
-
// 4.
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
// depleted
|
|
402
|
-
//
|
|
434
|
+
// 4. trial ended (error) — the grant clock ran out; the ONLY banner
|
|
435
|
+
// after day 30 (Andrew 2026-11-11), beating
|
|
436
|
+
// every credits reading — the backend maps
|
|
437
|
+
// expiry onto depleted / threshold_state
|
|
438
|
+
// "depleted", so an "explicit" depleted often
|
|
439
|
+
// MEANS trial over (portal 52109545)
|
|
440
|
+
// 5. depleted (error) — THE out-of-credits block. A free account
|
|
441
|
+
// with a live trial clock flies the slide's
|
|
442
|
+
// used-X/Y trial copy; clockless free and
|
|
443
|
+
// topup-paid accounts keep the generic copy
|
|
444
|
+
// 6. trial out_of_credits (error) — free grant spent WITHOUT the explicit
|
|
403
445
|
// signals (remaining <= 0 catch-all)
|
|
404
|
-
// 6. trial ended (error) — the grant clock ran out with credits left
|
|
405
446
|
// 7. over_included (warning) — paid metered overage (never blocked)
|
|
406
447
|
// 8. running low (warning) — free grant under low_threshold
|
|
407
448
|
// 9. trial active (warning) — the countdown; yields to running-low, the
|
|
@@ -448,12 +489,15 @@ export function resolveBillingBanner(entitlement, { actionLabel, actionsNoun, ap
|
|
|
448
489
|
}
|
|
449
490
|
|
|
450
491
|
// The free-grant trial states (null for paid plans — a paid plan is past
|
|
451
|
-
// the trial).
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
492
|
+
// the trial). TRIAL ENDED is resolved BEFORE the depleted branch: the
|
|
493
|
+
// backend maps an expired grant onto depleted / threshold_state "depleted"
|
|
494
|
+
// even with credits left, and after the 30-day window the lapsed clock is
|
|
495
|
+
// the true story (Andrew 2026-11-11 — portal 52109545 showed "You're out of
|
|
496
|
+
// credits" with 49/100 credits left).
|
|
456
497
|
const trial = onPaidPlan ? null : trialStateOf(ent, { appName });
|
|
498
|
+
if (trial?.kind === "trial_ended") {
|
|
499
|
+
return trialBannerView(trial);
|
|
500
|
+
}
|
|
457
501
|
|
|
458
502
|
// --- Hard-blocked. threshold_state "depleted" is the ONLY blocking state
|
|
459
503
|
// (a topup-model paid plan blocks at true zero); `over_included` overrides
|
|
@@ -464,11 +508,15 @@ export function resolveBillingBanner(entitlement, { actionLabel, actionsNoun, ap
|
|
|
464
508
|
const depleted =
|
|
465
509
|
thresholdState != null
|
|
466
510
|
? thresholdState === "depleted" && !overIncluded
|
|
467
|
-
: !onPaidPlan &&
|
|
468
|
-
(ent.depleted === true ||
|
|
469
|
-
(ent.entitled === false && trial?.kind !== "trial_ended"));
|
|
511
|
+
: !onPaidPlan && (ent.depleted === true || ent.entitled === false);
|
|
470
512
|
|
|
471
513
|
if (depleted) {
|
|
514
|
+
// A free TRIAL account at 100/100 inside the window flies the slide's
|
|
515
|
+
// used-X/Y trial copy (Andrew 2026-11-11); the generic line below is for
|
|
516
|
+
// clockless free accounts and topup-model paid plans blocked at zero.
|
|
517
|
+
if (trial?.kind === "out_of_credits" && trial.liveClock) {
|
|
518
|
+
return trialBannerView(trial);
|
|
519
|
+
}
|
|
472
520
|
return {
|
|
473
521
|
variant: "error",
|
|
474
522
|
title: "You're out of credits",
|
|
@@ -478,9 +526,9 @@ export function resolveBillingBanner(entitlement, { actionLabel, actionsNoun, ap
|
|
|
478
526
|
cta: { kind: "billing", label: "Go to Billing →" },
|
|
479
527
|
};
|
|
480
528
|
}
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
if (trial
|
|
529
|
+
// The spent free grant the explicit signals didn't catch (remaining <= 0
|
|
530
|
+
// without any flag, clock still running).
|
|
531
|
+
if (trial?.kind === "out_of_credits") {
|
|
484
532
|
return trialBannerView(trial);
|
|
485
533
|
}
|
|
486
534
|
if (overIncluded) {
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
// supportPath — the app's support route path, e.g.
|
|
13
13
|
// "/v1/hubspot/app_pages/<app_key>/support" (base-hosted) or
|
|
14
14
|
// "/api/v1/support" (self-hosted).
|
|
15
|
-
// appName — display name, used in headings/success copy.
|
|
16
15
|
// supportUrl — external support link shown as a fallback on error.
|
|
17
16
|
import React, { useState } from "react";
|
|
18
17
|
import {
|
|
@@ -37,7 +36,7 @@ import {
|
|
|
37
36
|
validateSupportFields,
|
|
38
37
|
} from "./supportLogic";
|
|
39
38
|
|
|
40
|
-
export function SupportTab({ context, supportPath,
|
|
39
|
+
export function SupportTab({ context, supportPath, supportUrl }) {
|
|
41
40
|
const [fields, setFields] = useState({
|
|
42
41
|
firstname: context.user?.firstName ?? "",
|
|
43
42
|
lastname: context.user?.lastName ?? "",
|
|
@@ -115,17 +114,14 @@ export function SupportTab({ context, supportPath, appName = "the app", supportU
|
|
|
115
114
|
return (
|
|
116
115
|
<Flex direction="column" gap="medium">
|
|
117
116
|
<Flex direction="column" gap="small">
|
|
118
|
-
<Heading>Contact
|
|
117
|
+
<Heading>Contact Us</Heading>
|
|
119
118
|
<Text>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
) : (
|
|
127
|
-
"use our contact page"
|
|
128
|
-
)}
|
|
119
|
+
Or shoot us your question at{" "}
|
|
120
|
+
<Link
|
|
121
|
+
href={{ url: "mailto:info@growthoperationsco.com", external: true }}
|
|
122
|
+
>
|
|
123
|
+
info@growthoperationsco.com
|
|
124
|
+
</Link>
|
|
129
125
|
.
|
|
130
126
|
</Text>
|
|
131
127
|
</Flex>
|