@windrun-huaiin/third-ui 31.3.5 → 31.3.7

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.
@@ -17,7 +17,7 @@ const formatNumber = (locale, value) => new Intl.NumberFormat(locale, {
17
17
  maximumFractionDigits: 0,
18
18
  }).format(value);
19
19
  function CreditOverviewClient({ data, locale, translations, className, expiringSoonThresholdDays = 7, customActionHandlers, }) {
20
- var _a;
20
+ var _a, _b;
21
21
  const { redirectToSignIn } = nextjs.useClerk();
22
22
  const navPopover = creditNavButton.useCreditNavPopover();
23
23
  const isMobile = (_a = navPopover === null || navPopover === void 0 ? void 0 : navPopover.isMobile) !== null && _a !== void 0 ? _a : false;
@@ -59,6 +59,9 @@ function CreditOverviewClient({ data, locale, translations, className, expiringS
59
59
  const subscription = data.subscription;
60
60
  const pricingContext = data.pricingContext;
61
61
  const ctaBehaviors = data.ctaBehaviors;
62
+ const billingOptions = (_b = pricingContext === null || pricingContext === void 0 ? void 0 : pricingContext.moneyPriceData.billingSwitch.options) !== null && _b !== void 0 ? _b : [];
63
+ const subscriptionBillingEnabled = billingOptions.some((option) => option.key === 'monthly' || option.key === 'yearly');
64
+ const onetimeBillingEnabled = billingOptions.some((option) => option.key === 'onetime');
62
65
  const getModalMoneyPriceData = React.useCallback((mode) => {
63
66
  if (!pricingContext) {
64
67
  return null;
@@ -232,10 +235,10 @@ function CreditOverviewClient({ data, locale, translations, className, expiringS
232
235
  userToggledRef.current = true;
233
236
  setBucketExpanded(true);
234
237
  }, []);
235
- return (jsxRuntime.jsxs("section", { className: utils.cn("flex flex-col gap-2 p-2 shadow-inner rounded-xl bg-white dark:bg-slate-900", className), children: [jsxRuntime.jsxs("header", { className: "relative rounded-2xl bg-linear-to-bl from-indigo-200/60 via-indigo-400/90 to-purple-200/50 p-4 shadow-inner dark:from-indigo-300/20 dark:via-slate-400 dark:to-slate-500/50 sm:p-6", children: [jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 sm:gap-3", children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-start rounded-full ", children: [jsxRuntime.jsx(icons.GemIcon, { "aria-hidden": true, className: "mr-2 h-6 w-6 sm:h-8 sm:w-8" }), jsxRuntime.jsx("span", { className: "text-base sm:text-lg", children: translations.totalLabel })] }), jsxRuntime.jsx("div", { className: "flex justify-center text-3xl font-semibold leading-tight sm:text-4xl", children: formatNumber(locale, data.totalBalance) }), jsxRuntime.jsxs("div", { className: "flex-1 flex-col gap-1", children: [jsxRuntime.jsx("p", { className: "text-xs text-gray-700 dark:text-slate-100 sm:text-sm", children: translations.subscriptionPeriodLabel }), jsxRuntime.jsx("h4", { className: "text-xl font-semibold sm:text-2xl", children: subscription ? subscription.planName : translations.subscriptionInactive })] }), jsxRuntime.jsx("div", { className: "pt-2 sm:pt-0", children: jsxRuntime.jsx(gradientButton.GradientButton, { title: subscription ? translations.subscriptionManage : translations.subscribePay, align: "center", icon: subscription ? jsxRuntime.jsx(icons.Settings2Icon, {}) : jsxRuntime.jsx(icons.BellIcon, {}), openInNewTab: false, className: "w-full", onClick: subscription ? handleManageAction : handleSubscribeAction }) })] }), jsxRuntime.jsx("div", { className: "absolute right-3 top-3 sm:right-6 sm:top-6", children: jsxRuntime.jsx(HoverInfo, { label: translations.totalLabel, description: translations.summaryDescription }) })] }), jsxRuntime.jsxs("section", { className: "relative flex flex-col gap-3 rounded-2xl border p-4 shadow-inner sm:gap-2 sm:p-5", children: [jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [jsxRuntime.jsx("h3", { className: "text-base text-gray-500 dark:text-slate-100 sm:text-lg", children: translations.bucketsTitle }), hasBuckets ? (jsxRuntime.jsx("button", { type: "button", "aria-expanded": bucketExpanded, "aria-label": bucketExpanded ? translations.hiddenDetail : translations.expandDetail, onClick: toggleBucketExpanded, className: "flex h-7 w-7 items-center justify-center rounded-full border border-transparent bg-white text-purple-600 shadow-[0_6px_20px_rgba(99,102,241,0.25)] transition-colors hover:text-purple-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-purple-500 dark:bg-[#1b1541] dark:text-purple-100 dark:hover:text-purple-50 dark:shadow-[0_6px_22px_rgba(112,86,255,0.35)]", children: bucketExpanded ? (jsxRuntime.jsx(icons.ChevronUpIcon, { className: "h-4 w-4" })) : (jsxRuntime.jsx(icons.ChevronDownIcon, { className: "h-4 w-4" })) })) : null] }), hasBuckets ? (bucketExpanded ? (jsxRuntime.jsx("ul", { className: "flex flex-col gap-2", children: buckets.map((bucket) => {
238
+ return (jsxRuntime.jsxs("section", { className: utils.cn("flex flex-col gap-2 p-2 shadow-inner rounded-xl bg-white dark:bg-slate-900", className), children: [jsxRuntime.jsxs("header", { className: "relative rounded-2xl bg-linear-to-bl from-indigo-200/60 via-indigo-400/90 to-purple-200/50 p-4 shadow-inner dark:from-indigo-300/20 dark:via-slate-400 dark:to-slate-500/50 sm:p-6", children: [jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 sm:gap-3", children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-start rounded-full ", children: [jsxRuntime.jsx(icons.GemIcon, { "aria-hidden": true, className: "mr-2 h-6 w-6 sm:h-8 sm:w-8" }), jsxRuntime.jsx("span", { className: "text-base sm:text-lg", children: translations.totalLabel })] }), jsxRuntime.jsx("div", { className: "flex justify-center text-3xl font-semibold leading-tight sm:text-4xl", children: formatNumber(locale, data.totalBalance) }), subscriptionBillingEnabled ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "flex-1 flex-col gap-1", children: [jsxRuntime.jsx("p", { className: "text-xs text-gray-700 dark:text-slate-100 sm:text-sm", children: translations.subscriptionPeriodLabel }), jsxRuntime.jsx("h4", { className: "text-xl font-semibold sm:text-2xl", children: subscription ? subscription.planName : translations.subscriptionInactive })] }), jsxRuntime.jsx("div", { className: "pt-2 sm:pt-0", children: jsxRuntime.jsx(gradientButton.GradientButton, { title: subscription ? translations.subscriptionManage : translations.subscribePay, align: "center", icon: subscription ? jsxRuntime.jsx(icons.Settings2Icon, {}) : jsxRuntime.jsx(icons.BellIcon, {}), openInNewTab: false, className: "w-full", onClick: subscription ? handleManageAction : handleSubscribeAction }) })] })) : null] }), jsxRuntime.jsx("div", { className: "absolute right-3 top-3 sm:right-6 sm:top-6", children: jsxRuntime.jsx(HoverInfo, { label: translations.totalLabel, description: translations.summaryDescription }) })] }), jsxRuntime.jsxs("section", { className: "relative flex flex-col gap-3 rounded-2xl border p-4 shadow-inner sm:gap-2 sm:p-5", children: [jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [jsxRuntime.jsx("h3", { className: "text-base text-gray-500 dark:text-slate-100 sm:text-lg", children: translations.bucketsTitle }), hasBuckets ? (jsxRuntime.jsx("button", { type: "button", "aria-expanded": bucketExpanded, "aria-label": bucketExpanded ? translations.hiddenDetail : translations.expandDetail, onClick: toggleBucketExpanded, className: "flex h-7 w-7 items-center justify-center rounded-full border border-transparent bg-white text-purple-600 shadow-[0_6px_20px_rgba(99,102,241,0.25)] transition-colors hover:text-purple-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-purple-500 dark:bg-[#1b1541] dark:text-purple-100 dark:hover:text-purple-50 dark:shadow-[0_6px_22px_rgba(112,86,255,0.35)]", children: bucketExpanded ? (jsxRuntime.jsx(icons.ChevronUpIcon, { className: "h-4 w-4" })) : (jsxRuntime.jsx(icons.ChevronDownIcon, { className: "h-4 w-4" })) })) : null] }), hasBuckets ? (bucketExpanded ? (jsxRuntime.jsx("ul", { className: "flex flex-col gap-2", children: buckets.map((bucket) => {
236
239
  const balanceDisplay = formatNumber(locale, bucket.balance);
237
240
  return (jsxRuntime.jsxs("li", { "data-credit-kind": bucket.kind, className: "rounded-2xl border border-slate-200/70 bg-white/85 px-3 py-3 text-sm shadow-sm transition-transform hover:-translate-y-0.5 hover:shadow-md dark:border-slate-800/60 dark:bg-slate-900/60 sm:px-4", children: [jsxRuntime.jsxs("div", { className: "grid grid-cols-[1fr_auto] items-center gap-3 text-xs sm:text-sm", children: [jsxRuntime.jsx("span", { className: "flex min-w-0 items-center gap-2", children: jsxRuntime.jsx("span", { className: "max-w-full truncate rounded-full bg-purple-50 px-2 py-1 text-xs text-purple-600 shadow-sm dark:bg-purple-500/20 dark:text-purple-100 sm:text-sm", children: bucket.computedLabel }) }), jsxRuntime.jsx("span", { className: "flex min-w-0 justify-end", children: jsxRuntime.jsx("span", { className: "text-right text-base font-semibold leading-tight text-gray-500 dark:text-slate-100 sm:text-lg", title: balanceDisplay, children: balanceDisplay }) })] }), jsxRuntime.jsx("div", { className: "mt-3 flex justify-end gap-2", children: jsxRuntime.jsxs("span", { className: "text-[11px] text-gray-500 dark:text-slate-100 sm:text-xs", children: [translations.expiredAtLabel, ": ", bucket.expiresAt] }) })] }, bucket.kind));
238
- }) })) : (jsxRuntime.jsx("button", { type: "button", onClick: expandBuckets, className: "w-full rounded-2xl border border-slate-200/70 bg-white/85 p-6 sm:px-4 text-sm shadow-sm transition-transform hover:-translate-y-0.5 hover:shadow-md dark:border-slate-800/60 dark:bg-slate-900/60 hover:text-purple-500", children: translations.expandDetail }))) : (jsxRuntime.jsx("div", { className: "w-full rounded-2xl border border-slate-200/70 bg-white/85 p-6 sm:px-4 text-sm shadow-sm transition-transform dark:border-slate-800/60 dark:bg-slate-900/60 text-center", children: translations.bucketsEmpty })), jsxRuntime.jsx(gradientButton.GradientButton, { title: translations.onetimeBuy, icon: jsxRuntime.jsx(icons.ShoppingCartIcon, {}), align: "center", className: "w-full text-sm sm:text-base", onClick: handleOnetimeAction })] })] }));
241
+ }) })) : (jsxRuntime.jsx("button", { type: "button", onClick: expandBuckets, className: "w-full rounded-2xl border border-slate-200/70 bg-white/85 p-6 sm:px-4 text-sm shadow-sm transition-transform hover:-translate-y-0.5 hover:shadow-md dark:border-slate-800/60 dark:bg-slate-900/60 hover:text-purple-500", children: translations.expandDetail }))) : (jsxRuntime.jsx("div", { className: "w-full rounded-2xl border border-slate-200/70 bg-white/85 p-6 sm:px-4 text-sm shadow-sm transition-transform dark:border-slate-800/60 dark:bg-slate-900/60 text-center", children: translations.bucketsEmpty })), onetimeBillingEnabled ? (jsxRuntime.jsx(gradientButton.GradientButton, { title: translations.onetimeBuy, icon: jsxRuntime.jsx(icons.ShoppingCartIcon, {}), align: "center", className: "w-full text-sm sm:text-base", onClick: handleOnetimeAction })) : null] })] }));
239
242
  }
240
243
  function deriveStatus(expiresAt, thresholdDays = 7) {
241
244
  if (!expiresAt) {
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { __awaiter } from 'tslib';
3
- import { jsxs, jsx } from 'react/jsx-runtime';
3
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
  import { useClerk } from '@clerk/nextjs';
5
5
  import { GradientButton } from '../buttons/gradient-button.mjs';
6
6
  import { useState, useRef, useCallback, useMemo, useLayoutEffect } from 'react';
@@ -15,7 +15,7 @@ const formatNumber = (locale, value) => new Intl.NumberFormat(locale, {
15
15
  maximumFractionDigits: 0,
16
16
  }).format(value);
17
17
  function CreditOverviewClient({ data, locale, translations, className, expiringSoonThresholdDays = 7, customActionHandlers, }) {
18
- var _a;
18
+ var _a, _b;
19
19
  const { redirectToSignIn } = useClerk();
20
20
  const navPopover = useCreditNavPopover();
21
21
  const isMobile = (_a = navPopover === null || navPopover === void 0 ? void 0 : navPopover.isMobile) !== null && _a !== void 0 ? _a : false;
@@ -57,6 +57,9 @@ function CreditOverviewClient({ data, locale, translations, className, expiringS
57
57
  const subscription = data.subscription;
58
58
  const pricingContext = data.pricingContext;
59
59
  const ctaBehaviors = data.ctaBehaviors;
60
+ const billingOptions = (_b = pricingContext === null || pricingContext === void 0 ? void 0 : pricingContext.moneyPriceData.billingSwitch.options) !== null && _b !== void 0 ? _b : [];
61
+ const subscriptionBillingEnabled = billingOptions.some((option) => option.key === 'monthly' || option.key === 'yearly');
62
+ const onetimeBillingEnabled = billingOptions.some((option) => option.key === 'onetime');
60
63
  const getModalMoneyPriceData = useCallback((mode) => {
61
64
  if (!pricingContext) {
62
65
  return null;
@@ -230,10 +233,10 @@ function CreditOverviewClient({ data, locale, translations, className, expiringS
230
233
  userToggledRef.current = true;
231
234
  setBucketExpanded(true);
232
235
  }, []);
233
- return (jsxs("section", { className: cn("flex flex-col gap-2 p-2 shadow-inner rounded-xl bg-white dark:bg-slate-900", className), children: [jsxs("header", { className: "relative rounded-2xl bg-linear-to-bl from-indigo-200/60 via-indigo-400/90 to-purple-200/50 p-4 shadow-inner dark:from-indigo-300/20 dark:via-slate-400 dark:to-slate-500/50 sm:p-6", children: [jsxs("div", { className: "flex flex-col gap-2 sm:gap-3", children: [jsxs("div", { className: "flex items-center justify-start rounded-full ", children: [jsx(GemIcon, { "aria-hidden": true, className: "mr-2 h-6 w-6 sm:h-8 sm:w-8" }), jsx("span", { className: "text-base sm:text-lg", children: translations.totalLabel })] }), jsx("div", { className: "flex justify-center text-3xl font-semibold leading-tight sm:text-4xl", children: formatNumber(locale, data.totalBalance) }), jsxs("div", { className: "flex-1 flex-col gap-1", children: [jsx("p", { className: "text-xs text-gray-700 dark:text-slate-100 sm:text-sm", children: translations.subscriptionPeriodLabel }), jsx("h4", { className: "text-xl font-semibold sm:text-2xl", children: subscription ? subscription.planName : translations.subscriptionInactive })] }), jsx("div", { className: "pt-2 sm:pt-0", children: jsx(GradientButton, { title: subscription ? translations.subscriptionManage : translations.subscribePay, align: "center", icon: subscription ? jsx(Settings2Icon, {}) : jsx(BellIcon, {}), openInNewTab: false, className: "w-full", onClick: subscription ? handleManageAction : handleSubscribeAction }) })] }), jsx("div", { className: "absolute right-3 top-3 sm:right-6 sm:top-6", children: jsx(HoverInfo, { label: translations.totalLabel, description: translations.summaryDescription }) })] }), jsxs("section", { className: "relative flex flex-col gap-3 rounded-2xl border p-4 shadow-inner sm:gap-2 sm:p-5", children: [jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [jsx("h3", { className: "text-base text-gray-500 dark:text-slate-100 sm:text-lg", children: translations.bucketsTitle }), hasBuckets ? (jsx("button", { type: "button", "aria-expanded": bucketExpanded, "aria-label": bucketExpanded ? translations.hiddenDetail : translations.expandDetail, onClick: toggleBucketExpanded, className: "flex h-7 w-7 items-center justify-center rounded-full border border-transparent bg-white text-purple-600 shadow-[0_6px_20px_rgba(99,102,241,0.25)] transition-colors hover:text-purple-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-purple-500 dark:bg-[#1b1541] dark:text-purple-100 dark:hover:text-purple-50 dark:shadow-[0_6px_22px_rgba(112,86,255,0.35)]", children: bucketExpanded ? (jsx(ChevronUpIcon, { className: "h-4 w-4" })) : (jsx(ChevronDownIcon, { className: "h-4 w-4" })) })) : null] }), hasBuckets ? (bucketExpanded ? (jsx("ul", { className: "flex flex-col gap-2", children: buckets.map((bucket) => {
236
+ return (jsxs("section", { className: cn("flex flex-col gap-2 p-2 shadow-inner rounded-xl bg-white dark:bg-slate-900", className), children: [jsxs("header", { className: "relative rounded-2xl bg-linear-to-bl from-indigo-200/60 via-indigo-400/90 to-purple-200/50 p-4 shadow-inner dark:from-indigo-300/20 dark:via-slate-400 dark:to-slate-500/50 sm:p-6", children: [jsxs("div", { className: "flex flex-col gap-2 sm:gap-3", children: [jsxs("div", { className: "flex items-center justify-start rounded-full ", children: [jsx(GemIcon, { "aria-hidden": true, className: "mr-2 h-6 w-6 sm:h-8 sm:w-8" }), jsx("span", { className: "text-base sm:text-lg", children: translations.totalLabel })] }), jsx("div", { className: "flex justify-center text-3xl font-semibold leading-tight sm:text-4xl", children: formatNumber(locale, data.totalBalance) }), subscriptionBillingEnabled ? (jsxs(Fragment, { children: [jsxs("div", { className: "flex-1 flex-col gap-1", children: [jsx("p", { className: "text-xs text-gray-700 dark:text-slate-100 sm:text-sm", children: translations.subscriptionPeriodLabel }), jsx("h4", { className: "text-xl font-semibold sm:text-2xl", children: subscription ? subscription.planName : translations.subscriptionInactive })] }), jsx("div", { className: "pt-2 sm:pt-0", children: jsx(GradientButton, { title: subscription ? translations.subscriptionManage : translations.subscribePay, align: "center", icon: subscription ? jsx(Settings2Icon, {}) : jsx(BellIcon, {}), openInNewTab: false, className: "w-full", onClick: subscription ? handleManageAction : handleSubscribeAction }) })] })) : null] }), jsx("div", { className: "absolute right-3 top-3 sm:right-6 sm:top-6", children: jsx(HoverInfo, { label: translations.totalLabel, description: translations.summaryDescription }) })] }), jsxs("section", { className: "relative flex flex-col gap-3 rounded-2xl border p-4 shadow-inner sm:gap-2 sm:p-5", children: [jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [jsx("h3", { className: "text-base text-gray-500 dark:text-slate-100 sm:text-lg", children: translations.bucketsTitle }), hasBuckets ? (jsx("button", { type: "button", "aria-expanded": bucketExpanded, "aria-label": bucketExpanded ? translations.hiddenDetail : translations.expandDetail, onClick: toggleBucketExpanded, className: "flex h-7 w-7 items-center justify-center rounded-full border border-transparent bg-white text-purple-600 shadow-[0_6px_20px_rgba(99,102,241,0.25)] transition-colors hover:text-purple-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-purple-500 dark:bg-[#1b1541] dark:text-purple-100 dark:hover:text-purple-50 dark:shadow-[0_6px_22px_rgba(112,86,255,0.35)]", children: bucketExpanded ? (jsx(ChevronUpIcon, { className: "h-4 w-4" })) : (jsx(ChevronDownIcon, { className: "h-4 w-4" })) })) : null] }), hasBuckets ? (bucketExpanded ? (jsx("ul", { className: "flex flex-col gap-2", children: buckets.map((bucket) => {
234
237
  const balanceDisplay = formatNumber(locale, bucket.balance);
235
238
  return (jsxs("li", { "data-credit-kind": bucket.kind, className: "rounded-2xl border border-slate-200/70 bg-white/85 px-3 py-3 text-sm shadow-sm transition-transform hover:-translate-y-0.5 hover:shadow-md dark:border-slate-800/60 dark:bg-slate-900/60 sm:px-4", children: [jsxs("div", { className: "grid grid-cols-[1fr_auto] items-center gap-3 text-xs sm:text-sm", children: [jsx("span", { className: "flex min-w-0 items-center gap-2", children: jsx("span", { className: "max-w-full truncate rounded-full bg-purple-50 px-2 py-1 text-xs text-purple-600 shadow-sm dark:bg-purple-500/20 dark:text-purple-100 sm:text-sm", children: bucket.computedLabel }) }), jsx("span", { className: "flex min-w-0 justify-end", children: jsx("span", { className: "text-right text-base font-semibold leading-tight text-gray-500 dark:text-slate-100 sm:text-lg", title: balanceDisplay, children: balanceDisplay }) })] }), jsx("div", { className: "mt-3 flex justify-end gap-2", children: jsxs("span", { className: "text-[11px] text-gray-500 dark:text-slate-100 sm:text-xs", children: [translations.expiredAtLabel, ": ", bucket.expiresAt] }) })] }, bucket.kind));
236
- }) })) : (jsx("button", { type: "button", onClick: expandBuckets, className: "w-full rounded-2xl border border-slate-200/70 bg-white/85 p-6 sm:px-4 text-sm shadow-sm transition-transform hover:-translate-y-0.5 hover:shadow-md dark:border-slate-800/60 dark:bg-slate-900/60 hover:text-purple-500", children: translations.expandDetail }))) : (jsx("div", { className: "w-full rounded-2xl border border-slate-200/70 bg-white/85 p-6 sm:px-4 text-sm shadow-sm transition-transform dark:border-slate-800/60 dark:bg-slate-900/60 text-center", children: translations.bucketsEmpty })), jsx(GradientButton, { title: translations.onetimeBuy, icon: jsx(ShoppingCartIcon, {}), align: "center", className: "w-full text-sm sm:text-base", onClick: handleOnetimeAction })] })] }));
239
+ }) })) : (jsx("button", { type: "button", onClick: expandBuckets, className: "w-full rounded-2xl border border-slate-200/70 bg-white/85 p-6 sm:px-4 text-sm shadow-sm transition-transform hover:-translate-y-0.5 hover:shadow-md dark:border-slate-800/60 dark:bg-slate-900/60 hover:text-purple-500", children: translations.expandDetail }))) : (jsx("div", { className: "w-full rounded-2xl border border-slate-200/70 bg-white/85 p-6 sm:px-4 text-sm shadow-sm transition-transform dark:border-slate-800/60 dark:bg-slate-900/60 text-center", children: translations.bucketsEmpty })), onetimeBillingEnabled ? (jsx(GradientButton, { title: translations.onetimeBuy, icon: jsx(ShoppingCartIcon, {}), align: "center", className: "w-full text-sm sm:text-base", onClick: handleOnetimeAction })) : null] })] }));
237
240
  }
238
241
  function deriveStatus(expiresAt, thresholdDays = 7) {
239
242
  if (!expiresAt) {
@@ -1,5 +1,6 @@
1
1
  import { type CreditOverviewTranslations } from './credit-overview-client';
2
2
  import type { CreditOverviewData } from './types';
3
+ export declare const DEFAULT_CREDIT_OVERVIEW_REFRESH_EVENT = "windrun-huaiin:credit-overview:refresh";
3
4
  export interface CreditOverviewPayload {
4
5
  data: CreditOverviewData;
5
6
  totalLabel: string;
@@ -8,5 +9,24 @@ export interface CreditOverviewPayload {
8
9
  export interface CreditOverviewNavClientProps {
9
10
  locale: string;
10
11
  endpoint: string;
12
+ /**
13
+ * Billing types enabled by the business app.
14
+ * Defaults are handled by the API/payload builder when omitted.
15
+ */
16
+ enabledBillingTypes?: string[];
17
+ /**
18
+ * Browser event names that should trigger credit overview reload.
19
+ * Business apps can dispatch these events after credit balance changes.
20
+ *
21
+ * Default: ['windrun:credit-overview:refresh']
22
+ * Empty array means event listening is disabled.
23
+ */
24
+ refreshEvents?: string[];
25
+ /**
26
+ * Optional debounce for refresh event bursts.
27
+ * Default: 300ms.
28
+ */
29
+ refreshDebounceMs?: number;
11
30
  }
12
- export declare function CreditOverviewNavClient({ locale, endpoint, }: CreditOverviewNavClientProps): import("react/jsx-runtime").JSX.Element | null;
31
+ export declare function dispatchCreditOverviewRefresh(eventName?: string): void;
32
+ export declare function CreditOverviewNavClient({ locale, endpoint, enabledBillingTypes, refreshEvents, refreshDebounceMs, }: CreditOverviewNavClientProps): import("react/jsx-runtime").JSX.Element | null;
@@ -8,54 +8,130 @@ var React = require('react');
8
8
  var creditNavButton = require('./credit-nav-button.js');
9
9
  var creditOverviewClient = require('./credit-overview-client.js');
10
10
 
11
- function buildCreditOverviewUrl(endpoint, locale) {
11
+ const DEFAULT_CREDIT_OVERVIEW_REFRESH_EVENT = 'windrun-huaiin:credit-overview:refresh';
12
+ function normalizeEnabledBillingTypes(enabledBillingTypes) {
13
+ if (!(enabledBillingTypes === null || enabledBillingTypes === void 0 ? void 0 : enabledBillingTypes.length)) {
14
+ return [];
15
+ }
16
+ return Array.from(new Set(enabledBillingTypes
17
+ .map((billingType) => billingType.trim())
18
+ .filter((billingType) => billingType.length > 0)));
19
+ }
20
+ function buildEnabledBillingTypesKey(enabledBillingTypes) {
21
+ return normalizeEnabledBillingTypes(enabledBillingTypes).join('\n');
22
+ }
23
+ function buildCreditOverviewUrl(endpoint, locale, enabledBillingTypes) {
12
24
  const url = new URL(endpoint, window.location.origin);
13
25
  url.searchParams.set('locale', locale);
26
+ for (const billingType of enabledBillingTypes) {
27
+ url.searchParams.append('enabledBillingTypes', billingType);
28
+ }
14
29
  return url.toString();
15
30
  }
16
- function CreditOverviewNavClient({ locale, endpoint, }) {
31
+ function normalizeRefreshEvents(refreshEvents) {
32
+ const events = refreshEvents !== null && refreshEvents !== void 0 ? refreshEvents : [DEFAULT_CREDIT_OVERVIEW_REFRESH_EVENT];
33
+ return Array.from(new Set(events
34
+ .map((eventName) => eventName.trim())
35
+ .filter((eventName) => eventName.length > 0)));
36
+ }
37
+ function buildRefreshEventsKey(refreshEvents) {
38
+ return normalizeRefreshEvents(refreshEvents).join('\n');
39
+ }
40
+ function dispatchCreditOverviewRefresh(eventName = DEFAULT_CREDIT_OVERVIEW_REFRESH_EVENT) {
41
+ if (typeof window === 'undefined') {
42
+ return;
43
+ }
44
+ window.dispatchEvent(new CustomEvent(eventName));
45
+ }
46
+ function CreditOverviewNavClient({ locale, endpoint, enabledBillingTypes, refreshEvents, refreshDebounceMs = 300, }) {
17
47
  const { isLoaded, isSignedIn, userId } = nextjs.useAuth();
18
48
  const [payload, setPayload] = React.useState(null);
49
+ const abortControllerRef = React.useRef(null);
50
+ const isMountedRef = React.useRef(false);
51
+ const refreshEventsKey = buildRefreshEventsKey(refreshEvents);
52
+ const normalizedRefreshEvents = React.useMemo(() => refreshEventsKey.split('\n').filter(Boolean), [refreshEventsKey]);
53
+ const enabledBillingTypesKey = buildEnabledBillingTypesKey(enabledBillingTypes);
54
+ const normalizedEnabledBillingTypes = React.useMemo(() => enabledBillingTypesKey.split('\n').filter(Boolean), [enabledBillingTypesKey]);
19
55
  React.useEffect(() => {
56
+ isMountedRef.current = true;
57
+ return () => {
58
+ var _a;
59
+ isMountedRef.current = false;
60
+ (_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
61
+ };
62
+ }, []);
63
+ const loadCreditOverview = React.useCallback(() => tslib.__awaiter(this, void 0, void 0, function* () {
64
+ var _a;
65
+ (_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
66
+ const controller = new AbortController();
67
+ abortControllerRef.current = controller;
68
+ try {
69
+ const response = yield fetch(buildCreditOverviewUrl(endpoint, locale, normalizedEnabledBillingTypes), {
70
+ cache: 'no-store',
71
+ credentials: 'same-origin',
72
+ signal: controller.signal,
73
+ });
74
+ if (!response.ok) {
75
+ if (!controller.signal.aborted && isMountedRef.current) {
76
+ setPayload(null);
77
+ }
78
+ return;
79
+ }
80
+ const nextPayload = (yield response.json());
81
+ if (!controller.signal.aborted && isMountedRef.current) {
82
+ setPayload(nextPayload);
83
+ }
84
+ }
85
+ catch (error) {
86
+ if (!controller.signal.aborted && isMountedRef.current) {
87
+ setPayload(null);
88
+ console.warn('[CreditOverviewNavClient] Failed to load credit overview', error);
89
+ }
90
+ }
91
+ finally {
92
+ if (abortControllerRef.current === controller) {
93
+ abortControllerRef.current = null;
94
+ }
95
+ }
96
+ }), [endpoint, locale, normalizedEnabledBillingTypes]);
97
+ React.useEffect(() => {
98
+ var _a;
20
99
  if (!isLoaded) {
21
100
  return;
22
101
  }
23
102
  if (!isSignedIn) {
103
+ (_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
104
+ abortControllerRef.current = null;
24
105
  setPayload(null);
25
106
  return;
26
107
  }
27
- const controller = new AbortController();
28
- function loadCreditOverview() {
29
- return tslib.__awaiter(this, void 0, void 0, function* () {
30
- try {
31
- const response = yield fetch(buildCreditOverviewUrl(endpoint, locale), {
32
- credentials: 'same-origin',
33
- signal: controller.signal,
34
- });
35
- if (!response.ok) {
36
- if (!controller.signal.aborted) {
37
- setPayload(null);
38
- }
39
- return;
40
- }
41
- const nextPayload = (yield response.json());
42
- if (!controller.signal.aborted) {
43
- setPayload(nextPayload);
44
- }
45
- }
46
- catch (error) {
47
- if (!controller.signal.aborted) {
48
- setPayload(null);
49
- console.warn('[CreditOverviewNavClient] Failed to load credit overview', error);
50
- }
51
- }
52
- });
108
+ void loadCreditOverview();
109
+ }, [isLoaded, isSignedIn, loadCreditOverview, userId]);
110
+ React.useEffect(() => {
111
+ if (!isLoaded || !isSignedIn || normalizedRefreshEvents.length === 0) {
112
+ return;
113
+ }
114
+ let debounceTimer = null;
115
+ const refresh = () => {
116
+ if (debounceTimer) {
117
+ window.clearTimeout(debounceTimer);
118
+ }
119
+ debounceTimer = window.setTimeout(() => {
120
+ void loadCreditOverview();
121
+ }, refreshDebounceMs);
122
+ };
123
+ for (const eventName of normalizedRefreshEvents) {
124
+ window.addEventListener(eventName, refresh);
53
125
  }
54
- loadCreditOverview();
55
126
  return () => {
56
- controller.abort();
127
+ if (debounceTimer) {
128
+ window.clearTimeout(debounceTimer);
129
+ }
130
+ for (const eventName of normalizedRefreshEvents) {
131
+ window.removeEventListener(eventName, refresh);
132
+ }
57
133
  };
58
- }, [endpoint, isLoaded, isSignedIn, locale, userId]);
134
+ }, [isLoaded, isSignedIn, loadCreditOverview, normalizedRefreshEvents, refreshDebounceMs]);
59
135
  if (!payload) {
60
136
  return null;
61
137
  }
@@ -63,3 +139,5 @@ function CreditOverviewNavClient({ locale, endpoint, }) {
63
139
  }
64
140
 
65
141
  exports.CreditOverviewNavClient = CreditOverviewNavClient;
142
+ exports.DEFAULT_CREDIT_OVERVIEW_REFRESH_EVENT = DEFAULT_CREDIT_OVERVIEW_REFRESH_EVENT;
143
+ exports.dispatchCreditOverviewRefresh = dispatchCreditOverviewRefresh;
@@ -2,62 +2,138 @@
2
2
  import { __awaiter } from 'tslib';
3
3
  import { jsx } from 'react/jsx-runtime';
4
4
  import { useAuth } from '@clerk/nextjs';
5
- import { useState, useEffect } from 'react';
5
+ import { useState, useRef, useMemo, useEffect, useCallback } from 'react';
6
6
  import { CreditNavButton } from './credit-nav-button.mjs';
7
7
  import { CreditOverviewClient } from './credit-overview-client.mjs';
8
8
 
9
- function buildCreditOverviewUrl(endpoint, locale) {
9
+ const DEFAULT_CREDIT_OVERVIEW_REFRESH_EVENT = 'windrun-huaiin:credit-overview:refresh';
10
+ function normalizeEnabledBillingTypes(enabledBillingTypes) {
11
+ if (!(enabledBillingTypes === null || enabledBillingTypes === void 0 ? void 0 : enabledBillingTypes.length)) {
12
+ return [];
13
+ }
14
+ return Array.from(new Set(enabledBillingTypes
15
+ .map((billingType) => billingType.trim())
16
+ .filter((billingType) => billingType.length > 0)));
17
+ }
18
+ function buildEnabledBillingTypesKey(enabledBillingTypes) {
19
+ return normalizeEnabledBillingTypes(enabledBillingTypes).join('\n');
20
+ }
21
+ function buildCreditOverviewUrl(endpoint, locale, enabledBillingTypes) {
10
22
  const url = new URL(endpoint, window.location.origin);
11
23
  url.searchParams.set('locale', locale);
24
+ for (const billingType of enabledBillingTypes) {
25
+ url.searchParams.append('enabledBillingTypes', billingType);
26
+ }
12
27
  return url.toString();
13
28
  }
14
- function CreditOverviewNavClient({ locale, endpoint, }) {
29
+ function normalizeRefreshEvents(refreshEvents) {
30
+ const events = refreshEvents !== null && refreshEvents !== void 0 ? refreshEvents : [DEFAULT_CREDIT_OVERVIEW_REFRESH_EVENT];
31
+ return Array.from(new Set(events
32
+ .map((eventName) => eventName.trim())
33
+ .filter((eventName) => eventName.length > 0)));
34
+ }
35
+ function buildRefreshEventsKey(refreshEvents) {
36
+ return normalizeRefreshEvents(refreshEvents).join('\n');
37
+ }
38
+ function dispatchCreditOverviewRefresh(eventName = DEFAULT_CREDIT_OVERVIEW_REFRESH_EVENT) {
39
+ if (typeof window === 'undefined') {
40
+ return;
41
+ }
42
+ window.dispatchEvent(new CustomEvent(eventName));
43
+ }
44
+ function CreditOverviewNavClient({ locale, endpoint, enabledBillingTypes, refreshEvents, refreshDebounceMs = 300, }) {
15
45
  const { isLoaded, isSignedIn, userId } = useAuth();
16
46
  const [payload, setPayload] = useState(null);
47
+ const abortControllerRef = useRef(null);
48
+ const isMountedRef = useRef(false);
49
+ const refreshEventsKey = buildRefreshEventsKey(refreshEvents);
50
+ const normalizedRefreshEvents = useMemo(() => refreshEventsKey.split('\n').filter(Boolean), [refreshEventsKey]);
51
+ const enabledBillingTypesKey = buildEnabledBillingTypesKey(enabledBillingTypes);
52
+ const normalizedEnabledBillingTypes = useMemo(() => enabledBillingTypesKey.split('\n').filter(Boolean), [enabledBillingTypesKey]);
17
53
  useEffect(() => {
54
+ isMountedRef.current = true;
55
+ return () => {
56
+ var _a;
57
+ isMountedRef.current = false;
58
+ (_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
59
+ };
60
+ }, []);
61
+ const loadCreditOverview = useCallback(() => __awaiter(this, void 0, void 0, function* () {
62
+ var _a;
63
+ (_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
64
+ const controller = new AbortController();
65
+ abortControllerRef.current = controller;
66
+ try {
67
+ const response = yield fetch(buildCreditOverviewUrl(endpoint, locale, normalizedEnabledBillingTypes), {
68
+ cache: 'no-store',
69
+ credentials: 'same-origin',
70
+ signal: controller.signal,
71
+ });
72
+ if (!response.ok) {
73
+ if (!controller.signal.aborted && isMountedRef.current) {
74
+ setPayload(null);
75
+ }
76
+ return;
77
+ }
78
+ const nextPayload = (yield response.json());
79
+ if (!controller.signal.aborted && isMountedRef.current) {
80
+ setPayload(nextPayload);
81
+ }
82
+ }
83
+ catch (error) {
84
+ if (!controller.signal.aborted && isMountedRef.current) {
85
+ setPayload(null);
86
+ console.warn('[CreditOverviewNavClient] Failed to load credit overview', error);
87
+ }
88
+ }
89
+ finally {
90
+ if (abortControllerRef.current === controller) {
91
+ abortControllerRef.current = null;
92
+ }
93
+ }
94
+ }), [endpoint, locale, normalizedEnabledBillingTypes]);
95
+ useEffect(() => {
96
+ var _a;
18
97
  if (!isLoaded) {
19
98
  return;
20
99
  }
21
100
  if (!isSignedIn) {
101
+ (_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
102
+ abortControllerRef.current = null;
22
103
  setPayload(null);
23
104
  return;
24
105
  }
25
- const controller = new AbortController();
26
- function loadCreditOverview() {
27
- return __awaiter(this, void 0, void 0, function* () {
28
- try {
29
- const response = yield fetch(buildCreditOverviewUrl(endpoint, locale), {
30
- credentials: 'same-origin',
31
- signal: controller.signal,
32
- });
33
- if (!response.ok) {
34
- if (!controller.signal.aborted) {
35
- setPayload(null);
36
- }
37
- return;
38
- }
39
- const nextPayload = (yield response.json());
40
- if (!controller.signal.aborted) {
41
- setPayload(nextPayload);
42
- }
43
- }
44
- catch (error) {
45
- if (!controller.signal.aborted) {
46
- setPayload(null);
47
- console.warn('[CreditOverviewNavClient] Failed to load credit overview', error);
48
- }
49
- }
50
- });
106
+ void loadCreditOverview();
107
+ }, [isLoaded, isSignedIn, loadCreditOverview, userId]);
108
+ useEffect(() => {
109
+ if (!isLoaded || !isSignedIn || normalizedRefreshEvents.length === 0) {
110
+ return;
111
+ }
112
+ let debounceTimer = null;
113
+ const refresh = () => {
114
+ if (debounceTimer) {
115
+ window.clearTimeout(debounceTimer);
116
+ }
117
+ debounceTimer = window.setTimeout(() => {
118
+ void loadCreditOverview();
119
+ }, refreshDebounceMs);
120
+ };
121
+ for (const eventName of normalizedRefreshEvents) {
122
+ window.addEventListener(eventName, refresh);
51
123
  }
52
- loadCreditOverview();
53
124
  return () => {
54
- controller.abort();
125
+ if (debounceTimer) {
126
+ window.clearTimeout(debounceTimer);
127
+ }
128
+ for (const eventName of normalizedRefreshEvents) {
129
+ window.removeEventListener(eventName, refresh);
130
+ }
55
131
  };
56
- }, [endpoint, isLoaded, isSignedIn, locale, userId]);
132
+ }, [isLoaded, isSignedIn, loadCreditOverview, normalizedRefreshEvents, refreshDebounceMs]);
57
133
  if (!payload) {
58
134
  return null;
59
135
  }
60
136
  return (jsx(CreditNavButton, { locale: locale, totalBalance: payload.data.totalBalance, totalLabel: payload.totalLabel, children: jsx(CreditOverviewClient, { locale: locale, data: payload.data, translations: payload.translations }) }));
61
137
  }
62
138
 
63
- export { CreditOverviewNavClient };
139
+ export { CreditOverviewNavClient, DEFAULT_CREDIT_OVERVIEW_REFRESH_EVENT, dispatchCreditOverviewRefresh };
@@ -1,5 +1,5 @@
1
1
  export { CreditOverviewClient } from './credit-overview-client';
2
- export { CreditOverviewNavClient } from './credit-overview-nav-client';
2
+ export { CreditOverviewNavClient, DEFAULT_CREDIT_OVERVIEW_REFRESH_EVENT, dispatchCreditOverviewRefresh, } from './credit-overview-nav-client';
3
3
  export { CreditNavButton } from './credit-nav-button';
4
4
  export type { CreditOverviewTranslations } from './credit-overview-client';
5
5
  export type { CreditOverviewNavClientProps, CreditOverviewPayload, } from './credit-overview-nav-client';
@@ -9,4 +9,6 @@ var creditNavButton = require('./credit-nav-button.js');
9
9
 
10
10
  exports.CreditOverviewClient = creditOverviewClient.CreditOverviewClient;
11
11
  exports.CreditOverviewNavClient = creditOverviewNavClient.CreditOverviewNavClient;
12
+ exports.DEFAULT_CREDIT_OVERVIEW_REFRESH_EVENT = creditOverviewNavClient.DEFAULT_CREDIT_OVERVIEW_REFRESH_EVENT;
13
+ exports.dispatchCreditOverviewRefresh = creditOverviewNavClient.dispatchCreditOverviewRefresh;
12
14
  exports.CreditNavButton = creditNavButton.CreditNavButton;
@@ -1,4 +1,4 @@
1
1
  "use client";
2
2
  export { CreditOverviewClient } from './credit-overview-client.mjs';
3
- export { CreditOverviewNavClient } from './credit-overview-nav-client.mjs';
3
+ export { CreditOverviewNavClient, DEFAULT_CREDIT_OVERVIEW_REFRESH_EVENT, dispatchCreditOverviewRefresh } from './credit-overview-nav-client.mjs';
4
4
  export { CreditNavButton } from './credit-nav-button.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windrun-huaiin/third-ui",
3
- "version": "31.3.5",
3
+ "version": "31.3.7",
4
4
  "description": "Third-party integrated UI components for windrun-huaiin projects",
5
5
  "exports": {
6
6
  "./clerk": {
@@ -243,9 +243,9 @@
243
243
  "tslib": "^2.8.1",
244
244
  "unified": "^11.0.5",
245
245
  "zod": "^4.3.6",
246
- "@windrun-huaiin/base-ui": "^31.0.1",
247
246
  "@windrun-huaiin/contracts": "^31.0.0",
248
- "@windrun-huaiin/lib": "^31.0.3"
247
+ "@windrun-huaiin/lib": "^31.0.3",
248
+ "@windrun-huaiin/base-ui": "^31.0.1"
249
249
  },
250
250
  "peerDependencies": {
251
251
  "clsx": "^2.1.1",
@@ -128,6 +128,11 @@ export function CreditOverviewClient({
128
128
  const subscription = data.subscription;
129
129
  const pricingContext = data.pricingContext;
130
130
  const ctaBehaviors = data.ctaBehaviors;
131
+ const billingOptions = pricingContext?.moneyPriceData.billingSwitch.options ?? [];
132
+ const subscriptionBillingEnabled = billingOptions.some(
133
+ (option) => option.key === 'monthly' || option.key === 'yearly',
134
+ );
135
+ const onetimeBillingEnabled = billingOptions.some((option) => option.key === 'onetime');
131
136
  const getModalMoneyPriceData = useCallback(
132
137
  (mode: PricingModalMode) => {
133
138
  if (!pricingContext) {
@@ -383,24 +388,28 @@ export function CreditOverviewClient({
383
388
  <div className="flex justify-center text-3xl font-semibold leading-tight sm:text-4xl">
384
389
  {formatNumber(locale, data.totalBalance)}
385
390
  </div>
386
- <div className="flex-1 flex-col gap-1">
387
- <p className="text-xs text-gray-700 dark:text-slate-100 sm:text-sm">
388
- {translations.subscriptionPeriodLabel}
389
- </p>
390
- <h4 className="text-xl font-semibold sm:text-2xl">
391
- {subscription ? subscription.planName : translations.subscriptionInactive}
392
- </h4>
393
- </div>
394
- <div className="pt-2 sm:pt-0">
395
- <GradientButton
396
- title={subscription ? translations.subscriptionManage : translations.subscribePay}
397
- align="center"
398
- icon={subscription ? <Settings2Icon /> : <BellIcon />}
399
- openInNewTab={false}
400
- className="w-full"
401
- onClick={subscription ? handleManageAction : handleSubscribeAction}
402
- />
403
- </div>
391
+ {subscriptionBillingEnabled ? (
392
+ <>
393
+ <div className="flex-1 flex-col gap-1">
394
+ <p className="text-xs text-gray-700 dark:text-slate-100 sm:text-sm">
395
+ {translations.subscriptionPeriodLabel}
396
+ </p>
397
+ <h4 className="text-xl font-semibold sm:text-2xl">
398
+ {subscription ? subscription.planName : translations.subscriptionInactive}
399
+ </h4>
400
+ </div>
401
+ <div className="pt-2 sm:pt-0">
402
+ <GradientButton
403
+ title={subscription ? translations.subscriptionManage : translations.subscribePay}
404
+ align="center"
405
+ icon={subscription ? <Settings2Icon /> : <BellIcon />}
406
+ openInNewTab={false}
407
+ className="w-full"
408
+ onClick={subscription ? handleManageAction : handleSubscribeAction}
409
+ />
410
+ </div>
411
+ </>
412
+ ) : null}
404
413
  </div>
405
414
  <div className="absolute right-3 top-3 sm:right-6 sm:top-6">
406
415
  <HoverInfo
@@ -482,13 +491,15 @@ export function CreditOverviewClient({
482
491
  {translations.bucketsEmpty}
483
492
  </div>
484
493
  )}
485
- <GradientButton
486
- title={translations.onetimeBuy}
487
- icon={<ShoppingCartIcon />}
488
- align="center"
489
- className="w-full text-sm sm:text-base"
490
- onClick={handleOnetimeAction}
491
- />
494
+ {onetimeBillingEnabled ? (
495
+ <GradientButton
496
+ title={translations.onetimeBuy}
497
+ icon={<ShoppingCartIcon />}
498
+ align="center"
499
+ className="w-full text-sm sm:text-base"
500
+ onClick={handleOnetimeAction}
501
+ />
502
+ ) : null}
492
503
  </section>
493
504
  </section>
494
505
  );
@@ -1,11 +1,13 @@
1
1
  'use client';
2
2
 
3
3
  import { useAuth } from '@clerk/nextjs';
4
- import { useEffect, useState } from 'react';
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
5
  import { CreditNavButton } from './credit-nav-button';
6
6
  import { CreditOverviewClient, type CreditOverviewTranslations } from './credit-overview-client';
7
7
  import type { CreditOverviewData } from './types';
8
8
 
9
+ export const DEFAULT_CREDIT_OVERVIEW_REFRESH_EVENT = 'windrun-huaiin:credit-overview:refresh';
10
+
9
11
  export interface CreditOverviewPayload {
10
12
  data: CreditOverviewData;
11
13
  totalLabel: string;
@@ -15,20 +17,152 @@ export interface CreditOverviewPayload {
15
17
  export interface CreditOverviewNavClientProps {
16
18
  locale: string;
17
19
  endpoint: string;
20
+ /**
21
+ * Billing types enabled by the business app.
22
+ * Defaults are handled by the API/payload builder when omitted.
23
+ */
24
+ enabledBillingTypes?: string[];
25
+ /**
26
+ * Browser event names that should trigger credit overview reload.
27
+ * Business apps can dispatch these events after credit balance changes.
28
+ *
29
+ * Default: ['windrun:credit-overview:refresh']
30
+ * Empty array means event listening is disabled.
31
+ */
32
+ refreshEvents?: string[];
33
+ /**
34
+ * Optional debounce for refresh event bursts.
35
+ * Default: 300ms.
36
+ */
37
+ refreshDebounceMs?: number;
38
+ }
39
+
40
+ function normalizeEnabledBillingTypes(enabledBillingTypes: string[] | undefined) {
41
+ if (!enabledBillingTypes?.length) {
42
+ return [];
43
+ }
44
+
45
+ return Array.from(
46
+ new Set(
47
+ enabledBillingTypes
48
+ .map((billingType) => billingType.trim())
49
+ .filter((billingType) => billingType.length > 0),
50
+ ),
51
+ );
52
+ }
53
+
54
+ function buildEnabledBillingTypesKey(enabledBillingTypes: string[] | undefined) {
55
+ return normalizeEnabledBillingTypes(enabledBillingTypes).join('\n');
18
56
  }
19
57
 
20
- function buildCreditOverviewUrl(endpoint: string, locale: string) {
58
+ function buildCreditOverviewUrl(
59
+ endpoint: string,
60
+ locale: string,
61
+ enabledBillingTypes: string[],
62
+ ) {
21
63
  const url = new URL(endpoint, window.location.origin);
22
64
  url.searchParams.set('locale', locale);
65
+ for (const billingType of enabledBillingTypes) {
66
+ url.searchParams.append('enabledBillingTypes', billingType);
67
+ }
23
68
  return url.toString();
24
69
  }
25
70
 
71
+ function normalizeRefreshEvents(refreshEvents: string[] | undefined) {
72
+ const events = refreshEvents ?? [DEFAULT_CREDIT_OVERVIEW_REFRESH_EVENT];
73
+ return Array.from(
74
+ new Set(
75
+ events
76
+ .map((eventName) => eventName.trim())
77
+ .filter((eventName) => eventName.length > 0),
78
+ ),
79
+ );
80
+ }
81
+
82
+ function buildRefreshEventsKey(refreshEvents: string[] | undefined) {
83
+ return normalizeRefreshEvents(refreshEvents).join('\n');
84
+ }
85
+
86
+ export function dispatchCreditOverviewRefresh(
87
+ eventName = DEFAULT_CREDIT_OVERVIEW_REFRESH_EVENT,
88
+ ) {
89
+ if (typeof window === 'undefined') {
90
+ return;
91
+ }
92
+
93
+ window.dispatchEvent(new CustomEvent(eventName));
94
+ }
95
+
26
96
  export function CreditOverviewNavClient({
27
97
  locale,
28
98
  endpoint,
99
+ enabledBillingTypes,
100
+ refreshEvents,
101
+ refreshDebounceMs = 300,
29
102
  }: CreditOverviewNavClientProps) {
30
103
  const { isLoaded, isSignedIn, userId } = useAuth();
31
104
  const [payload, setPayload] = useState<CreditOverviewPayload | null>(null);
105
+ const abortControllerRef = useRef<AbortController | null>(null);
106
+ const isMountedRef = useRef(false);
107
+ const refreshEventsKey = buildRefreshEventsKey(refreshEvents);
108
+ const normalizedRefreshEvents = useMemo(
109
+ () => refreshEventsKey.split('\n').filter(Boolean),
110
+ [refreshEventsKey],
111
+ );
112
+ const enabledBillingTypesKey = buildEnabledBillingTypesKey(enabledBillingTypes);
113
+ const normalizedEnabledBillingTypes = useMemo(
114
+ () => enabledBillingTypesKey.split('\n').filter(Boolean),
115
+ [enabledBillingTypesKey],
116
+ );
117
+
118
+ useEffect(() => {
119
+ isMountedRef.current = true;
120
+
121
+ return () => {
122
+ isMountedRef.current = false;
123
+ abortControllerRef.current?.abort();
124
+ };
125
+ }, []);
126
+
127
+ const loadCreditOverview = useCallback(async () => {
128
+ abortControllerRef.current?.abort();
129
+
130
+ const controller = new AbortController();
131
+ abortControllerRef.current = controller;
132
+
133
+ try {
134
+ const response = await fetch(buildCreditOverviewUrl(
135
+ endpoint,
136
+ locale,
137
+ normalizedEnabledBillingTypes,
138
+ ), {
139
+ cache: 'no-store',
140
+ credentials: 'same-origin',
141
+ signal: controller.signal,
142
+ });
143
+
144
+ if (!response.ok) {
145
+ if (!controller.signal.aborted && isMountedRef.current) {
146
+ setPayload(null);
147
+ }
148
+ return;
149
+ }
150
+
151
+ const nextPayload = (await response.json()) as CreditOverviewPayload | null;
152
+ if (!controller.signal.aborted && isMountedRef.current) {
153
+ setPayload(nextPayload);
154
+ }
155
+ } catch (error) {
156
+ if (!controller.signal.aborted && isMountedRef.current) {
157
+ setPayload(null);
158
+ console.warn('[CreditOverviewNavClient] Failed to load credit overview', error);
159
+ }
160
+ } finally {
161
+ if (abortControllerRef.current === controller) {
162
+ abortControllerRef.current = null;
163
+ }
164
+ }
165
+ }, [endpoint, locale, normalizedEnabledBillingTypes]);
32
166
 
33
167
  useEffect(() => {
34
168
  if (!isLoaded) {
@@ -36,44 +170,46 @@ export function CreditOverviewNavClient({
36
170
  }
37
171
 
38
172
  if (!isSignedIn) {
173
+ abortControllerRef.current?.abort();
174
+ abortControllerRef.current = null;
39
175
  setPayload(null);
40
176
  return;
41
177
  }
42
178
 
43
- const controller = new AbortController();
179
+ void loadCreditOverview();
180
+ }, [isLoaded, isSignedIn, loadCreditOverview, userId]);
44
181
 
45
- async function loadCreditOverview() {
46
- try {
47
- const response = await fetch(buildCreditOverviewUrl(endpoint, locale), {
48
- credentials: 'same-origin',
49
- signal: controller.signal,
50
- });
51
-
52
- if (!response.ok) {
53
- if (!controller.signal.aborted) {
54
- setPayload(null);
55
- }
56
- return;
57
- }
182
+ useEffect(() => {
183
+ if (!isLoaded || !isSignedIn || normalizedRefreshEvents.length === 0) {
184
+ return;
185
+ }
58
186
 
59
- const nextPayload = (await response.json()) as CreditOverviewPayload | null;
60
- if (!controller.signal.aborted) {
61
- setPayload(nextPayload);
62
- }
63
- } catch (error) {
64
- if (!controller.signal.aborted) {
65
- setPayload(null);
66
- console.warn('[CreditOverviewNavClient] Failed to load credit overview', error);
67
- }
187
+ let debounceTimer: number | null = null;
188
+
189
+ const refresh = () => {
190
+ if (debounceTimer) {
191
+ window.clearTimeout(debounceTimer);
68
192
  }
69
- }
70
193
 
71
- loadCreditOverview();
194
+ debounceTimer = window.setTimeout(() => {
195
+ void loadCreditOverview();
196
+ }, refreshDebounceMs);
197
+ };
198
+
199
+ for (const eventName of normalizedRefreshEvents) {
200
+ window.addEventListener(eventName, refresh);
201
+ }
72
202
 
73
203
  return () => {
74
- controller.abort();
204
+ if (debounceTimer) {
205
+ window.clearTimeout(debounceTimer);
206
+ }
207
+
208
+ for (const eventName of normalizedRefreshEvents) {
209
+ window.removeEventListener(eventName, refresh);
210
+ }
75
211
  };
76
- }, [endpoint, isLoaded, isSignedIn, locale, userId]);
212
+ }, [isLoaded, isSignedIn, loadCreditOverview, normalizedRefreshEvents, refreshDebounceMs]);
77
213
 
78
214
  if (!payload) {
79
215
  return null;
@@ -1,7 +1,11 @@
1
1
  'use client';
2
2
 
3
3
  export { CreditOverviewClient } from './credit-overview-client';
4
- export { CreditOverviewNavClient } from './credit-overview-nav-client';
4
+ export {
5
+ CreditOverviewNavClient,
6
+ DEFAULT_CREDIT_OVERVIEW_REFRESH_EVENT,
7
+ dispatchCreditOverviewRefresh,
8
+ } from './credit-overview-nav-client';
5
9
  export { CreditNavButton } from './credit-nav-button';
6
10
  export type { CreditOverviewTranslations } from './credit-overview-client';
7
11
  export type {