@streamoid/settings 0.13.0 → 0.15.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/dist/index.mjs CHANGED
@@ -7676,6 +7676,7 @@ import {
7676
7676
  useEffect as useEffect18,
7677
7677
  useCallback as useCallback8,
7678
7678
  useMemo as useMemo4,
7679
+ useReducer,
7679
7680
  useRef as useRef10
7680
7681
  } from "react";
7681
7682
  import {
@@ -7826,7 +7827,10 @@ function formatDateRange(date) {
7826
7827
  function ExportDateModal({
7827
7828
  title,
7828
7829
  onClose,
7829
- onExport
7830
+ onExport,
7831
+ busy = false,
7832
+ busyText,
7833
+ notice = null
7830
7834
  }) {
7831
7835
  useOverlayOpen(true, onClose);
7832
7836
  const now = /* @__PURE__ */ new Date();
@@ -7913,6 +7917,7 @@ function ExportDateModal({
7913
7917
  [rangeStart, rangeEnd, isFuture]
7914
7918
  );
7915
7919
  const handleExport = () => {
7920
+ if (busy) return;
7916
7921
  if (rangeStart && rangeEnd) onExport(rangeStart, rangeEnd);
7917
7922
  };
7918
7923
  const getCellVisuals = (cell, idx) => {
@@ -8134,24 +8139,42 @@ function ExportDateModal({
8134
8139
  borderTop: "1px solid var(--alias-border-subtle)"
8135
8140
  },
8136
8141
  children: [
8137
- /* @__PURE__ */ jsx21(
8138
- "p",
8139
- {
8140
- className: "flex-1 min-w-0 truncate",
8141
- style: {
8142
- fontFamily: "var(--font-family-font-family-body)",
8143
- fontSize: "var(--font-size-font-size-md)",
8144
- fontWeight: 400,
8145
- lineHeight: "var(--line-height-line-height-md)",
8146
- color: "var(--alias-text---icons-primary)"
8147
- },
8148
- children: rangeStart && rangeEnd ? `${formatDateRange(rangeStart)} - ${formatDateRange(rangeEnd)}` : rangeStart ? `${formatDateRange(rangeStart)} - Select end date` : "Select date range"
8149
- }
8150
- ),
8142
+ /* @__PURE__ */ jsxs18("div", { className: "flex-1 min-w-0 flex flex-col", children: [
8143
+ /* @__PURE__ */ jsx21(
8144
+ "p",
8145
+ {
8146
+ className: "min-w-0 truncate",
8147
+ style: {
8148
+ fontFamily: "var(--font-family-font-family-body)",
8149
+ fontSize: "var(--font-size-font-size-md)",
8150
+ fontWeight: 400,
8151
+ lineHeight: "var(--line-height-line-height-md)",
8152
+ color: "var(--alias-text---icons-primary)"
8153
+ },
8154
+ "aria-live": "polite",
8155
+ children: busy && busyText ? busyText : rangeStart && rangeEnd ? `${formatDateRange(rangeStart)} - ${formatDateRange(rangeEnd)}` : rangeStart ? `${formatDateRange(rangeStart)} - Select end date` : "Select date range"
8156
+ }
8157
+ ),
8158
+ notice && !busy && /* @__PURE__ */ jsx21(
8159
+ "p",
8160
+ {
8161
+ role: "alert",
8162
+ style: {
8163
+ fontFamily: "var(--font-family-font-family-body)",
8164
+ fontSize: "var(--font-size-font-size-sm)",
8165
+ fontWeight: 400,
8166
+ lineHeight: "var(--line-height-line-height-sm)",
8167
+ color: "var(--alias-text---icons-error)"
8168
+ },
8169
+ children: notice
8170
+ }
8171
+ )
8172
+ ] }),
8151
8173
  /* @__PURE__ */ jsx21(
8152
8174
  "div",
8153
8175
  {
8154
- className: `flex items-center justify-center shrink-0 ${rangeStart && rangeEnd ? "cursor-pointer" : "opacity-50 cursor-not-allowed"}`,
8176
+ className: `flex items-center justify-center shrink-0 ${rangeStart && rangeEnd && !busy ? "cursor-pointer" : "opacity-50 cursor-not-allowed"}`,
8177
+ "aria-disabled": busy || !(rangeStart && rangeEnd),
8155
8178
  style: {
8156
8179
  backgroundColor: "var(--alias-text---icons-primary)",
8157
8180
  borderRadius: "var(--radius-xl)",
@@ -8169,7 +8192,7 @@ function ExportDateModal({
8169
8192
  lineHeight: "var(--line-height-line-height-sm)",
8170
8193
  color: "var(--alias-text---icons-inverse)"
8171
8194
  },
8172
- children: "Export"
8195
+ children: busy ? "Exporting\u2026" : "Export"
8173
8196
  }
8174
8197
  )
8175
8198
  }
@@ -10523,6 +10546,261 @@ function SharedCreditsView() {
10523
10546
  ] });
10524
10547
  }
10525
10548
 
10549
+ // src/entitlement.ts
10550
+ function parseBillingEntitlement(value) {
10551
+ if (!value || typeof value !== "object") return null;
10552
+ const raw = value;
10553
+ if (typeof raw.paid !== "boolean") return null;
10554
+ const source = raw.source === "plan" || raw.source === "pool" ? raw.source : null;
10555
+ const entitlement = { paid: raw.paid, source };
10556
+ if (typeof raw.sponsorWorkspaceId === "string") entitlement.sponsorWorkspaceId = raw.sponsorWorkspaceId;
10557
+ if (typeof raw.sponsorName === "string") entitlement.sponsorName = raw.sponsorName;
10558
+ if (typeof raw.endsAt === "string" || raw.endsAt === null) entitlement.endsAt = raw.endsAt;
10559
+ return entitlement;
10560
+ }
10561
+ function entitlementFromBody(body) {
10562
+ if (!body || typeof body !== "object") return null;
10563
+ return parseBillingEntitlement(body.entitlement);
10564
+ }
10565
+ function entitlementFromFailure(failure) {
10566
+ const seen = /* @__PURE__ */ new Set();
10567
+ const visit = (value, depth) => {
10568
+ if (depth > 4 || value == null || typeof value !== "object" || seen.has(value)) return null;
10569
+ seen.add(value);
10570
+ const found = entitlementFromBody(value);
10571
+ if (found) return found;
10572
+ const error = value;
10573
+ const response = error.response;
10574
+ for (const child of [error.body, error.data, response?.data, error.cause]) {
10575
+ const nested = visit(child, depth + 1);
10576
+ if (nested) return nested;
10577
+ }
10578
+ return null;
10579
+ };
10580
+ return visit(failure, 0);
10581
+ }
10582
+ function isPaidPlan(subscription) {
10583
+ const amount = subscription?.plan?.amount;
10584
+ return amount != null && amount > 0;
10585
+ }
10586
+ function hasPaidFeatures({
10587
+ entitlement,
10588
+ subscription
10589
+ }) {
10590
+ return entitlement ? entitlement.paid : isPaidPlan(subscription);
10591
+ }
10592
+ function isSponsoredEntitlement(entitlement) {
10593
+ return entitlement?.source === "pool" && entitlement.paid;
10594
+ }
10595
+ function sponsoredCreditsText(entitlement) {
10596
+ const sponsor = entitlement.sponsorName?.trim() || "another workspace";
10597
+ const ends = entitlement.endsAt ? ` Sharing ends on ${formatPoolEndsAt(entitlement.endsAt)}.` : "";
10598
+ return `Credits shared by ${sponsor}.${ends}`;
10599
+ }
10600
+ async function loadCurrentPlan(fetchCurrentPlan) {
10601
+ let response;
10602
+ try {
10603
+ response = await fetchCurrentPlan();
10604
+ } catch (failure) {
10605
+ const entitlement2 = entitlementFromFailure(failure);
10606
+ if (!entitlement2) throw failure;
10607
+ return { subscription: null, entitlement: entitlement2, paid: entitlement2.paid };
10608
+ }
10609
+ const subscription = response?.data ?? null;
10610
+ const entitlement = entitlementFromBody(response);
10611
+ return { subscription, entitlement, paid: hasPaidFeatures({ entitlement, subscription }) };
10612
+ }
10613
+
10614
+ // src/usage-logs.ts
10615
+ var USAGE_LOGS_PAGE_SIZE = 50;
10616
+ var USAGE_LOGS_EXPORT_BATCH_SIZE = 100;
10617
+ var USAGE_LOGS_EXPORT_MAX_ROWS = 2e4;
10618
+ var USAGE_LOGS_EXPORT_EXTRA_REQUESTS = 10;
10619
+ function rowsOf(res) {
10620
+ return Array.isArray(res?.data) ? res.data : [];
10621
+ }
10622
+ function sliceUsageLogsPage(all, page, pageSize = USAGE_LOGS_PAGE_SIZE) {
10623
+ const total = all.length;
10624
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
10625
+ const safePage = Math.min(Math.max(1, Math.floor(page) || 1), totalPages);
10626
+ return {
10627
+ rows: all.slice((safePage - 1) * pageSize, safePage * pageSize),
10628
+ page: safePage,
10629
+ totalPages,
10630
+ total,
10631
+ unpaged: all
10632
+ };
10633
+ }
10634
+ function usageLogsPage(res, page, pageSize = USAGE_LOGS_PAGE_SIZE) {
10635
+ const data = rowsOf(res);
10636
+ const pagination = res?.pagination;
10637
+ if (!pagination || !Number.isFinite(pagination.total)) {
10638
+ return sliceUsageLogsPage(data, page, pageSize);
10639
+ }
10640
+ const total = Math.max(0, pagination.total);
10641
+ const limit = pagination.limit > 0 ? pagination.limit : pageSize;
10642
+ const totalPages = Math.max(
10643
+ 1,
10644
+ pagination.totalPages > 0 ? pagination.totalPages : Math.ceil(total / limit)
10645
+ );
10646
+ return {
10647
+ rows: data.slice(0, pageSize),
10648
+ page: pagination.page > 0 ? pagination.page : page,
10649
+ totalPages,
10650
+ total,
10651
+ unpaged: null
10652
+ };
10653
+ }
10654
+ var EMPTY_USAGE_LOGS_PAGE = {
10655
+ rows: [],
10656
+ page: 1,
10657
+ totalPages: 1,
10658
+ total: 0,
10659
+ unpaged: null
10660
+ };
10661
+ var INITIAL_USAGE_LOGS_STATE = {
10662
+ logs: EMPTY_USAGE_LOGS_PAGE,
10663
+ requestedPage: 1,
10664
+ loading: false,
10665
+ error: null
10666
+ };
10667
+ function usageLogsReducer(state, action) {
10668
+ switch (action.type) {
10669
+ case "request":
10670
+ return { ...state, requestedPage: action.page, loading: true, error: null };
10671
+ case "loaded":
10672
+ return {
10673
+ logs: action.logs,
10674
+ requestedPage: action.logs.page,
10675
+ loading: false,
10676
+ error: null
10677
+ };
10678
+ case "failed":
10679
+ return { ...state, loading: false, error: action.error };
10680
+ case "turn": {
10681
+ if (!state.logs.unpaged || state.error) return state;
10682
+ const logs = sliceUsageLogsPage(state.logs.unpaged, action.page);
10683
+ return { ...state, logs, requestedPage: logs.page };
10684
+ }
10685
+ case "reset":
10686
+ return INITIAL_USAGE_LOGS_STATE;
10687
+ }
10688
+ }
10689
+ function usageLogsSpentByOthers(logs, workspaceId) {
10690
+ return (logs.unpaged ?? logs.rows).some(
10691
+ (log) => usedByWorkspaceKey(log, workspaceId) !== null
10692
+ );
10693
+ }
10694
+ function createUsedByNamesLoader(save) {
10695
+ let savedFor = null;
10696
+ let inFlight = null;
10697
+ return {
10698
+ async load(workspaceId, fetchCreditPool) {
10699
+ if (savedFor === workspaceId || inFlight?.workspaceId === workspaceId) {
10700
+ return;
10701
+ }
10702
+ const lookup = { workspaceId };
10703
+ inFlight = lookup;
10704
+ try {
10705
+ const pool = await fetchCreditPool();
10706
+ if (inFlight !== lookup) return;
10707
+ if (save(workspaceId, usedByNamesFrom(pool))) savedFor = workspaceId;
10708
+ } catch {
10709
+ } finally {
10710
+ if (inFlight === lookup) inFlight = null;
10711
+ }
10712
+ },
10713
+ reset() {
10714
+ savedFor = null;
10715
+ inFlight = null;
10716
+ }
10717
+ };
10718
+ }
10719
+ function usageLogsExportFileName(start, end) {
10720
+ return `usage_logs_${toLocalDateInput(start)}_${toLocalDateInput(end)}.csv`;
10721
+ }
10722
+ function usageLogsDateRange(start, end) {
10723
+ const from = new Date(start);
10724
+ from.setHours(0, 0, 0, 0);
10725
+ const to = new Date(end);
10726
+ to.setHours(23, 59, 59, 999);
10727
+ return { startDate: from.toISOString(), endDate: to.toISOString() };
10728
+ }
10729
+ async function fetchUsageLogsInRange(fetchUsageLogs, range, {
10730
+ batchSize = USAGE_LOGS_EXPORT_BATCH_SIZE,
10731
+ maxRows = USAGE_LOGS_EXPORT_MAX_ROWS,
10732
+ maxRequests,
10733
+ onProgress,
10734
+ signal
10735
+ } = {}) {
10736
+ const first = await fetchUsageLogs({ ...range, page: 1, limit: batchSize });
10737
+ if (signal?.aborted) return { status: "cancelled" };
10738
+ if (!first?.pagination || !Number.isFinite(first.pagination.total)) {
10739
+ const from = Date.parse(range.startDate);
10740
+ const to = Date.parse(range.endDate);
10741
+ const rows2 = rowsOf(first).filter((row) => {
10742
+ const at = Date.parse(row.createdAt);
10743
+ return at >= from && at <= to;
10744
+ });
10745
+ if (rows2.length > maxRows) return { status: "too-large", total: rows2.length };
10746
+ onProgress?.(rows2.length, rows2.length);
10747
+ return { status: "ok", rows: rows2, total: rows2.length };
10748
+ }
10749
+ let total = Math.max(0, first.pagination.total);
10750
+ if (total > maxRows) return { status: "too-large", total };
10751
+ const limit = first.pagination.limit > 0 ? first.pagination.limit : batchSize;
10752
+ const requestCap = maxRequests ?? Math.ceil(maxRows / limit) + USAGE_LOGS_EXPORT_EXTRA_REQUESTS;
10753
+ const seen = /* @__PURE__ */ new Set();
10754
+ const rows = [];
10755
+ const add = (batch) => {
10756
+ for (const row of batch) {
10757
+ if (rows.length >= maxRows) return;
10758
+ if (seen.has(row._id)) continue;
10759
+ seen.add(row._id);
10760
+ rows.push(row);
10761
+ }
10762
+ };
10763
+ const hasNextAfter = (pagination, page) => typeof pagination?.hasNextPage === "boolean" ? pagination.hasNextPage : page * limit < total;
10764
+ add(rowsOf(first));
10765
+ onProgress?.(rows.length, total);
10766
+ let hasNext = hasNextAfter(first.pagination, 1);
10767
+ let requests = 1;
10768
+ for (let page = 2; hasNext && rows.length < total; page++) {
10769
+ if (requests >= requestCap) {
10770
+ return { status: "incomplete", fetched: rows.length, total };
10771
+ }
10772
+ if (signal?.aborted) return { status: "cancelled" };
10773
+ const res = await fetchUsageLogs({ ...range, page, limit });
10774
+ requests++;
10775
+ if (signal?.aborted) return { status: "cancelled" };
10776
+ if (res?.pagination && Number.isFinite(res.pagination.total)) {
10777
+ total = Math.max(0, res.pagination.total);
10778
+ if (total > maxRows) return { status: "too-large", total };
10779
+ }
10780
+ const batch = rowsOf(res);
10781
+ if (batch.length === 0) break;
10782
+ add(batch);
10783
+ onProgress?.(rows.length, total);
10784
+ hasNext = hasNextAfter(res?.pagination, page);
10785
+ }
10786
+ return { status: "ok", rows, total };
10787
+ }
10788
+ function usageLogsCsvRows(logs, userMap, formatDate5) {
10789
+ return [
10790
+ ["Date", "Type", "Credits", "Service", "User ID"],
10791
+ ...logs.map((log) => [
10792
+ formatDate5(log.createdAt),
10793
+ log.type,
10794
+ log.amount.toLocaleString("en-US"),
10795
+ log.service?.serviceGroupName ?? log.meta?.info ?? "",
10796
+ log.triggeredByUserId != null ? userMap[log.triggeredByUserId] ?? log.triggeredByUserId : ""
10797
+ ])
10798
+ ];
10799
+ }
10800
+ function toCsv(rows) {
10801
+ return rows.map((r) => r.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(",")).join("\n");
10802
+ }
10803
+
10526
10804
  // src/billing-content.tsx
10527
10805
  import { Fragment as Fragment11, jsx as jsx29, jsxs as jsxs26 } from "react/jsx-runtime";
10528
10806
  var DEFAULT_TOPUP_SERVICE = "catalogix";
@@ -11408,6 +11686,8 @@ function UsageLogsTable({
11408
11686
  logs,
11409
11687
  userMap,
11410
11688
  loading,
11689
+ error = null,
11690
+ onRetry,
11411
11691
  currentWorkspaceId = null,
11412
11692
  usedByNames = {}
11413
11693
  }) {
@@ -11438,6 +11718,50 @@ function UsageLogsTable({
11438
11718
  }
11439
11719
  );
11440
11720
  }
11721
+ if (error) {
11722
+ return /* @__PURE__ */ jsxs26(
11723
+ "div",
11724
+ {
11725
+ role: "alert",
11726
+ className: "flex flex-col items-center justify-center w-full",
11727
+ style: {
11728
+ backgroundColor: "var(--alias-surface-base)",
11729
+ borderRadius: "var(--radius-3xl)",
11730
+ border: "1px solid var(--alias-border-subtle)",
11731
+ padding: "var(--spacing-6xl)",
11732
+ gap: "var(--spacing-md)"
11733
+ },
11734
+ children: [
11735
+ /* @__PURE__ */ jsxs26(
11736
+ "span",
11737
+ {
11738
+ style: {
11739
+ fontFamily: "var(--font-family-font-family-body)",
11740
+ fontSize: "var(--font-size-font-size-sm)",
11741
+ fontWeight: 400,
11742
+ lineHeight: "var(--line-height-line-height-sm)",
11743
+ color: "var(--alias-text---icons-muted)",
11744
+ textAlign: "center"
11745
+ },
11746
+ children: [
11747
+ "Couldn't load usage logs. ",
11748
+ error
11749
+ ]
11750
+ }
11751
+ ),
11752
+ onRetry && /* @__PURE__ */ jsx29(
11753
+ ScButton14,
11754
+ {
11755
+ text: "Try again",
11756
+ variant: "tertiary",
11757
+ size: "sm",
11758
+ onClick: onRetry
11759
+ }
11760
+ )
11761
+ ]
11762
+ }
11763
+ );
11764
+ }
11441
11765
  return /* @__PURE__ */ jsxs26(
11442
11766
  "div",
11443
11767
  {
@@ -11613,6 +11937,71 @@ function UsageLogsTable({
11613
11937
  }
11614
11938
  );
11615
11939
  }
11940
+ function UsageLogsPager({
11941
+ page,
11942
+ totalPages,
11943
+ total,
11944
+ loading = false,
11945
+ onPageChange
11946
+ }) {
11947
+ if (total <= 0) return null;
11948
+ const canPrevious = !loading && page > 1;
11949
+ const canNext = !loading && page < totalPages;
11950
+ return /* @__PURE__ */ jsxs26(
11951
+ "div",
11952
+ {
11953
+ className: "flex items-center justify-between shrink-0 w-full",
11954
+ style: { gap: "var(--spacing-md)" },
11955
+ children: [
11956
+ /* @__PURE__ */ jsxs26(
11957
+ "span",
11958
+ {
11959
+ style: {
11960
+ color: "var(--alias-text---icons-muted)",
11961
+ fontFamily: "var(--font-family-font-family-body)",
11962
+ fontSize: "var(--font-size-font-size-sm)",
11963
+ lineHeight: "var(--line-height-line-height-sm)"
11964
+ },
11965
+ children: [
11966
+ "Page ",
11967
+ page.toLocaleString("en-US"),
11968
+ " of",
11969
+ " ",
11970
+ totalPages.toLocaleString("en-US"),
11971
+ " \xB7",
11972
+ " ",
11973
+ total.toLocaleString("en-US"),
11974
+ " ",
11975
+ total === 1 ? "log" : "logs"
11976
+ ]
11977
+ }
11978
+ ),
11979
+ totalPages > 1 && /* @__PURE__ */ jsxs26("div", { className: "flex items-center", style: { gap: "var(--spacing-md)" }, children: [
11980
+ /* @__PURE__ */ jsx29(
11981
+ ScButton14,
11982
+ {
11983
+ text: "Previous",
11984
+ variant: "tertiary",
11985
+ size: "sm",
11986
+ state: canPrevious ? "default" : "disabled",
11987
+ onClick: canPrevious ? () => onPageChange(page - 1) : void 0
11988
+ }
11989
+ ),
11990
+ /* @__PURE__ */ jsx29(
11991
+ ScButton14,
11992
+ {
11993
+ text: "Next",
11994
+ variant: "tertiary",
11995
+ size: "sm",
11996
+ state: canNext ? "default" : "disabled",
11997
+ onClick: canNext ? () => onPageChange(page + 1) : void 0
11998
+ }
11999
+ )
12000
+ ] })
12001
+ ]
12002
+ }
12003
+ );
12004
+ }
11616
12005
  function BackToBilling({ onClick }) {
11617
12006
  return /* @__PURE__ */ jsxs26(
11618
12007
  "div",
@@ -11654,7 +12043,8 @@ function BillingContent({
11654
12043
  showPerUserLimits: showPerUserLimitsProp,
11655
12044
  onShowPerUserLimitsChange,
11656
12045
  showSharedCredits: showSharedCreditsProp,
11657
- onShowSharedCreditsChange
12046
+ onShowSharedCreditsChange,
12047
+ onEntitlementChange
11658
12048
  } = {}) {
11659
12049
  const { auth, workspace, organization, api, toast, navigation } = useSettingsAdapters();
11660
12050
  const user = auth.user;
@@ -11691,15 +12081,38 @@ function BillingContent({
11691
12081
  else setShowSharedCreditsInternal(next);
11692
12082
  };
11693
12083
  const [showExportModal, setShowExportModal] = useState21(false);
12084
+ const [exportProgress, setExportProgress] = useState21(null);
12085
+ const [exportNotice, setExportNotice] = useState21(null);
12086
+ const exportAbortRef = useRef10(null);
11694
12087
  const [showBuyCreditsModal, setShowBuyCreditsModal] = useState21(false);
11695
12088
  const [subscription, setSubscription] = useState21(null);
12089
+ const [entitlement, setEntitlement] = useState21(
12090
+ null
12091
+ );
11696
12092
  const [availableCredits, setAvailableCredits] = useState21(null);
11697
12093
  const [sharedPool, setSharedPool] = useState21(null);
11698
12094
  const [usedByNames, setUsedByNames] = useState21({});
11699
12095
  const [products, setProducts] = useState21([]);
11700
12096
  const [invoices, setInvoices] = useState21([]);
11701
12097
  const [totalCredits, setTotalCredits] = useState21(null);
11702
- const [usageLogs, setUsageLogs] = useState21([]);
12098
+ const [usageLogsState, dispatchUsageLogs] = useReducer(
12099
+ usageLogsReducer,
12100
+ INITIAL_USAGE_LOGS_STATE
12101
+ );
12102
+ const {
12103
+ logs: usageLogs,
12104
+ loading: loadingUsageLogs,
12105
+ error: usageLogsError
12106
+ } = usageLogsState;
12107
+ const usageLogsRequestRef = useRef10(0);
12108
+ const workspaceIdRef = useRef10(workspaceId);
12109
+ workspaceIdRef.current = workspaceId;
12110
+ const loadUsedByNamesRef = useRef10(null);
12111
+ loadUsedByNamesRef.current ??= createUsedByNamesLoader((forWorkspace, names) => {
12112
+ if (forWorkspace !== workspaceIdRef.current) return false;
12113
+ setUsedByNames(names);
12114
+ return true;
12115
+ });
11703
12116
  const [userMap, setUserMap] = useState21({});
11704
12117
  const [customPlanCredits, setCustomPlanCredits] = useState21(
11705
12118
  null
@@ -11738,12 +12151,15 @@ function BillingContent({
11738
12151
  try {
11739
12152
  console.log("calling Billing apis");
11740
12153
  const [subRes, creditsRes, productsRes, customRes] = await Promise.all([
11741
- api.fetchCurrentPlan().catch(() => null),
12154
+ // A sponsored workspace with no subscription of its own gets a 404
12155
+ // that carries its entitlement: loadCurrentPlan answers it, not throws.
12156
+ loadCurrentPlan(api.fetchCurrentPlan).catch(() => null),
11742
12157
  api.fetchAvailableCredits().catch(() => null),
11743
12158
  api.fetchPlans().catch(() => null),
11744
12159
  api.getCustomCreditInfo?.().catch(() => null) ?? Promise.resolve(null)
11745
12160
  ]);
11746
- if (subRes?.data) setSubscription(subRes.data);
12161
+ if (subRes?.subscription) setSubscription(subRes.subscription);
12162
+ if (subRes) setEntitlement(subRes.entitlement);
11747
12163
  if (creditsRes?.availableCredits != null)
11748
12164
  setAvailableCredits(creditsRes.availableCredits);
11749
12165
  setSharedPool(creditsRes?.sharedPool ?? null);
@@ -11780,8 +12196,68 @@ function BillingContent({
11780
12196
  } catch {
11781
12197
  }
11782
12198
  }, [api]);
12199
+ const ensureUsedByNames = useCallback8(
12200
+ (logs) => {
12201
+ const fetchCreditPool = api.fetchCreditPool;
12202
+ if (!fetchCreditPool || !workspaceId || !usageLogsSpentByOthers(logs, workspaceId)) {
12203
+ return;
12204
+ }
12205
+ void loadUsedByNamesRef.current?.load(
12206
+ workspaceId,
12207
+ () => fetchCreditPool.call(api)
12208
+ );
12209
+ },
12210
+ [api, workspaceId]
12211
+ );
12212
+ const loadUsageLogsPage = useCallback8(
12213
+ async (page) => {
12214
+ const request = ++usageLogsRequestRef.current;
12215
+ if (!api.fetchUsageLogs) {
12216
+ dispatchUsageLogs({ type: "loaded", logs: EMPTY_USAGE_LOGS_PAGE });
12217
+ return;
12218
+ }
12219
+ dispatchUsageLogs({ type: "request", page });
12220
+ try {
12221
+ const res = await api.fetchUsageLogs({
12222
+ page,
12223
+ limit: USAGE_LOGS_PAGE_SIZE
12224
+ });
12225
+ if (request !== usageLogsRequestRef.current) return;
12226
+ const next = usageLogsPage(res, page);
12227
+ dispatchUsageLogs({ type: "loaded", logs: next });
12228
+ ensureUsedByNames(next);
12229
+ } catch (err) {
12230
+ if (request !== usageLogsRequestRef.current) return;
12231
+ dispatchUsageLogs({
12232
+ type: "failed",
12233
+ error: errorMessage(err, { method: "GET" })
12234
+ });
12235
+ }
12236
+ },
12237
+ [api, ensureUsedByNames]
12238
+ );
12239
+ const goToUsageLogsPage = useCallback8(
12240
+ (page) => {
12241
+ if (usageLogs.unpaged && !usageLogsError) {
12242
+ dispatchUsageLogs({ type: "turn", page });
12243
+ ensureUsedByNames(usageLogs);
12244
+ return;
12245
+ }
12246
+ void loadUsageLogsPage(page);
12247
+ },
12248
+ [usageLogs, usageLogsError, loadUsageLogsPage, ensureUsedByNames]
12249
+ );
12250
+ useEffect18(() => {
12251
+ if (!loadingUsageLogs && !usageLogsError && usageLogs.unpaged === null && usageLogs.rows.length === 0 && usageLogs.total > 0 && usageLogs.page > usageLogs.totalPages) {
12252
+ void loadUsageLogsPage(usageLogs.totalPages);
12253
+ }
12254
+ }, [usageLogs, loadingUsageLogs, usageLogsError, loadUsageLogsPage]);
11783
12255
  const fetchTableData = useCallback8(
11784
12256
  async (tab) => {
12257
+ if (tab === "usage") {
12258
+ await loadUsageLogsPage(1);
12259
+ return;
12260
+ }
11785
12261
  setLoadingTable(true);
11786
12262
  try {
11787
12263
  if (tab === "invoice") {
@@ -11793,25 +12269,14 @@ function BillingContent({
11793
12269
  (item) => item.expiresAt && item.expiresAt.split("T")[0] > today
11794
12270
  ).reduce((total, item) => total + item.amount, 0)
11795
12271
  );
11796
- } else if (api.fetchUsageLogs) {
11797
- const res = await api.fetchUsageLogs();
11798
- const logs = res.data?.length ? res.data : [];
11799
- setUsageLogs(logs);
11800
- if (api.fetchCreditPool && logs.some((log) => usedByWorkspaceKey(log, workspaceId))) {
11801
- const pool = await api.fetchCreditPool().catch(() => null);
11802
- setUsedByNames(usedByNamesFrom(pool));
11803
- }
11804
- } else {
11805
- setUsageLogs([]);
11806
12272
  }
11807
12273
  } catch {
11808
- if (tab === "invoice") setInvoices([]);
11809
- else setUsageLogs([]);
12274
+ setInvoices([]);
11810
12275
  } finally {
11811
12276
  setLoadingTable(false);
11812
12277
  }
11813
12278
  },
11814
- [api, workspaceId]
12279
+ [api, loadUsageLogsPage]
11815
12280
  );
11816
12281
  const handlePlanAction = useCallback8(
11817
12282
  async ({ productId, priceId, planName: planName2 }) => {
@@ -11913,10 +12378,15 @@ function BillingContent({
11913
12378
  setInitialFetchTriggered(false);
11914
12379
  setBillingOverviewLoaded(false);
11915
12380
  setSubscription(null);
12381
+ setEntitlement(null);
11916
12382
  setAvailableCredits(null);
11917
12383
  setProducts([]);
11918
12384
  setTotalCredits(null);
11919
12385
  setCustomPlanCredits(null);
12386
+ usageLogsRequestRef.current++;
12387
+ dispatchUsageLogs({ type: "reset" });
12388
+ loadUsedByNamesRef.current?.reset();
12389
+ setUsedByNames({});
11920
12390
  }, [workspaceId]);
11921
12391
  useEffect18(() => {
11922
12392
  setInitialFetchTriggered(true);
@@ -11968,7 +12438,16 @@ function BillingContent({
11968
12438
  subscription?.scheduledDowngradeEffectiveDate
11969
12439
  );
11970
12440
  const scheduledDowngradeText = subscription?.downgradeScheduled && subscription.plan?.name ? `Downgrades to ${subscription.plan.name}${scheduledDowngradeDateLabel ? ` ${scheduledDowngradeDateLabel}` : ""}` : void 0;
11971
- const isPlanDetailsLoading = !billingOverviewLoaded || loadingBilling || subscription?.plan == null || !planName || planName === "-" || !planAmount || currentPlanCredits == null;
12441
+ const sponsored = isSponsoredEntitlement(entitlement);
12442
+ const sponsoredWithoutPlan = sponsored && billingOverviewLoaded && !loadingBilling && subscription == null;
12443
+ const isPlanDetailsLoading = sponsoredWithoutPlan ? false : !billingOverviewLoaded || loadingBilling || subscription?.plan == null || !planName || planName === "-" || !planAmount || currentPlanCredits == null;
12444
+ const paidFeatures = hasPaidFeatures({ entitlement, subscription });
12445
+ const onEntitlementChangeRef = useRef10(onEntitlementChange);
12446
+ onEntitlementChangeRef.current = onEntitlementChange;
12447
+ useEffect18(() => {
12448
+ if (!billingOverviewLoaded) return;
12449
+ onEntitlementChangeRef.current?.({ entitlement, paid: paidFeatures });
12450
+ }, [billingOverviewLoaded, entitlement, paidFeatures]);
11972
12451
  const {
11973
12452
  used: usedCredits,
11974
12453
  remaining: remainingCredits,
@@ -12010,35 +12489,74 @@ function BillingContent({
12010
12489
  toast.success(`Opened ${successCount} invoice PDF(s).`);
12011
12490
  }
12012
12491
  } else {
12013
- const filtered = usageLogs.filter((log) => {
12014
- const d = new Date(log.createdAt);
12015
- return d >= startDate && d <= end;
12492
+ await exportUsageLogs(startDate, endDate);
12493
+ }
12494
+ };
12495
+ const closeExportModal = () => {
12496
+ exportAbortRef.current?.abort();
12497
+ exportAbortRef.current = null;
12498
+ setExportProgress(null);
12499
+ setExportNotice(null);
12500
+ setShowExportModal(false);
12501
+ };
12502
+ useEffect18(() => () => exportAbortRef.current?.abort(), []);
12503
+ const exportUsageLogs = async (startDate, endDate) => {
12504
+ if (!api.fetchUsageLogs) {
12505
+ toast.info?.("Usage logs are not available in this workspace.");
12506
+ closeExportModal();
12507
+ return;
12508
+ }
12509
+ exportAbortRef.current?.abort();
12510
+ const controller = new AbortController();
12511
+ exportAbortRef.current = controller;
12512
+ setExportNotice(null);
12513
+ setExportProgress({ fetched: 0, total: 0 });
12514
+ const range = usageLogsDateRange(startDate, endDate);
12515
+ try {
12516
+ const result = await fetchUsageLogsInRange(api.fetchUsageLogs, range, {
12517
+ signal: controller.signal,
12518
+ onProgress: (fetched, total) => {
12519
+ if (!controller.signal.aborted) setExportProgress({ fetched, total });
12520
+ }
12016
12521
  });
12017
- if (filtered.length === 0) {
12522
+ if (result.status === "cancelled" || controller.signal.aborted) return;
12523
+ if (result.status === "too-large") {
12524
+ setExportNotice(
12525
+ `${result.total.toLocaleString("en-US")} logs in this range; an export holds up to ${USAGE_LOGS_EXPORT_MAX_ROWS.toLocaleString("en-US")}. Choose a shorter range.`
12526
+ );
12527
+ return;
12528
+ }
12529
+ if (result.status === "incomplete") {
12530
+ setExportNotice(
12531
+ "Usage logs kept changing while exporting, so some were missed. Try again."
12532
+ );
12533
+ return;
12534
+ }
12535
+ if (result.rows.length === 0) {
12018
12536
  toast.info?.("No usage logs found in the selected date range.");
12019
- setShowExportModal(false);
12537
+ closeExportModal();
12020
12538
  return;
12021
12539
  }
12022
- const headers = ["Date", "Type", "Credits", "Service", "User ID"];
12023
- const rows = filtered.map((log) => [
12024
- formatDate3(log.createdAt),
12025
- log.type,
12026
- log.amount.toLocaleString("en-US"),
12027
- log.service?.serviceGroupName ?? log.meta?.info ?? "",
12028
- log.triggeredByUserId != null ? userMap[log.triggeredByUserId] ?? log.triggeredByUserId : ""
12029
- ]);
12030
12540
  downloadCsv(
12031
- [headers, ...rows],
12032
- `usage_logs_${fmtFilename(startDate)}_${fmtFilename(end)}.csv`
12541
+ usageLogsCsvRows(result.rows, userMap, formatDate3),
12542
+ usageLogsExportFileName(startDate, endDate)
12033
12543
  );
12034
- toast.success(`Exported ${filtered.length} log(s).`);
12035
- setShowExportModal(false);
12544
+ toast.success(`Exported ${result.rows.length.toLocaleString("en-US")} log(s).`);
12545
+ closeExportModal();
12546
+ } catch (err) {
12547
+ if (controller.signal.aborted) return;
12548
+ setExportNotice(
12549
+ `Couldn't export usage logs. ${errorMessage(err, { method: "GET" })}`
12550
+ );
12551
+ } finally {
12552
+ if (exportAbortRef.current === controller) {
12553
+ exportAbortRef.current = null;
12554
+ setExportProgress(null);
12555
+ }
12036
12556
  }
12037
12557
  };
12038
12558
  function downloadCsv(rows, filename) {
12039
- const csv = rows.map(
12040
- (r) => r.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(",")
12041
- ).join("\n");
12559
+ const csv = toCsv(rows);
12042
12560
  const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
12043
12561
  const url = URL.createObjectURL(blob);
12044
12562
  const a = document.createElement("a");
@@ -12049,9 +12567,6 @@ function BillingContent({
12049
12567
  document.body.removeChild(a);
12050
12568
  URL.revokeObjectURL(url);
12051
12569
  }
12052
- function fmtFilename(d) {
12053
- return d.toISOString().slice(0, 10);
12054
- }
12055
12570
  async function handleBuyCredits(credits2, price) {
12056
12571
  if (!workspaceId) {
12057
12572
  toast.error("Missing workspace details for credit top-up");
@@ -12205,9 +12720,9 @@ function BillingContent({
12205
12720
  /* @__PURE__ */ jsx29(
12206
12721
  ScPlanDetailsCard,
12207
12722
  {
12208
- planName,
12209
- planCredits: planCreditsText,
12210
- planPrice: planAmount,
12723
+ planName: sponsoredWithoutPlan ? "Free" : planName,
12724
+ planCredits: sponsoredWithoutPlan ? "Shared credits" : planCreditsText,
12725
+ planPrice: sponsoredWithoutPlan ? `${workspace.current?.country?.trim().toUpperCase() === "IN" ? "\u20B9" : "$"}0.00 / month` : planAmount,
12211
12726
  badgeText: "Active",
12212
12727
  badgeSubText: scheduledDowngradeText,
12213
12728
  currentPlanLabel: "Current plan",
@@ -12296,6 +12811,50 @@ function BillingContent({
12296
12811
  )
12297
12812
  ]
12298
12813
  }
12814
+ ) : sponsored && entitlement ? (
12815
+ // Billing says a pool entitles it, but the balance carried no
12816
+ // allowance (an older credits answer, or it failed): say whose.
12817
+ /* @__PURE__ */ jsxs26(
12818
+ "div",
12819
+ {
12820
+ role: "status",
12821
+ className: "flex flex-col shrink-0 w-full",
12822
+ style: {
12823
+ gap: 4,
12824
+ padding: 16,
12825
+ backgroundColor: "var(--alias-surface-base)",
12826
+ border: "1px solid var(--alias-border-subtle)",
12827
+ borderRadius: "var(--radius-3xl)"
12828
+ },
12829
+ children: [
12830
+ /* @__PURE__ */ jsx29(
12831
+ "span",
12832
+ {
12833
+ style: {
12834
+ fontFamily: "var(--font-family-font-family-body)",
12835
+ fontSize: "var(--font-size-font-size-sm)",
12836
+ fontWeight: 500,
12837
+ lineHeight: "var(--line-height-line-height-sm)",
12838
+ color: "var(--alias-text---icons-primary)"
12839
+ },
12840
+ children: sponsoredCreditsText(entitlement)
12841
+ }
12842
+ ),
12843
+ /* @__PURE__ */ jsx29(
12844
+ "span",
12845
+ {
12846
+ style: {
12847
+ fontFamily: "var(--font-family-font-family-body)",
12848
+ fontSize: "var(--font-size-font-size-sm)",
12849
+ lineHeight: "var(--line-height-line-height-sm)",
12850
+ color: "var(--alias-text---icons-muted)"
12851
+ },
12852
+ children: ownCreditsFirstText()
12853
+ }
12854
+ )
12855
+ ]
12856
+ }
12857
+ )
12299
12858
  ) : null,
12300
12859
  usage ? /* @__PURE__ */ jsxs26(
12301
12860
  "div",
@@ -12460,22 +13019,39 @@ function BillingContent({
12460
13019
  loading: loadingTable,
12461
13020
  workspaceCountry: workspace.current?.country
12462
13021
  }
12463
- ) : /* @__PURE__ */ jsx29(
12464
- UsageLogsTable,
12465
- {
12466
- logs: usageLogs,
12467
- userMap,
12468
- loading: loadingTable,
12469
- currentWorkspaceId: workspaceId,
12470
- usedByNames
12471
- }
12472
- ),
13022
+ ) : /* @__PURE__ */ jsxs26(Fragment11, { children: [
13023
+ /* @__PURE__ */ jsx29(
13024
+ UsageLogsTable,
13025
+ {
13026
+ logs: usageLogs.rows,
13027
+ userMap,
13028
+ loading: loadingUsageLogs,
13029
+ error: usageLogsError,
13030
+ onRetry: () => void loadUsageLogsPage(usageLogsState.requestedPage),
13031
+ currentWorkspaceId: workspaceId,
13032
+ usedByNames
13033
+ }
13034
+ ),
13035
+ !usageLogsError && /* @__PURE__ */ jsx29(
13036
+ UsageLogsPager,
13037
+ {
13038
+ page: usageLogs.page,
13039
+ totalPages: usageLogs.totalPages,
13040
+ total: usageLogs.total,
13041
+ loading: loadingUsageLogs,
13042
+ onPageChange: goToUsageLogsPage
13043
+ }
13044
+ )
13045
+ ] }),
12473
13046
  showExportModal && /* @__PURE__ */ jsx29(
12474
13047
  ExportDateModal,
12475
13048
  {
12476
13049
  title: activeTab === "invoice" ? "Export invoices" : "Export log",
12477
- onClose: () => setShowExportModal(false),
12478
- onExport: handleExport
13050
+ onClose: closeExportModal,
13051
+ onExport: handleExport,
13052
+ busy: exportProgress !== null,
13053
+ busyText: exportProgress && exportProgress.total > 0 ? `Fetched ${exportProgress.fetched.toLocaleString("en-US")} of ${exportProgress.total.toLocaleString("en-US")} logs\u2026` : "Fetching logs\u2026",
13054
+ notice: exportNotice
12479
13055
  }
12480
13056
  ),
12481
13057
  showBuyCreditsModal && /* @__PURE__ */ jsx29(
@@ -16676,6 +17252,30 @@ function OrganizationPicker({
16676
17252
  );
16677
17253
  }
16678
17254
 
17255
+ // src/use-billing-entitlement.ts
17256
+ import { useEffect as useEffect22, useRef as useRef12, useState as useState28 } from "react";
17257
+ function useBillingEntitlement(fetchCurrentPlan, key) {
17258
+ const fetchRef = useRef12(fetchCurrentPlan);
17259
+ fetchRef.current = fetchCurrentPlan;
17260
+ const [state, setState] = useState28(null);
17261
+ const [loading, setLoading] = useState28(false);
17262
+ useEffect22(() => {
17263
+ const fetch2 = fetchRef.current;
17264
+ setState(null);
17265
+ if (!key || !fetch2) {
17266
+ setLoading(false);
17267
+ return;
17268
+ }
17269
+ let live = true;
17270
+ setLoading(true);
17271
+ loadCurrentPlan(fetch2).then((next) => live && setState(next)).catch(() => live && setState(null)).finally(() => live && setLoading(false));
17272
+ return () => {
17273
+ live = false;
17274
+ };
17275
+ }, [key]);
17276
+ return { state, loading };
17277
+ }
17278
+
16679
17279
  // src/administration-navigation.ts
16680
17280
  var normalizedBase = (base) => base.replace(/\/$/, "");
16681
17281
  var returnKey = (base, entryKey) => `streamoid:settings:return:${base}:${entryKey}`;
@@ -16822,7 +17422,9 @@ export {
16822
17422
  CreditPoolMemberModal,
16823
17423
  DEFAULT_PERMISSIONS,
16824
17424
  DEFAULT_USER_LANGUAGE,
17425
+ EMPTY_USAGE_LOGS_PAGE,
16825
17426
  ExportDateModal,
17427
+ INITIAL_USAGE_LOGS_STATE,
16826
17428
  InviteUpdateModal,
16827
17429
  MemberCreditLimitModal,
16828
17430
  MemberListFrame,
@@ -16855,7 +17457,12 @@ export {
16855
17457
  TeamsContent,
16856
17458
  TokensContent,
16857
17459
  URL_TO_TAB,
17460
+ USAGE_LOGS_EXPORT_BATCH_SIZE,
17461
+ USAGE_LOGS_EXPORT_EXTRA_REQUESTS,
17462
+ USAGE_LOGS_EXPORT_MAX_ROWS,
17463
+ USAGE_LOGS_PAGE_SIZE,
16858
17464
  USER_LANGUAGES,
17465
+ UsageLogsPager,
16859
17466
  WORKSPACE_LIST_PAGE_SIZE,
16860
17467
  WorkspaceGeneralTab,
16861
17468
  WorkspaceListTab,
@@ -16867,13 +17474,17 @@ export {
16867
17474
  cleanCreditLimitInput,
16868
17475
  cleanPoolValueInput,
16869
17476
  countLabel,
17477
+ createUsedByNamesLoader,
16870
17478
  creditCycleUnit,
16871
17479
  creditsMeter,
16872
17480
  deriveWorkspaceInvitePermission,
16873
17481
  describeAllowance,
16874
17482
  describeDefaultLimit,
16875
17483
  describeMemberLimit,
17484
+ entitlementFromBody,
17485
+ entitlementFromFailure,
16876
17486
  expiryChoiceFor,
17487
+ fetchUsageLogsInRange,
16877
17488
  findBillingError,
16878
17489
  findPoolError,
16879
17490
  formatCreditLimit,
@@ -16881,16 +17492,21 @@ export {
16881
17492
  formatPoolEndsAt,
16882
17493
  formatPoolSplit,
16883
17494
  groupWorkspaceList,
17495
+ hasPaidFeatures,
16884
17496
  isLimitBelowUsage,
17497
+ isPaidPlan,
17498
+ isSponsoredEntitlement,
16885
17499
  joinMembersWithLimits,
16886
17500
  lastUsedLabel,
16887
17501
  limitBelowUsageUsed,
16888
17502
  limitSourceLabel,
17503
+ loadCurrentPlan,
16889
17504
  loadWorkspaceOrganization,
16890
17505
  memberRoleUpdate,
16891
17506
  minPoolEndDate,
16892
17507
  minimumCreditLimit,
16893
17508
  ownCreditsFirstText,
17509
+ parseBillingEntitlement,
16894
17510
  pausedOwnCreditsText,
16895
17511
  poolEndsAtFrom,
16896
17512
  poolErrorMessage,
@@ -16903,7 +17519,17 @@ export {
16903
17519
  settingsReturnTarget,
16904
17520
  sharedPoolBannerText,
16905
17521
  sharedPoolEndsText,
17522
+ sliceUsageLogsPage,
17523
+ sponsoredCreditsText,
17524
+ toCsv,
16906
17525
  toLocalDateInput,
17526
+ usageLogsCsvRows,
17527
+ usageLogsDateRange,
17528
+ usageLogsExportFileName,
17529
+ usageLogsPage,
17530
+ usageLogsReducer,
17531
+ usageLogsSpentByOthers,
17532
+ useBillingEntitlement,
16907
17533
  useSettingsAdapters,
16908
17534
  useWorkspaceInvitePermission,
16909
17535
  useWorkspaceOrganization,