@streamoid/settings 0.12.4 → 0.14.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
@@ -9601,6 +9601,76 @@ function describeAllowance(member) {
9601
9601
  const allowance = `${credits(member.allowance)} this cycle`;
9602
9602
  return member.mode === "percent" ? `${formatPoolSplit(member)} \xB7 ${allowance}` : allowance;
9603
9603
  }
9604
+ var POOL_EXPIRY_MAX_MONTHS = 120;
9605
+ function addCalendarMonths(date, months) {
9606
+ const target = new Date(date.getFullYear(), date.getMonth() + months, 1);
9607
+ const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
9608
+ return new Date(
9609
+ target.getFullYear(),
9610
+ target.getMonth(),
9611
+ Math.min(date.getDate(), lastDay),
9612
+ date.getHours(),
9613
+ date.getMinutes(),
9614
+ date.getSeconds(),
9615
+ date.getMilliseconds()
9616
+ );
9617
+ }
9618
+ var pad2 = (n) => String(n).padStart(2, "0");
9619
+ function toLocalDateInput(date) {
9620
+ return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
9621
+ }
9622
+ function minPoolEndDate(now = /* @__PURE__ */ new Date()) {
9623
+ return toLocalDateInput(new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1));
9624
+ }
9625
+ function parseLocalDate(value) {
9626
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim());
9627
+ if (!match) return null;
9628
+ const [year, month, day] = [Number(match[1]), Number(match[2]) - 1, Number(match[3])];
9629
+ const date = new Date(year, month, day);
9630
+ return date.getFullYear() === year && date.getMonth() === month && date.getDate() === day ? date : null;
9631
+ }
9632
+ function poolEndsAtFrom(choice, now = /* @__PURE__ */ new Date()) {
9633
+ switch (choice.kind) {
9634
+ case "never":
9635
+ return { ok: true, value: null };
9636
+ case "month":
9637
+ return { ok: true, value: addCalendarMonths(now, 1).toISOString() };
9638
+ case "year":
9639
+ return { ok: true, value: addCalendarMonths(now, 12).toISOString() };
9640
+ case "months": {
9641
+ const { months } = choice;
9642
+ if (!Number.isInteger(months) || months < 1 || months > POOL_EXPIRY_MAX_MONTHS) {
9643
+ return { ok: false, reason: `Enter a whole number of months from 1 to ${POOL_EXPIRY_MAX_MONTHS}` };
9644
+ }
9645
+ return { ok: true, value: addCalendarMonths(now, months).toISOString() };
9646
+ }
9647
+ case "date": {
9648
+ if (!choice.date) return { ok: false, reason: "Choose a date" };
9649
+ const date = parseLocalDate(choice.date);
9650
+ if (!date) return { ok: false, reason: "Choose a valid date" };
9651
+ if (choice.date < minPoolEndDate(now)) return { ok: false, reason: "Choose a date after today" };
9652
+ return { ok: true, value: date.toISOString() };
9653
+ }
9654
+ }
9655
+ }
9656
+ function expiryChoiceFor(member) {
9657
+ const endsAt = member?.endsAt ? new Date(member.endsAt) : null;
9658
+ if (!endsAt || !Number.isFinite(endsAt.getTime())) return { kind: "never" };
9659
+ return { kind: "date", date: toLocalDateInput(endsAt) };
9660
+ }
9661
+ function formatPoolEndsAt(endsAt) {
9662
+ const date = endsAt ? new Date(endsAt) : null;
9663
+ if (!date || !Number.isFinite(date.getTime())) return "Never";
9664
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
9665
+ }
9666
+ function sharedPoolEndsText(pool) {
9667
+ return pool.endsAt ? `Sharing ends on ${formatPoolEndsAt(pool.endsAt)}.` : "Sharing ends: Never.";
9668
+ }
9669
+ function poolRenewsAtCycleEnd(pool) {
9670
+ if (!pool.cycleEnd) return false;
9671
+ if (!pool.endsAt) return true;
9672
+ return new Date(pool.endsAt).getTime() > new Date(pool.cycleEnd).getTime();
9673
+ }
9604
9674
  function poolWorkspaceName(name) {
9605
9675
  return name?.trim() || "Unknown workspace";
9606
9676
  }
@@ -9608,12 +9678,8 @@ function sharedPoolBannerText(pool) {
9608
9678
  const sponsor = pool.sponsorName?.trim() || "another workspace";
9609
9679
  return `Credits shared by ${sponsor}: ${formatCredits(pool.remaining)} of ${formatCredits(pool.allowance)} left this cycle`;
9610
9680
  }
9611
- function pausedOwnCreditsText(pool) {
9612
- const own = pool.ownCredits;
9613
- if (typeof own !== "number" || !Number.isFinite(own)) {
9614
- return "This workspace spends these shared credits, not its own; credits bought here can't be used while it does.";
9615
- }
9616
- return `This workspace's own ${credits(own)} ${own === 1 ? "is" : "are"} paused while it uses shared credits. They stay with it and can be used again once sharing stops.`;
9681
+ function pausedOwnCreditsText(_pool) {
9682
+ return ownCreditsFirstText();
9617
9683
  }
9618
9684
  function ownCreditsFirstText() {
9619
9685
  return "This workspace uses its own credits first, then these shared credits.";
@@ -9808,25 +9874,36 @@ function PoolModalShell({
9808
9874
  }
9809
9875
  );
9810
9876
  }
9811
- function PoolWarning({ children }) {
9812
- return /* @__PURE__ */ jsx26(
9813
- "div",
9814
- {
9815
- role: "alert",
9816
- className: "w-full",
9817
- style: {
9818
- ...modalBodyText,
9819
- color: T16.primary,
9820
- background: T16.warningFill,
9821
- border: `1px solid ${T16.warningBorder}`,
9822
- borderRadius: T16.r3xl,
9823
- padding: `${T16.spXl} ${T16.sp3xl}`
9824
- },
9825
- children
9826
- }
9827
- );
9828
- }
9829
9877
  var fieldLabel = { ...modalBodyText, fontWeight: 500, color: T16.primary };
9878
+ var fieldBox = {
9879
+ background: T16.fillPlus,
9880
+ height: 48,
9881
+ borderRadius: T16.r3xl,
9882
+ padding: `${T16.spXl} ${T16.sp3xl}`,
9883
+ gap: T16.spXl
9884
+ };
9885
+ var bareInput = {
9886
+ ...modalBodyText,
9887
+ background: "transparent",
9888
+ border: "none",
9889
+ outline: "none",
9890
+ color: T16.primary,
9891
+ padding: 0
9892
+ };
9893
+ var EXPIRY_OPTIONS = [
9894
+ { id: "never", label: "Never" },
9895
+ { id: "month", label: "In 1 month" },
9896
+ { id: "months", label: "In a number of months" },
9897
+ { id: "year", label: "In 1 year" },
9898
+ { id: "date", label: "On a date" }
9899
+ ];
9900
+ var isExpiryKind = (id) => EXPIRY_OPTIONS.some((o) => o.id === id);
9901
+ var endsText = (member) => member.endsAt ? `, until ${formatPoolEndsAt(member.endsAt)}` : "";
9902
+ function endsAtHelp(kind, endsAt) {
9903
+ if (kind === "never") return "Shares every billing cycle until it is removed.";
9904
+ const stop = "After that, the other workspace spends only its own credits.";
9905
+ return endsAt ? `Sharing stops on ${formatPoolEndsAt(endsAt)}. ${stop}` : stop;
9906
+ }
9830
9907
  function CreditPoolMemberModal(props) {
9831
9908
  const { toast } = useSettingsAdapters();
9832
9909
  const editing = props.kind === "edit" ? props.member : null;
@@ -9837,17 +9914,28 @@ function CreditPoolMemberModal(props) {
9837
9914
  const [touched, setTouched] = useState18(false);
9838
9915
  const [saving, setSaving] = useState18(false);
9839
9916
  const [serverError, setServerError] = useState18(null);
9917
+ const initialExpiry = expiryChoiceFor(editing);
9918
+ const [expiryKind, setExpiryKind] = useState18(initialExpiry.kind);
9919
+ const [monthsRaw, setMonthsRaw] = useState18("3");
9920
+ const [endDate, setEndDate] = useState18(initialExpiry.kind === "date" ? initialExpiry.date : "");
9840
9921
  useEffect16(() => {
9841
9922
  setServerError(null);
9842
- }, [memberId, mode, raw, note]);
9923
+ }, [memberId, mode, raw, note, expiryKind, monthsRaw, endDate]);
9843
9924
  const value = validatePoolValue(mode, raw);
9844
9925
  const noteCheck = validatePoolText(note, POOL_NOTE_MAX_LENGTH);
9845
- const changed = !editing || mode !== editing.mode || !value.ok || value.value !== editing.value;
9846
- const valid = value.ok && (props.kind === "edit" || Boolean(memberId) && noteCheck.ok);
9926
+ const expiry = expiryKind === "months" ? { kind: "months", months: monthsRaw.trim() ? Number(monthsRaw) : NaN } : expiryKind === "date" ? { kind: "date", date: endDate } : { kind: expiryKind };
9927
+ const endsAtUnchanged = Boolean(editing) && expiry.kind === initialExpiry.kind && (expiry.kind !== "date" || initialExpiry.kind === "date" && expiry.date === initialExpiry.date);
9928
+ const endsAt = endsAtUnchanged ? null : poolEndsAtFrom(expiry);
9929
+ const splitChanged = !editing || mode !== editing.mode || !value.ok || value.value !== editing.value;
9930
+ const changed = splitChanged || !endsAtUnchanged;
9931
+ const valid = value.ok && (endsAt === null || endsAt.ok) && (props.kind === "edit" || Boolean(memberId) && noteCheck.ok);
9847
9932
  const canSave = !saving && changed && valid;
9848
9933
  const valueHint = touched && !value.ok ? value.reason : null;
9934
+ const expiryHint = endsAt && !endsAt.ok && !(expiry.kind === "date" && !expiry.date) ? endsAt.reason : null;
9849
9935
  const handleSave = async () => {
9850
9936
  if (!canSave || !value.ok) return;
9937
+ const sendEndsAt = endsAtUnchanged ? null : poolEndsAtFrom(expiry);
9938
+ if (sendEndsAt && !sendEndsAt.ok) return;
9851
9939
  setSaving(true);
9852
9940
  try {
9853
9941
  if (props.kind === "add") {
@@ -9855,12 +9943,17 @@ function CreditPoolMemberModal(props) {
9855
9943
  memberWorkspaceId: memberId,
9856
9944
  mode,
9857
9945
  value: value.value,
9858
- note: noteCheck.ok ? noteCheck.value : ""
9946
+ note: noteCheck.ok ? noteCheck.value : "",
9947
+ ...sendEndsAt?.value ? { endsAt: sendEndsAt.value } : {}
9859
9948
  });
9860
- toast.success(`Credits shared. ${describeAllowance(added)}.`);
9949
+ toast.success(`Credits shared. ${describeAllowance(added)}${endsText(added)}.`);
9861
9950
  } else {
9862
- const updated = await props.onSave({ mode, value: value.value });
9863
- toast.success(`Share updated. ${describeAllowance(updated)}.`);
9951
+ const updated = await props.onSave({
9952
+ mode,
9953
+ value: value.value,
9954
+ ...sendEndsAt ? { endsAt: sendEndsAt.value } : {}
9955
+ });
9956
+ toast.success(`Share updated. ${describeAllowance(updated)}${endsText(updated)}.`);
9864
9957
  }
9865
9958
  props.onSaved();
9866
9959
  props.onClose();
@@ -9945,11 +10038,7 @@ function CreditPoolMemberModal(props) {
9945
10038
  {
9946
10039
  className: "flex items-center w-full",
9947
10040
  style: {
9948
- background: T16.fillPlus,
9949
- height: 48,
9950
- borderRadius: T16.r3xl,
9951
- padding: `${T16.spXl} ${T16.sp3xl}`,
9952
- gap: T16.spXl,
10041
+ ...fieldBox,
9953
10042
  border: valueHint ? `1px solid ${T16.error}` : "1px solid transparent"
9954
10043
  },
9955
10044
  children: [
@@ -9967,14 +10056,7 @@ function CreditPoolMemberModal(props) {
9967
10056
  setTouched(true);
9968
10057
  },
9969
10058
  className: "flex-1 min-w-0",
9970
- style: {
9971
- ...modalBodyText,
9972
- background: "transparent",
9973
- border: "none",
9974
- outline: "none",
9975
- color: T16.primary,
9976
- padding: 0
9977
- }
10059
+ style: bareInput
9978
10060
  }
9979
10061
  ),
9980
10062
  /* @__PURE__ */ jsx26("span", { className: "shrink-0", style: { ...modalBodyText, color: T16.muted }, children: mode === "percent" ? "%" : "credits" })
@@ -9982,10 +10064,76 @@ function CreditPoolMemberModal(props) {
9982
10064
  }
9983
10065
  ),
9984
10066
  valueHint ? /* @__PURE__ */ jsx26("span", { role: "alert", style: { ...modalBodyText, color: T16.error }, children: valueHint }) : /* @__PURE__ */ jsxs23("span", { style: { ...modalBodyText, color: T16.tertiary }, children: [
9985
- mode === "percent" ? "A share of this workspace's plan credits for each billing cycle. Credits stay with this workspace; the other workspace spends them up to this share." : "Credits the other workspace may spend each billing cycle. Credits stay with this workspace until spent.",
10067
+ mode === "percent" ? "A share of this workspace's plan credits for each billing cycle. Credits stay with this workspace; the other workspace spends them up to this share, after its own credits." : "Credits the other workspace may spend each billing cycle, after its own credits. Credits stay with this workspace until spent.",
9986
10068
  editing ? " Applies immediately." : ""
9987
10069
  ] })
9988
10070
  ] }),
10071
+ /* @__PURE__ */ jsxs23("div", { className: "flex flex-col w-full", style: { gap: T16.spMd }, children: [
10072
+ /* @__PURE__ */ jsx26("span", { style: fieldLabel, children: "Ends" }),
10073
+ /* @__PURE__ */ jsx26(
10074
+ ScSelect,
10075
+ {
10076
+ value: expiryKind,
10077
+ onChange: (id) => {
10078
+ if (isExpiryKind(id)) setExpiryKind(id);
10079
+ },
10080
+ options: EXPIRY_OPTIONS,
10081
+ disabled: saving
10082
+ }
10083
+ ),
10084
+ expiryKind === "months" && /* @__PURE__ */ jsxs23(
10085
+ "div",
10086
+ {
10087
+ className: "flex items-center w-full",
10088
+ style: {
10089
+ ...fieldBox,
10090
+ border: expiryHint ? `1px solid ${T16.error}` : "1px solid transparent"
10091
+ },
10092
+ children: [
10093
+ /* @__PURE__ */ jsx26(
10094
+ "input",
10095
+ {
10096
+ inputMode: "numeric",
10097
+ "aria-label": "Number of months",
10098
+ "aria-invalid": expiryHint ? true : void 0,
10099
+ placeholder: "Enter a number of months",
10100
+ value: monthsRaw,
10101
+ disabled: saving,
10102
+ onChange: (e) => setMonthsRaw(e.target.value.replace(/\D/g, "").slice(0, 3)),
10103
+ className: "flex-1 min-w-0",
10104
+ style: bareInput
10105
+ }
10106
+ ),
10107
+ /* @__PURE__ */ jsx26("span", { className: "shrink-0", style: { ...modalBodyText, color: T16.muted }, children: "months" })
10108
+ ]
10109
+ }
10110
+ ),
10111
+ expiryKind === "date" && /* @__PURE__ */ jsx26(
10112
+ "div",
10113
+ {
10114
+ className: "flex items-center w-full",
10115
+ style: {
10116
+ ...fieldBox,
10117
+ border: expiryHint ? `1px solid ${T16.error}` : "1px solid transparent"
10118
+ },
10119
+ children: /* @__PURE__ */ jsx26(
10120
+ "input",
10121
+ {
10122
+ type: "date",
10123
+ "aria-label": "End date",
10124
+ "aria-invalid": expiryHint ? true : void 0,
10125
+ min: minPoolEndDate(),
10126
+ value: endDate,
10127
+ disabled: saving,
10128
+ onChange: (e) => setEndDate(e.target.value),
10129
+ className: "flex-1 min-w-0",
10130
+ style: { ...bareInput, colorScheme: "light dark" }
10131
+ }
10132
+ )
10133
+ }
10134
+ ),
10135
+ expiryHint ? /* @__PURE__ */ jsx26("span", { role: "alert", style: { ...modalBodyText, color: T16.error }, children: expiryHint }) : /* @__PURE__ */ jsx26("span", { style: { ...modalBodyText, color: T16.tertiary }, children: endsAtHelp(expiryKind, endsAtUnchanged ? editing?.endsAt ?? null : endsAt?.ok ? endsAt.value : null) })
10136
+ ] }),
9989
10137
  props.kind === "add" && /* @__PURE__ */ jsxs23("div", { className: "flex flex-col w-full", style: { gap: T16.spXs }, children: [
9990
10138
  /* @__PURE__ */ jsx26("span", { style: fieldLabel, children: "Note" }),
9991
10139
  /* @__PURE__ */ jsx26(
@@ -10007,7 +10155,6 @@ function CreditPoolMemberModal(props) {
10007
10155
  POOL_NOTE_MAX_LENGTH
10008
10156
  ] })
10009
10157
  ] }),
10010
- props.kind === "add" && /* @__PURE__ */ jsx26(PoolWarning, { children: "While it spends shared credits, the other workspace uses only this workspace's credits, not its own." }),
10011
10158
  serverError && /* @__PURE__ */ jsx26("span", { role: "alert", style: { ...modalBodyText, color: T16.error }, children: serverError })
10012
10159
  ]
10013
10160
  }
@@ -10128,7 +10275,9 @@ var T17 = {
10128
10275
  r3xl: "var(--radius-3xl)",
10129
10276
  rFull: "var(--radius-full)"
10130
10277
  };
10131
- var MEMBER_COLUMNS2 = "minmax(220px, 1fr) 220px 110px 110px 150px";
10278
+ var MEMBER_COLUMNS2 = "minmax(180px, 1fr) 150px 80px 90px 110px 150px";
10279
+ var MEMBER_GAP = 16;
10280
+ var MEMBER_MIN_WIDTH = 880;
10132
10281
  var cellText2 = {
10133
10282
  fontFamily: T17.font,
10134
10283
  fontSize: T17.fsSm,
@@ -10141,7 +10290,7 @@ var tableHeader = (columns, labels) => /* @__PURE__ */ jsx28(
10141
10290
  style: {
10142
10291
  display: "grid",
10143
10292
  gridTemplateColumns: columns,
10144
- gap: 24,
10293
+ gap: MEMBER_GAP,
10145
10294
  padding: "8px 16px",
10146
10295
  fontSize: 11,
10147
10296
  color: T17.muted,
@@ -10165,17 +10314,21 @@ function MemberRow2({
10165
10314
  gridTemplateColumns: MEMBER_COLUMNS2,
10166
10315
  alignItems: "center",
10167
10316
  padding: `10px ${T17.sp3xl}`,
10168
- gap: 24
10317
+ gap: MEMBER_GAP
10169
10318
  },
10170
10319
  children: [
10171
10320
  /* @__PURE__ */ jsxs25("div", { className: "flex flex-col min-w-0", style: { gap: 2 }, children: [
10172
10321
  /* @__PURE__ */ jsx28("span", { className: "truncate", style: { ...cellText2, fontWeight: 500 }, children: poolWorkspaceName(member.memberWorkspaceName) }),
10173
10322
  member.note && /* @__PURE__ */ jsx28("span", { className: "truncate", title: member.note, style: { ...cellText2, color: T17.tertiary }, children: member.note })
10174
10323
  ] }),
10175
- /* @__PURE__ */ jsx28("span", { style: cellText2, children: describeAllowance(member) }),
10324
+ /* @__PURE__ */ jsxs25("div", { className: "flex flex-col min-w-0", style: { gap: 2 }, children: [
10325
+ /* @__PURE__ */ jsx28("span", { style: cellText2, children: formatPoolSplit({ mode: "fixed", value: member.allowance }) }),
10326
+ /* @__PURE__ */ jsx28("span", { style: { ...cellText2, color: T17.tertiary }, children: member.mode === "percent" ? `${formatPoolSplit(member)} \xB7 this cycle` : "this cycle" })
10327
+ ] }),
10176
10328
  /* @__PURE__ */ jsx28("span", { style: cellText2, children: formatCredits(member.used) }),
10177
10329
  /* @__PURE__ */ jsx28("span", { style: { ...cellText2, color: spent ? T17.warning : T17.primary }, children: formatCredits(member.remaining) }),
10178
- /* @__PURE__ */ jsxs25("div", { className: "flex items-center", style: { gap: 8 }, children: [
10330
+ /* @__PURE__ */ jsx28("span", { style: { ...cellText2, color: member.endsAt ? T17.primary : T17.muted }, children: formatPoolEndsAt(member.endsAt) }),
10331
+ /* @__PURE__ */ jsxs25("div", { className: "flex items-center whitespace-nowrap", style: { gap: 8 }, children: [
10179
10332
  onEdit && /* @__PURE__ */ jsx28(ScButton13, { text: "Edit", variant: "secondary", size: "sm", onClick: onEdit }),
10180
10333
  onRemove && /* @__PURE__ */ jsx28(ScButton13, { text: "Remove", variant: "secondary", size: "sm", onClick: onRemove })
10181
10334
  ] })
@@ -10301,11 +10454,13 @@ function SharedCreditsView() {
10301
10454
  remaining: sponsor.remaining
10302
10455
  }) }),
10303
10456
  /* @__PURE__ */ jsxs25("span", { style: { ...cellText2, color: T17.muted }, children: [
10304
- "This workspace spends ",
10457
+ "This workspace uses its own credits first, then",
10458
+ " ",
10305
10459
  poolWorkspaceName(sponsor.sponsorWorkspaceName),
10306
- "'s credits, not its own",
10307
- sponsor.cycle?.end ? `, until its allowance starts again on ${formatDate2(sponsor.cycle.end)}` : "",
10308
- "."
10460
+ "'s, up to its allowance",
10461
+ sponsor.cycle?.end && poolRenewsAtCycleEnd({ cycleEnd: sponsor.cycle.end, endsAt: sponsor.endsAt }) ? `, which starts again on ${formatDate2(sponsor.cycle.end)}` : "",
10462
+ ". ",
10463
+ sharedPoolEndsText(sponsor)
10309
10464
  ] })
10310
10465
  ]
10311
10466
  }
@@ -10317,8 +10472,8 @@ function SharedCreditsView() {
10317
10472
  {
10318
10473
  grow: "cap",
10319
10474
  maxHeight: 420,
10320
- contentMinWidth: 860,
10321
- header: tableHeader(MEMBER_COLUMNS2, ["Workspace", "Allowance", "Used", "Remaining", "Actions"]),
10475
+ contentMinWidth: MEMBER_MIN_WIDTH,
10476
+ header: tableHeader(MEMBER_COLUMNS2, ["Workspace", "Allowance", "Used", "Remaining", "Ends", "Actions"]),
10322
10477
  isEmpty: failed || members.length === 0,
10323
10478
  emptyText: failed ? "Couldn't load shared credits. Try again later." : "This workspace doesn't share its credits with any other workspace.",
10324
10479
  children: members.map((member) => /* @__PURE__ */ jsx28(
@@ -10368,6 +10523,71 @@ function SharedCreditsView() {
10368
10523
  ] });
10369
10524
  }
10370
10525
 
10526
+ // src/entitlement.ts
10527
+ function parseBillingEntitlement(value) {
10528
+ if (!value || typeof value !== "object") return null;
10529
+ const raw = value;
10530
+ if (typeof raw.paid !== "boolean") return null;
10531
+ const source = raw.source === "plan" || raw.source === "pool" ? raw.source : null;
10532
+ const entitlement = { paid: raw.paid, source };
10533
+ if (typeof raw.sponsorWorkspaceId === "string") entitlement.sponsorWorkspaceId = raw.sponsorWorkspaceId;
10534
+ if (typeof raw.sponsorName === "string") entitlement.sponsorName = raw.sponsorName;
10535
+ if (typeof raw.endsAt === "string" || raw.endsAt === null) entitlement.endsAt = raw.endsAt;
10536
+ return entitlement;
10537
+ }
10538
+ function entitlementFromBody(body) {
10539
+ if (!body || typeof body !== "object") return null;
10540
+ return parseBillingEntitlement(body.entitlement);
10541
+ }
10542
+ function entitlementFromFailure(failure) {
10543
+ const seen = /* @__PURE__ */ new Set();
10544
+ const visit = (value, depth) => {
10545
+ if (depth > 4 || value == null || typeof value !== "object" || seen.has(value)) return null;
10546
+ seen.add(value);
10547
+ const found = entitlementFromBody(value);
10548
+ if (found) return found;
10549
+ const error = value;
10550
+ const response = error.response;
10551
+ for (const child of [error.body, error.data, response?.data, error.cause]) {
10552
+ const nested = visit(child, depth + 1);
10553
+ if (nested) return nested;
10554
+ }
10555
+ return null;
10556
+ };
10557
+ return visit(failure, 0);
10558
+ }
10559
+ function isPaidPlan(subscription) {
10560
+ const amount = subscription?.plan?.amount;
10561
+ return amount != null && amount > 0;
10562
+ }
10563
+ function hasPaidFeatures({
10564
+ entitlement,
10565
+ subscription
10566
+ }) {
10567
+ return entitlement ? entitlement.paid : isPaidPlan(subscription);
10568
+ }
10569
+ function isSponsoredEntitlement(entitlement) {
10570
+ return entitlement?.source === "pool" && entitlement.paid;
10571
+ }
10572
+ function sponsoredCreditsText(entitlement) {
10573
+ const sponsor = entitlement.sponsorName?.trim() || "another workspace";
10574
+ const ends = entitlement.endsAt ? ` Sharing ends on ${formatPoolEndsAt(entitlement.endsAt)}.` : "";
10575
+ return `Credits shared by ${sponsor}.${ends}`;
10576
+ }
10577
+ async function loadCurrentPlan(fetchCurrentPlan) {
10578
+ let response;
10579
+ try {
10580
+ response = await fetchCurrentPlan();
10581
+ } catch (failure) {
10582
+ const entitlement2 = entitlementFromFailure(failure);
10583
+ if (!entitlement2) throw failure;
10584
+ return { subscription: null, entitlement: entitlement2, paid: entitlement2.paid };
10585
+ }
10586
+ const subscription = response?.data ?? null;
10587
+ const entitlement = entitlementFromBody(response);
10588
+ return { subscription, entitlement, paid: hasPaidFeatures({ entitlement, subscription }) };
10589
+ }
10590
+
10371
10591
  // src/billing-content.tsx
10372
10592
  import { Fragment as Fragment11, jsx as jsx29, jsxs as jsxs26 } from "react/jsx-runtime";
10373
10593
  var DEFAULT_TOPUP_SERVICE = "catalogix";
@@ -11499,7 +11719,8 @@ function BillingContent({
11499
11719
  showPerUserLimits: showPerUserLimitsProp,
11500
11720
  onShowPerUserLimitsChange,
11501
11721
  showSharedCredits: showSharedCreditsProp,
11502
- onShowSharedCreditsChange
11722
+ onShowSharedCreditsChange,
11723
+ onEntitlementChange
11503
11724
  } = {}) {
11504
11725
  const { auth, workspace, organization, api, toast, navigation } = useSettingsAdapters();
11505
11726
  const user = auth.user;
@@ -11538,6 +11759,9 @@ function BillingContent({
11538
11759
  const [showExportModal, setShowExportModal] = useState21(false);
11539
11760
  const [showBuyCreditsModal, setShowBuyCreditsModal] = useState21(false);
11540
11761
  const [subscription, setSubscription] = useState21(null);
11762
+ const [entitlement, setEntitlement] = useState21(
11763
+ null
11764
+ );
11541
11765
  const [availableCredits, setAvailableCredits] = useState21(null);
11542
11766
  const [sharedPool, setSharedPool] = useState21(null);
11543
11767
  const [usedByNames, setUsedByNames] = useState21({});
@@ -11583,12 +11807,15 @@ function BillingContent({
11583
11807
  try {
11584
11808
  console.log("calling Billing apis");
11585
11809
  const [subRes, creditsRes, productsRes, customRes] = await Promise.all([
11586
- api.fetchCurrentPlan().catch(() => null),
11810
+ // A sponsored workspace with no subscription of its own gets a 404
11811
+ // that carries its entitlement: loadCurrentPlan answers it, not throws.
11812
+ loadCurrentPlan(api.fetchCurrentPlan).catch(() => null),
11587
11813
  api.fetchAvailableCredits().catch(() => null),
11588
11814
  api.fetchPlans().catch(() => null),
11589
11815
  api.getCustomCreditInfo?.().catch(() => null) ?? Promise.resolve(null)
11590
11816
  ]);
11591
- if (subRes?.data) setSubscription(subRes.data);
11817
+ if (subRes?.subscription) setSubscription(subRes.subscription);
11818
+ if (subRes) setEntitlement(subRes.entitlement);
11592
11819
  if (creditsRes?.availableCredits != null)
11593
11820
  setAvailableCredits(creditsRes.availableCredits);
11594
11821
  setSharedPool(creditsRes?.sharedPool ?? null);
@@ -11758,6 +11985,7 @@ function BillingContent({
11758
11985
  setInitialFetchTriggered(false);
11759
11986
  setBillingOverviewLoaded(false);
11760
11987
  setSubscription(null);
11988
+ setEntitlement(null);
11761
11989
  setAvailableCredits(null);
11762
11990
  setProducts([]);
11763
11991
  setTotalCredits(null);
@@ -11813,7 +12041,16 @@ function BillingContent({
11813
12041
  subscription?.scheduledDowngradeEffectiveDate
11814
12042
  );
11815
12043
  const scheduledDowngradeText = subscription?.downgradeScheduled && subscription.plan?.name ? `Downgrades to ${subscription.plan.name}${scheduledDowngradeDateLabel ? ` ${scheduledDowngradeDateLabel}` : ""}` : void 0;
11816
- const isPlanDetailsLoading = !billingOverviewLoaded || loadingBilling || subscription?.plan == null || !planName || planName === "-" || !planAmount || currentPlanCredits == null;
12044
+ const sponsored = isSponsoredEntitlement(entitlement);
12045
+ const sponsoredWithoutPlan = sponsored && billingOverviewLoaded && !loadingBilling && subscription == null;
12046
+ const isPlanDetailsLoading = sponsoredWithoutPlan ? false : !billingOverviewLoaded || loadingBilling || subscription?.plan == null || !planName || planName === "-" || !planAmount || currentPlanCredits == null;
12047
+ const paidFeatures = hasPaidFeatures({ entitlement, subscription });
12048
+ const onEntitlementChangeRef = useRef10(onEntitlementChange);
12049
+ onEntitlementChangeRef.current = onEntitlementChange;
12050
+ useEffect18(() => {
12051
+ if (!billingOverviewLoaded) return;
12052
+ onEntitlementChangeRef.current?.({ entitlement, paid: paidFeatures });
12053
+ }, [billingOverviewLoaded, entitlement, paidFeatures]);
11817
12054
  const {
11818
12055
  used: usedCredits,
11819
12056
  remaining: remainingCredits,
@@ -12050,9 +12287,9 @@ function BillingContent({
12050
12287
  /* @__PURE__ */ jsx29(
12051
12288
  ScPlanDetailsCard,
12052
12289
  {
12053
- planName,
12054
- planCredits: planCreditsText,
12055
- planPrice: planAmount,
12290
+ planName: sponsoredWithoutPlan ? "Free" : planName,
12291
+ planCredits: sponsoredWithoutPlan ? "Shared credits" : planCreditsText,
12292
+ planPrice: sponsoredWithoutPlan ? `${workspace.current?.country?.trim().toUpperCase() === "IN" ? "\u20B9" : "$"}0.00 / month` : planAmount,
12056
12293
  badgeText: "Active",
12057
12294
  badgeSubText: scheduledDowngradeText,
12058
12295
  currentPlanLabel: "Current plan",
@@ -12134,12 +12371,57 @@ function BillingContent({
12134
12371
  },
12135
12372
  children: [
12136
12373
  ownCreditsFirstText(),
12137
- sharedPool.cycleEnd ? ` The allowance starts again on ${formatDate3(sharedPool.cycleEnd)}.` : ""
12374
+ poolRenewsAtCycleEnd(sharedPool) ? ` The allowance starts again on ${formatDate3(sharedPool.cycleEnd)}.` : "",
12375
+ ` ${sharedPoolEndsText(sharedPool)}`
12138
12376
  ]
12139
12377
  }
12140
12378
  )
12141
12379
  ]
12142
12380
  }
12381
+ ) : sponsored && entitlement ? (
12382
+ // Billing says a pool entitles it, but the balance carried no
12383
+ // allowance (an older credits answer, or it failed): say whose.
12384
+ /* @__PURE__ */ jsxs26(
12385
+ "div",
12386
+ {
12387
+ role: "status",
12388
+ className: "flex flex-col shrink-0 w-full",
12389
+ style: {
12390
+ gap: 4,
12391
+ padding: 16,
12392
+ backgroundColor: "var(--alias-surface-base)",
12393
+ border: "1px solid var(--alias-border-subtle)",
12394
+ borderRadius: "var(--radius-3xl)"
12395
+ },
12396
+ children: [
12397
+ /* @__PURE__ */ jsx29(
12398
+ "span",
12399
+ {
12400
+ style: {
12401
+ fontFamily: "var(--font-family-font-family-body)",
12402
+ fontSize: "var(--font-size-font-size-sm)",
12403
+ fontWeight: 500,
12404
+ lineHeight: "var(--line-height-line-height-sm)",
12405
+ color: "var(--alias-text---icons-primary)"
12406
+ },
12407
+ children: sponsoredCreditsText(entitlement)
12408
+ }
12409
+ ),
12410
+ /* @__PURE__ */ jsx29(
12411
+ "span",
12412
+ {
12413
+ style: {
12414
+ fontFamily: "var(--font-family-font-family-body)",
12415
+ fontSize: "var(--font-size-font-size-sm)",
12416
+ lineHeight: "var(--line-height-line-height-sm)",
12417
+ color: "var(--alias-text---icons-muted)"
12418
+ },
12419
+ children: ownCreditsFirstText()
12420
+ }
12421
+ )
12422
+ ]
12423
+ }
12424
+ )
12143
12425
  ) : null,
12144
12426
  usage ? /* @__PURE__ */ jsxs26(
12145
12427
  "div",
@@ -12388,7 +12670,7 @@ import {
12388
12670
  SiconClose as SiconClose10
12389
12671
  } from "@streamoid/icons";
12390
12672
  import { Fragment as Fragment12, jsx as jsx30, jsxs as jsxs27 } from "react/jsx-runtime";
12391
- var EXPIRY_OPTIONS = [
12673
+ var EXPIRY_OPTIONS2 = [
12392
12674
  { label: "30 days", value: "30d" },
12393
12675
  { label: "60 days", value: "60d" },
12394
12676
  { label: "90 days", value: "90d" },
@@ -12579,7 +12861,7 @@ function CreateTokenModal({ onClose, onCreated }) {
12579
12861
  children: "Expiry"
12580
12862
  }
12581
12863
  ) }),
12582
- /* @__PURE__ */ jsx30("div", { style: { display: "flex", gap: "var(--spacing-md)", flexWrap: "wrap" }, children: EXPIRY_OPTIONS.map((opt) => /* @__PURE__ */ jsx30(
12864
+ /* @__PURE__ */ jsx30("div", { style: { display: "flex", gap: "var(--spacing-md)", flexWrap: "wrap" }, children: EXPIRY_OPTIONS2.map((opt) => /* @__PURE__ */ jsx30(
12583
12865
  ScButton15,
12584
12866
  {
12585
12867
  text: opt.label,
@@ -16520,6 +16802,30 @@ function OrganizationPicker({
16520
16802
  );
16521
16803
  }
16522
16804
 
16805
+ // src/use-billing-entitlement.ts
16806
+ import { useEffect as useEffect22, useRef as useRef12, useState as useState28 } from "react";
16807
+ function useBillingEntitlement(fetchCurrentPlan, key) {
16808
+ const fetchRef = useRef12(fetchCurrentPlan);
16809
+ fetchRef.current = fetchCurrentPlan;
16810
+ const [state, setState] = useState28(null);
16811
+ const [loading, setLoading] = useState28(false);
16812
+ useEffect22(() => {
16813
+ const fetch2 = fetchRef.current;
16814
+ setState(null);
16815
+ if (!key || !fetch2) {
16816
+ setLoading(false);
16817
+ return;
16818
+ }
16819
+ let live = true;
16820
+ setLoading(true);
16821
+ loadCurrentPlan(fetch2).then((next) => live && setState(next)).catch(() => live && setState(null)).finally(() => live && setLoading(false));
16822
+ return () => {
16823
+ live = false;
16824
+ };
16825
+ }, [key]);
16826
+ return { state, loading };
16827
+ }
16828
+
16523
16829
  // src/administration-navigation.ts
16524
16830
  var normalizedBase = (base) => base.replace(/\/$/, "");
16525
16831
  var returnKey = (base, entryKey) => `streamoid:settings:return:${base}:${entryKey}`;
@@ -16677,6 +16983,7 @@ export {
16677
16983
  OrganizationModal,
16678
16984
  OrganizationPicker,
16679
16985
  POOL_ERROR_CODES,
16986
+ POOL_EXPIRY_MAX_MONTHS,
16680
16987
  POOL_NOTE_MAX_LENGTH,
16681
16988
  PerUserLimitsView,
16682
16989
  ProfileAvatar,
@@ -16716,23 +17023,35 @@ export {
16716
17023
  describeAllowance,
16717
17024
  describeDefaultLimit,
16718
17025
  describeMemberLimit,
17026
+ entitlementFromBody,
17027
+ entitlementFromFailure,
17028
+ expiryChoiceFor,
16719
17029
  findBillingError,
16720
17030
  findPoolError,
16721
17031
  formatCreditLimit,
16722
17032
  formatCredits,
17033
+ formatPoolEndsAt,
16723
17034
  formatPoolSplit,
16724
17035
  groupWorkspaceList,
17036
+ hasPaidFeatures,
16725
17037
  isLimitBelowUsage,
17038
+ isPaidPlan,
17039
+ isSponsoredEntitlement,
16726
17040
  joinMembersWithLimits,
16727
17041
  lastUsedLabel,
16728
17042
  limitBelowUsageUsed,
16729
17043
  limitSourceLabel,
17044
+ loadCurrentPlan,
16730
17045
  loadWorkspaceOrganization,
16731
17046
  memberRoleUpdate,
17047
+ minPoolEndDate,
16732
17048
  minimumCreditLimit,
16733
17049
  ownCreditsFirstText,
17050
+ parseBillingEntitlement,
16734
17051
  pausedOwnCreditsText,
17052
+ poolEndsAtFrom,
16735
17053
  poolErrorMessage,
17054
+ poolRenewsAtCycleEnd,
16736
17055
  poolWorkspaceName,
16737
17056
  readSettingsCaller,
16738
17057
  rememberSettingsCaller,
@@ -16740,6 +17059,10 @@ export {
16740
17059
  roundCredits,
16741
17060
  settingsReturnTarget,
16742
17061
  sharedPoolBannerText,
17062
+ sharedPoolEndsText,
17063
+ sponsoredCreditsText,
17064
+ toLocalDateInput,
17065
+ useBillingEntitlement,
16743
17066
  useSettingsAdapters,
16744
17067
  useWorkspaceInvitePermission,
16745
17068
  useWorkspaceOrganization,