@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.js CHANGED
@@ -45,6 +45,7 @@ __export(index_exports, {
45
45
  OrganizationModal: () => OrganizationModal,
46
46
  OrganizationPicker: () => OrganizationPicker,
47
47
  POOL_ERROR_CODES: () => POOL_ERROR_CODES,
48
+ POOL_EXPIRY_MAX_MONTHS: () => POOL_EXPIRY_MAX_MONTHS,
48
49
  POOL_NOTE_MAX_LENGTH: () => POOL_NOTE_MAX_LENGTH,
49
50
  PerUserLimitsView: () => PerUserLimitsView,
50
51
  ProfileAvatar: () => ProfileAvatar,
@@ -84,23 +85,35 @@ __export(index_exports, {
84
85
  describeAllowance: () => describeAllowance,
85
86
  describeDefaultLimit: () => describeDefaultLimit,
86
87
  describeMemberLimit: () => describeMemberLimit,
88
+ entitlementFromBody: () => entitlementFromBody,
89
+ entitlementFromFailure: () => entitlementFromFailure,
90
+ expiryChoiceFor: () => expiryChoiceFor,
87
91
  findBillingError: () => findBillingError,
88
92
  findPoolError: () => findPoolError,
89
93
  formatCreditLimit: () => formatCreditLimit,
90
94
  formatCredits: () => formatCredits,
95
+ formatPoolEndsAt: () => formatPoolEndsAt,
91
96
  formatPoolSplit: () => formatPoolSplit,
92
97
  groupWorkspaceList: () => groupWorkspaceList,
98
+ hasPaidFeatures: () => hasPaidFeatures,
93
99
  isLimitBelowUsage: () => isLimitBelowUsage,
100
+ isPaidPlan: () => isPaidPlan,
101
+ isSponsoredEntitlement: () => isSponsoredEntitlement,
94
102
  joinMembersWithLimits: () => joinMembersWithLimits,
95
103
  lastUsedLabel: () => lastUsedLabel,
96
104
  limitBelowUsageUsed: () => limitBelowUsageUsed,
97
105
  limitSourceLabel: () => limitSourceLabel,
106
+ loadCurrentPlan: () => loadCurrentPlan,
98
107
  loadWorkspaceOrganization: () => loadWorkspaceOrganization,
99
108
  memberRoleUpdate: () => memberRoleUpdate,
109
+ minPoolEndDate: () => minPoolEndDate,
100
110
  minimumCreditLimit: () => minimumCreditLimit,
101
111
  ownCreditsFirstText: () => ownCreditsFirstText,
112
+ parseBillingEntitlement: () => parseBillingEntitlement,
102
113
  pausedOwnCreditsText: () => pausedOwnCreditsText,
114
+ poolEndsAtFrom: () => poolEndsAtFrom,
103
115
  poolErrorMessage: () => poolErrorMessage,
116
+ poolRenewsAtCycleEnd: () => poolRenewsAtCycleEnd,
104
117
  poolWorkspaceName: () => poolWorkspaceName,
105
118
  readSettingsCaller: () => readSettingsCaller,
106
119
  rememberSettingsCaller: () => rememberSettingsCaller,
@@ -108,6 +121,10 @@ __export(index_exports, {
108
121
  roundCredits: () => roundCredits,
109
122
  settingsReturnTarget: () => settingsReturnTarget,
110
123
  sharedPoolBannerText: () => sharedPoolBannerText,
124
+ sharedPoolEndsText: () => sharedPoolEndsText,
125
+ sponsoredCreditsText: () => sponsoredCreditsText,
126
+ toLocalDateInput: () => toLocalDateInput,
127
+ useBillingEntitlement: () => useBillingEntitlement,
111
128
  useSettingsAdapters: () => useSettingsAdapters,
112
129
  useWorkspaceInvitePermission: () => useWorkspaceInvitePermission,
113
130
  useWorkspaceOrganization: () => useWorkspaceOrganization,
@@ -9655,6 +9672,76 @@ function describeAllowance(member) {
9655
9672
  const allowance = `${credits(member.allowance)} this cycle`;
9656
9673
  return member.mode === "percent" ? `${formatPoolSplit(member)} \xB7 ${allowance}` : allowance;
9657
9674
  }
9675
+ var POOL_EXPIRY_MAX_MONTHS = 120;
9676
+ function addCalendarMonths(date, months) {
9677
+ const target = new Date(date.getFullYear(), date.getMonth() + months, 1);
9678
+ const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
9679
+ return new Date(
9680
+ target.getFullYear(),
9681
+ target.getMonth(),
9682
+ Math.min(date.getDate(), lastDay),
9683
+ date.getHours(),
9684
+ date.getMinutes(),
9685
+ date.getSeconds(),
9686
+ date.getMilliseconds()
9687
+ );
9688
+ }
9689
+ var pad2 = (n) => String(n).padStart(2, "0");
9690
+ function toLocalDateInput(date) {
9691
+ return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
9692
+ }
9693
+ function minPoolEndDate(now = /* @__PURE__ */ new Date()) {
9694
+ return toLocalDateInput(new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1));
9695
+ }
9696
+ function parseLocalDate(value) {
9697
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim());
9698
+ if (!match) return null;
9699
+ const [year, month, day] = [Number(match[1]), Number(match[2]) - 1, Number(match[3])];
9700
+ const date = new Date(year, month, day);
9701
+ return date.getFullYear() === year && date.getMonth() === month && date.getDate() === day ? date : null;
9702
+ }
9703
+ function poolEndsAtFrom(choice, now = /* @__PURE__ */ new Date()) {
9704
+ switch (choice.kind) {
9705
+ case "never":
9706
+ return { ok: true, value: null };
9707
+ case "month":
9708
+ return { ok: true, value: addCalendarMonths(now, 1).toISOString() };
9709
+ case "year":
9710
+ return { ok: true, value: addCalendarMonths(now, 12).toISOString() };
9711
+ case "months": {
9712
+ const { months } = choice;
9713
+ if (!Number.isInteger(months) || months < 1 || months > POOL_EXPIRY_MAX_MONTHS) {
9714
+ return { ok: false, reason: `Enter a whole number of months from 1 to ${POOL_EXPIRY_MAX_MONTHS}` };
9715
+ }
9716
+ return { ok: true, value: addCalendarMonths(now, months).toISOString() };
9717
+ }
9718
+ case "date": {
9719
+ if (!choice.date) return { ok: false, reason: "Choose a date" };
9720
+ const date = parseLocalDate(choice.date);
9721
+ if (!date) return { ok: false, reason: "Choose a valid date" };
9722
+ if (choice.date < minPoolEndDate(now)) return { ok: false, reason: "Choose a date after today" };
9723
+ return { ok: true, value: date.toISOString() };
9724
+ }
9725
+ }
9726
+ }
9727
+ function expiryChoiceFor(member) {
9728
+ const endsAt = member?.endsAt ? new Date(member.endsAt) : null;
9729
+ if (!endsAt || !Number.isFinite(endsAt.getTime())) return { kind: "never" };
9730
+ return { kind: "date", date: toLocalDateInput(endsAt) };
9731
+ }
9732
+ function formatPoolEndsAt(endsAt) {
9733
+ const date = endsAt ? new Date(endsAt) : null;
9734
+ if (!date || !Number.isFinite(date.getTime())) return "Never";
9735
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
9736
+ }
9737
+ function sharedPoolEndsText(pool) {
9738
+ return pool.endsAt ? `Sharing ends on ${formatPoolEndsAt(pool.endsAt)}.` : "Sharing ends: Never.";
9739
+ }
9740
+ function poolRenewsAtCycleEnd(pool) {
9741
+ if (!pool.cycleEnd) return false;
9742
+ if (!pool.endsAt) return true;
9743
+ return new Date(pool.endsAt).getTime() > new Date(pool.cycleEnd).getTime();
9744
+ }
9658
9745
  function poolWorkspaceName(name) {
9659
9746
  return name?.trim() || "Unknown workspace";
9660
9747
  }
@@ -9662,12 +9749,8 @@ function sharedPoolBannerText(pool) {
9662
9749
  const sponsor = pool.sponsorName?.trim() || "another workspace";
9663
9750
  return `Credits shared by ${sponsor}: ${formatCredits(pool.remaining)} of ${formatCredits(pool.allowance)} left this cycle`;
9664
9751
  }
9665
- function pausedOwnCreditsText(pool) {
9666
- const own = pool.ownCredits;
9667
- if (typeof own !== "number" || !Number.isFinite(own)) {
9668
- return "This workspace spends these shared credits, not its own; credits bought here can't be used while it does.";
9669
- }
9670
- 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.`;
9752
+ function pausedOwnCreditsText(_pool) {
9753
+ return ownCreditsFirstText();
9671
9754
  }
9672
9755
  function ownCreditsFirstText() {
9673
9756
  return "This workspace uses its own credits first, then these shared credits.";
@@ -9862,25 +9945,36 @@ function PoolModalShell({
9862
9945
  }
9863
9946
  );
9864
9947
  }
9865
- function PoolWarning({ children }) {
9866
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
9867
- "div",
9868
- {
9869
- role: "alert",
9870
- className: "w-full",
9871
- style: {
9872
- ...modalBodyText,
9873
- color: T16.primary,
9874
- background: T16.warningFill,
9875
- border: `1px solid ${T16.warningBorder}`,
9876
- borderRadius: T16.r3xl,
9877
- padding: `${T16.spXl} ${T16.sp3xl}`
9878
- },
9879
- children
9880
- }
9881
- );
9882
- }
9883
9948
  var fieldLabel = { ...modalBodyText, fontWeight: 500, color: T16.primary };
9949
+ var fieldBox = {
9950
+ background: T16.fillPlus,
9951
+ height: 48,
9952
+ borderRadius: T16.r3xl,
9953
+ padding: `${T16.spXl} ${T16.sp3xl}`,
9954
+ gap: T16.spXl
9955
+ };
9956
+ var bareInput = {
9957
+ ...modalBodyText,
9958
+ background: "transparent",
9959
+ border: "none",
9960
+ outline: "none",
9961
+ color: T16.primary,
9962
+ padding: 0
9963
+ };
9964
+ var EXPIRY_OPTIONS = [
9965
+ { id: "never", label: "Never" },
9966
+ { id: "month", label: "In 1 month" },
9967
+ { id: "months", label: "In a number of months" },
9968
+ { id: "year", label: "In 1 year" },
9969
+ { id: "date", label: "On a date" }
9970
+ ];
9971
+ var isExpiryKind = (id) => EXPIRY_OPTIONS.some((o) => o.id === id);
9972
+ var endsText = (member) => member.endsAt ? `, until ${formatPoolEndsAt(member.endsAt)}` : "";
9973
+ function endsAtHelp(kind, endsAt) {
9974
+ if (kind === "never") return "Shares every billing cycle until it is removed.";
9975
+ const stop = "After that, the other workspace spends only its own credits.";
9976
+ return endsAt ? `Sharing stops on ${formatPoolEndsAt(endsAt)}. ${stop}` : stop;
9977
+ }
9884
9978
  function CreditPoolMemberModal(props) {
9885
9979
  const { toast } = useSettingsAdapters();
9886
9980
  const editing = props.kind === "edit" ? props.member : null;
@@ -9891,17 +9985,28 @@ function CreditPoolMemberModal(props) {
9891
9985
  const [touched, setTouched] = (0, import_react20.useState)(false);
9892
9986
  const [saving, setSaving] = (0, import_react20.useState)(false);
9893
9987
  const [serverError, setServerError] = (0, import_react20.useState)(null);
9988
+ const initialExpiry = expiryChoiceFor(editing);
9989
+ const [expiryKind, setExpiryKind] = (0, import_react20.useState)(initialExpiry.kind);
9990
+ const [monthsRaw, setMonthsRaw] = (0, import_react20.useState)("3");
9991
+ const [endDate, setEndDate] = (0, import_react20.useState)(initialExpiry.kind === "date" ? initialExpiry.date : "");
9894
9992
  (0, import_react20.useEffect)(() => {
9895
9993
  setServerError(null);
9896
- }, [memberId, mode, raw, note]);
9994
+ }, [memberId, mode, raw, note, expiryKind, monthsRaw, endDate]);
9897
9995
  const value = validatePoolValue(mode, raw);
9898
9996
  const noteCheck = validatePoolText(note, POOL_NOTE_MAX_LENGTH);
9899
- const changed = !editing || mode !== editing.mode || !value.ok || value.value !== editing.value;
9900
- const valid = value.ok && (props.kind === "edit" || Boolean(memberId) && noteCheck.ok);
9997
+ const expiry = expiryKind === "months" ? { kind: "months", months: monthsRaw.trim() ? Number(monthsRaw) : NaN } : expiryKind === "date" ? { kind: "date", date: endDate } : { kind: expiryKind };
9998
+ const endsAtUnchanged = Boolean(editing) && expiry.kind === initialExpiry.kind && (expiry.kind !== "date" || initialExpiry.kind === "date" && expiry.date === initialExpiry.date);
9999
+ const endsAt = endsAtUnchanged ? null : poolEndsAtFrom(expiry);
10000
+ const splitChanged = !editing || mode !== editing.mode || !value.ok || value.value !== editing.value;
10001
+ const changed = splitChanged || !endsAtUnchanged;
10002
+ const valid = value.ok && (endsAt === null || endsAt.ok) && (props.kind === "edit" || Boolean(memberId) && noteCheck.ok);
9901
10003
  const canSave = !saving && changed && valid;
9902
10004
  const valueHint = touched && !value.ok ? value.reason : null;
10005
+ const expiryHint = endsAt && !endsAt.ok && !(expiry.kind === "date" && !expiry.date) ? endsAt.reason : null;
9903
10006
  const handleSave = async () => {
9904
10007
  if (!canSave || !value.ok) return;
10008
+ const sendEndsAt = endsAtUnchanged ? null : poolEndsAtFrom(expiry);
10009
+ if (sendEndsAt && !sendEndsAt.ok) return;
9905
10010
  setSaving(true);
9906
10011
  try {
9907
10012
  if (props.kind === "add") {
@@ -9909,12 +10014,17 @@ function CreditPoolMemberModal(props) {
9909
10014
  memberWorkspaceId: memberId,
9910
10015
  mode,
9911
10016
  value: value.value,
9912
- note: noteCheck.ok ? noteCheck.value : ""
10017
+ note: noteCheck.ok ? noteCheck.value : "",
10018
+ ...sendEndsAt?.value ? { endsAt: sendEndsAt.value } : {}
9913
10019
  });
9914
- toast.success(`Credits shared. ${describeAllowance(added)}.`);
10020
+ toast.success(`Credits shared. ${describeAllowance(added)}${endsText(added)}.`);
9915
10021
  } else {
9916
- const updated = await props.onSave({ mode, value: value.value });
9917
- toast.success(`Share updated. ${describeAllowance(updated)}.`);
10022
+ const updated = await props.onSave({
10023
+ mode,
10024
+ value: value.value,
10025
+ ...sendEndsAt ? { endsAt: sendEndsAt.value } : {}
10026
+ });
10027
+ toast.success(`Share updated. ${describeAllowance(updated)}${endsText(updated)}.`);
9918
10028
  }
9919
10029
  props.onSaved();
9920
10030
  props.onClose();
@@ -9999,11 +10109,7 @@ function CreditPoolMemberModal(props) {
9999
10109
  {
10000
10110
  className: "flex items-center w-full",
10001
10111
  style: {
10002
- background: T16.fillPlus,
10003
- height: 48,
10004
- borderRadius: T16.r3xl,
10005
- padding: `${T16.spXl} ${T16.sp3xl}`,
10006
- gap: T16.spXl,
10112
+ ...fieldBox,
10007
10113
  border: valueHint ? `1px solid ${T16.error}` : "1px solid transparent"
10008
10114
  },
10009
10115
  children: [
@@ -10021,14 +10127,7 @@ function CreditPoolMemberModal(props) {
10021
10127
  setTouched(true);
10022
10128
  },
10023
10129
  className: "flex-1 min-w-0",
10024
- style: {
10025
- ...modalBodyText,
10026
- background: "transparent",
10027
- border: "none",
10028
- outline: "none",
10029
- color: T16.primary,
10030
- padding: 0
10031
- }
10130
+ style: bareInput
10032
10131
  }
10033
10132
  ),
10034
10133
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "shrink-0", style: { ...modalBodyText, color: T16.muted }, children: mode === "percent" ? "%" : "credits" })
@@ -10036,10 +10135,76 @@ function CreditPoolMemberModal(props) {
10036
10135
  }
10037
10136
  ),
10038
10137
  valueHint ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { role: "alert", style: { ...modalBodyText, color: T16.error }, children: valueHint }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("span", { style: { ...modalBodyText, color: T16.tertiary }, children: [
10039
- 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.",
10138
+ 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.",
10040
10139
  editing ? " Applies immediately." : ""
10041
10140
  ] })
10042
10141
  ] }),
10142
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex flex-col w-full", style: { gap: T16.spMd }, children: [
10143
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { style: fieldLabel, children: "Ends" }),
10144
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
10145
+ import_ui14.ScSelect,
10146
+ {
10147
+ value: expiryKind,
10148
+ onChange: (id) => {
10149
+ if (isExpiryKind(id)) setExpiryKind(id);
10150
+ },
10151
+ options: EXPIRY_OPTIONS,
10152
+ disabled: saving
10153
+ }
10154
+ ),
10155
+ expiryKind === "months" && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
10156
+ "div",
10157
+ {
10158
+ className: "flex items-center w-full",
10159
+ style: {
10160
+ ...fieldBox,
10161
+ border: expiryHint ? `1px solid ${T16.error}` : "1px solid transparent"
10162
+ },
10163
+ children: [
10164
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
10165
+ "input",
10166
+ {
10167
+ inputMode: "numeric",
10168
+ "aria-label": "Number of months",
10169
+ "aria-invalid": expiryHint ? true : void 0,
10170
+ placeholder: "Enter a number of months",
10171
+ value: monthsRaw,
10172
+ disabled: saving,
10173
+ onChange: (e) => setMonthsRaw(e.target.value.replace(/\D/g, "").slice(0, 3)),
10174
+ className: "flex-1 min-w-0",
10175
+ style: bareInput
10176
+ }
10177
+ ),
10178
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "shrink-0", style: { ...modalBodyText, color: T16.muted }, children: "months" })
10179
+ ]
10180
+ }
10181
+ ),
10182
+ expiryKind === "date" && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
10183
+ "div",
10184
+ {
10185
+ className: "flex items-center w-full",
10186
+ style: {
10187
+ ...fieldBox,
10188
+ border: expiryHint ? `1px solid ${T16.error}` : "1px solid transparent"
10189
+ },
10190
+ children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
10191
+ "input",
10192
+ {
10193
+ type: "date",
10194
+ "aria-label": "End date",
10195
+ "aria-invalid": expiryHint ? true : void 0,
10196
+ min: minPoolEndDate(),
10197
+ value: endDate,
10198
+ disabled: saving,
10199
+ onChange: (e) => setEndDate(e.target.value),
10200
+ className: "flex-1 min-w-0",
10201
+ style: { ...bareInput, colorScheme: "light dark" }
10202
+ }
10203
+ )
10204
+ }
10205
+ ),
10206
+ expiryHint ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { role: "alert", style: { ...modalBodyText, color: T16.error }, children: expiryHint }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { style: { ...modalBodyText, color: T16.tertiary }, children: endsAtHelp(expiryKind, endsAtUnchanged ? editing?.endsAt ?? null : endsAt?.ok ? endsAt.value : null) })
10207
+ ] }),
10043
10208
  props.kind === "add" && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex flex-col w-full", style: { gap: T16.spXs }, children: [
10044
10209
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { style: fieldLabel, children: "Note" }),
10045
10210
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
@@ -10061,7 +10226,6 @@ function CreditPoolMemberModal(props) {
10061
10226
  POOL_NOTE_MAX_LENGTH
10062
10227
  ] })
10063
10228
  ] }),
10064
- props.kind === "add" && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(PoolWarning, { children: "While it spends shared credits, the other workspace uses only this workspace's credits, not its own." }),
10065
10229
  serverError && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { role: "alert", style: { ...modalBodyText, color: T16.error }, children: serverError })
10066
10230
  ]
10067
10231
  }
@@ -10182,7 +10346,9 @@ var T17 = {
10182
10346
  r3xl: "var(--radius-3xl)",
10183
10347
  rFull: "var(--radius-full)"
10184
10348
  };
10185
- var MEMBER_COLUMNS2 = "minmax(220px, 1fr) 220px 110px 110px 150px";
10349
+ var MEMBER_COLUMNS2 = "minmax(180px, 1fr) 150px 80px 90px 110px 150px";
10350
+ var MEMBER_GAP = 16;
10351
+ var MEMBER_MIN_WIDTH = 880;
10186
10352
  var cellText2 = {
10187
10353
  fontFamily: T17.font,
10188
10354
  fontSize: T17.fsSm,
@@ -10195,7 +10361,7 @@ var tableHeader = (columns, labels) => /* @__PURE__ */ (0, import_jsx_runtime28.
10195
10361
  style: {
10196
10362
  display: "grid",
10197
10363
  gridTemplateColumns: columns,
10198
- gap: 24,
10364
+ gap: MEMBER_GAP,
10199
10365
  padding: "8px 16px",
10200
10366
  fontSize: 11,
10201
10367
  color: T17.muted,
@@ -10219,17 +10385,21 @@ function MemberRow2({
10219
10385
  gridTemplateColumns: MEMBER_COLUMNS2,
10220
10386
  alignItems: "center",
10221
10387
  padding: `10px ${T17.sp3xl}`,
10222
- gap: 24
10388
+ gap: MEMBER_GAP
10223
10389
  },
10224
10390
  children: [
10225
10391
  /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex flex-col min-w-0", style: { gap: 2 }, children: [
10226
10392
  /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "truncate", style: { ...cellText2, fontWeight: 500 }, children: poolWorkspaceName(member.memberWorkspaceName) }),
10227
10393
  member.note && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "truncate", title: member.note, style: { ...cellText2, color: T17.tertiary }, children: member.note })
10228
10394
  ] }),
10229
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { style: cellText2, children: describeAllowance(member) }),
10395
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex flex-col min-w-0", style: { gap: 2 }, children: [
10396
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { style: cellText2, children: formatPoolSplit({ mode: "fixed", value: member.allowance }) }),
10397
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { style: { ...cellText2, color: T17.tertiary }, children: member.mode === "percent" ? `${formatPoolSplit(member)} \xB7 this cycle` : "this cycle" })
10398
+ ] }),
10230
10399
  /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { style: cellText2, children: formatCredits(member.used) }),
10231
10400
  /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { style: { ...cellText2, color: spent ? T17.warning : T17.primary }, children: formatCredits(member.remaining) }),
10232
- /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex items-center", style: { gap: 8 }, children: [
10401
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { style: { ...cellText2, color: member.endsAt ? T17.primary : T17.muted }, children: formatPoolEndsAt(member.endsAt) }),
10402
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex items-center whitespace-nowrap", style: { gap: 8 }, children: [
10233
10403
  onEdit && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui16.ScButton, { text: "Edit", variant: "secondary", size: "sm", onClick: onEdit }),
10234
10404
  onRemove && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_ui16.ScButton, { text: "Remove", variant: "secondary", size: "sm", onClick: onRemove })
10235
10405
  ] })
@@ -10355,11 +10525,13 @@ function SharedCreditsView() {
10355
10525
  remaining: sponsor.remaining
10356
10526
  }) }),
10357
10527
  /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("span", { style: { ...cellText2, color: T17.muted }, children: [
10358
- "This workspace spends ",
10528
+ "This workspace uses its own credits first, then",
10529
+ " ",
10359
10530
  poolWorkspaceName(sponsor.sponsorWorkspaceName),
10360
- "'s credits, not its own",
10361
- sponsor.cycle?.end ? `, until its allowance starts again on ${formatDate2(sponsor.cycle.end)}` : "",
10362
- "."
10531
+ "'s, up to its allowance",
10532
+ sponsor.cycle?.end && poolRenewsAtCycleEnd({ cycleEnd: sponsor.cycle.end, endsAt: sponsor.endsAt }) ? `, which starts again on ${formatDate2(sponsor.cycle.end)}` : "",
10533
+ ". ",
10534
+ sharedPoolEndsText(sponsor)
10363
10535
  ] })
10364
10536
  ]
10365
10537
  }
@@ -10371,8 +10543,8 @@ function SharedCreditsView() {
10371
10543
  {
10372
10544
  grow: "cap",
10373
10545
  maxHeight: 420,
10374
- contentMinWidth: 860,
10375
- header: tableHeader(MEMBER_COLUMNS2, ["Workspace", "Allowance", "Used", "Remaining", "Actions"]),
10546
+ contentMinWidth: MEMBER_MIN_WIDTH,
10547
+ header: tableHeader(MEMBER_COLUMNS2, ["Workspace", "Allowance", "Used", "Remaining", "Ends", "Actions"]),
10376
10548
  isEmpty: failed || members.length === 0,
10377
10549
  emptyText: failed ? "Couldn't load shared credits. Try again later." : "This workspace doesn't share its credits with any other workspace.",
10378
10550
  children: members.map((member) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
@@ -10422,6 +10594,71 @@ function SharedCreditsView() {
10422
10594
  ] });
10423
10595
  }
10424
10596
 
10597
+ // src/entitlement.ts
10598
+ function parseBillingEntitlement(value) {
10599
+ if (!value || typeof value !== "object") return null;
10600
+ const raw = value;
10601
+ if (typeof raw.paid !== "boolean") return null;
10602
+ const source = raw.source === "plan" || raw.source === "pool" ? raw.source : null;
10603
+ const entitlement = { paid: raw.paid, source };
10604
+ if (typeof raw.sponsorWorkspaceId === "string") entitlement.sponsorWorkspaceId = raw.sponsorWorkspaceId;
10605
+ if (typeof raw.sponsorName === "string") entitlement.sponsorName = raw.sponsorName;
10606
+ if (typeof raw.endsAt === "string" || raw.endsAt === null) entitlement.endsAt = raw.endsAt;
10607
+ return entitlement;
10608
+ }
10609
+ function entitlementFromBody(body) {
10610
+ if (!body || typeof body !== "object") return null;
10611
+ return parseBillingEntitlement(body.entitlement);
10612
+ }
10613
+ function entitlementFromFailure(failure) {
10614
+ const seen = /* @__PURE__ */ new Set();
10615
+ const visit = (value, depth) => {
10616
+ if (depth > 4 || value == null || typeof value !== "object" || seen.has(value)) return null;
10617
+ seen.add(value);
10618
+ const found = entitlementFromBody(value);
10619
+ if (found) return found;
10620
+ const error = value;
10621
+ const response = error.response;
10622
+ for (const child of [error.body, error.data, response?.data, error.cause]) {
10623
+ const nested = visit(child, depth + 1);
10624
+ if (nested) return nested;
10625
+ }
10626
+ return null;
10627
+ };
10628
+ return visit(failure, 0);
10629
+ }
10630
+ function isPaidPlan(subscription) {
10631
+ const amount = subscription?.plan?.amount;
10632
+ return amount != null && amount > 0;
10633
+ }
10634
+ function hasPaidFeatures({
10635
+ entitlement,
10636
+ subscription
10637
+ }) {
10638
+ return entitlement ? entitlement.paid : isPaidPlan(subscription);
10639
+ }
10640
+ function isSponsoredEntitlement(entitlement) {
10641
+ return entitlement?.source === "pool" && entitlement.paid;
10642
+ }
10643
+ function sponsoredCreditsText(entitlement) {
10644
+ const sponsor = entitlement.sponsorName?.trim() || "another workspace";
10645
+ const ends = entitlement.endsAt ? ` Sharing ends on ${formatPoolEndsAt(entitlement.endsAt)}.` : "";
10646
+ return `Credits shared by ${sponsor}.${ends}`;
10647
+ }
10648
+ async function loadCurrentPlan(fetchCurrentPlan) {
10649
+ let response;
10650
+ try {
10651
+ response = await fetchCurrentPlan();
10652
+ } catch (failure) {
10653
+ const entitlement2 = entitlementFromFailure(failure);
10654
+ if (!entitlement2) throw failure;
10655
+ return { subscription: null, entitlement: entitlement2, paid: entitlement2.paid };
10656
+ }
10657
+ const subscription = response?.data ?? null;
10658
+ const entitlement = entitlementFromBody(response);
10659
+ return { subscription, entitlement, paid: hasPaidFeatures({ entitlement, subscription }) };
10660
+ }
10661
+
10425
10662
  // src/billing-content.tsx
10426
10663
  var import_jsx_runtime29 = require("react/jsx-runtime");
10427
10664
  var DEFAULT_TOPUP_SERVICE = "catalogix";
@@ -11553,7 +11790,8 @@ function BillingContent({
11553
11790
  showPerUserLimits: showPerUserLimitsProp,
11554
11791
  onShowPerUserLimitsChange,
11555
11792
  showSharedCredits: showSharedCreditsProp,
11556
- onShowSharedCreditsChange
11793
+ onShowSharedCreditsChange,
11794
+ onEntitlementChange
11557
11795
  } = {}) {
11558
11796
  const { auth, workspace, organization, api, toast, navigation } = useSettingsAdapters();
11559
11797
  const user = auth.user;
@@ -11592,6 +11830,9 @@ function BillingContent({
11592
11830
  const [showExportModal, setShowExportModal] = (0, import_react23.useState)(false);
11593
11831
  const [showBuyCreditsModal, setShowBuyCreditsModal] = (0, import_react23.useState)(false);
11594
11832
  const [subscription, setSubscription] = (0, import_react23.useState)(null);
11833
+ const [entitlement, setEntitlement] = (0, import_react23.useState)(
11834
+ null
11835
+ );
11595
11836
  const [availableCredits, setAvailableCredits] = (0, import_react23.useState)(null);
11596
11837
  const [sharedPool, setSharedPool] = (0, import_react23.useState)(null);
11597
11838
  const [usedByNames, setUsedByNames] = (0, import_react23.useState)({});
@@ -11637,12 +11878,15 @@ function BillingContent({
11637
11878
  try {
11638
11879
  console.log("calling Billing apis");
11639
11880
  const [subRes, creditsRes, productsRes, customRes] = await Promise.all([
11640
- api.fetchCurrentPlan().catch(() => null),
11881
+ // A sponsored workspace with no subscription of its own gets a 404
11882
+ // that carries its entitlement: loadCurrentPlan answers it, not throws.
11883
+ loadCurrentPlan(api.fetchCurrentPlan).catch(() => null),
11641
11884
  api.fetchAvailableCredits().catch(() => null),
11642
11885
  api.fetchPlans().catch(() => null),
11643
11886
  api.getCustomCreditInfo?.().catch(() => null) ?? Promise.resolve(null)
11644
11887
  ]);
11645
- if (subRes?.data) setSubscription(subRes.data);
11888
+ if (subRes?.subscription) setSubscription(subRes.subscription);
11889
+ if (subRes) setEntitlement(subRes.entitlement);
11646
11890
  if (creditsRes?.availableCredits != null)
11647
11891
  setAvailableCredits(creditsRes.availableCredits);
11648
11892
  setSharedPool(creditsRes?.sharedPool ?? null);
@@ -11812,6 +12056,7 @@ function BillingContent({
11812
12056
  setInitialFetchTriggered(false);
11813
12057
  setBillingOverviewLoaded(false);
11814
12058
  setSubscription(null);
12059
+ setEntitlement(null);
11815
12060
  setAvailableCredits(null);
11816
12061
  setProducts([]);
11817
12062
  setTotalCredits(null);
@@ -11867,7 +12112,16 @@ function BillingContent({
11867
12112
  subscription?.scheduledDowngradeEffectiveDate
11868
12113
  );
11869
12114
  const scheduledDowngradeText = subscription?.downgradeScheduled && subscription.plan?.name ? `Downgrades to ${subscription.plan.name}${scheduledDowngradeDateLabel ? ` ${scheduledDowngradeDateLabel}` : ""}` : void 0;
11870
- const isPlanDetailsLoading = !billingOverviewLoaded || loadingBilling || subscription?.plan == null || !planName || planName === "-" || !planAmount || currentPlanCredits == null;
12115
+ const sponsored = isSponsoredEntitlement(entitlement);
12116
+ const sponsoredWithoutPlan = sponsored && billingOverviewLoaded && !loadingBilling && subscription == null;
12117
+ const isPlanDetailsLoading = sponsoredWithoutPlan ? false : !billingOverviewLoaded || loadingBilling || subscription?.plan == null || !planName || planName === "-" || !planAmount || currentPlanCredits == null;
12118
+ const paidFeatures = hasPaidFeatures({ entitlement, subscription });
12119
+ const onEntitlementChangeRef = (0, import_react23.useRef)(onEntitlementChange);
12120
+ onEntitlementChangeRef.current = onEntitlementChange;
12121
+ (0, import_react23.useEffect)(() => {
12122
+ if (!billingOverviewLoaded) return;
12123
+ onEntitlementChangeRef.current?.({ entitlement, paid: paidFeatures });
12124
+ }, [billingOverviewLoaded, entitlement, paidFeatures]);
11871
12125
  const {
11872
12126
  used: usedCredits,
11873
12127
  remaining: remainingCredits,
@@ -12104,9 +12358,9 @@ function BillingContent({
12104
12358
  /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
12105
12359
  import_ui17.ScPlanDetailsCard,
12106
12360
  {
12107
- planName,
12108
- planCredits: planCreditsText,
12109
- planPrice: planAmount,
12361
+ planName: sponsoredWithoutPlan ? "Free" : planName,
12362
+ planCredits: sponsoredWithoutPlan ? "Shared credits" : planCreditsText,
12363
+ planPrice: sponsoredWithoutPlan ? `${workspace.current?.country?.trim().toUpperCase() === "IN" ? "\u20B9" : "$"}0.00 / month` : planAmount,
12110
12364
  badgeText: "Active",
12111
12365
  badgeSubText: scheduledDowngradeText,
12112
12366
  currentPlanLabel: "Current plan",
@@ -12188,12 +12442,57 @@ function BillingContent({
12188
12442
  },
12189
12443
  children: [
12190
12444
  ownCreditsFirstText(),
12191
- sharedPool.cycleEnd ? ` The allowance starts again on ${formatDate3(sharedPool.cycleEnd)}.` : ""
12445
+ poolRenewsAtCycleEnd(sharedPool) ? ` The allowance starts again on ${formatDate3(sharedPool.cycleEnd)}.` : "",
12446
+ ` ${sharedPoolEndsText(sharedPool)}`
12192
12447
  ]
12193
12448
  }
12194
12449
  )
12195
12450
  ]
12196
12451
  }
12452
+ ) : sponsored && entitlement ? (
12453
+ // Billing says a pool entitles it, but the balance carried no
12454
+ // allowance (an older credits answer, or it failed): say whose.
12455
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
12456
+ "div",
12457
+ {
12458
+ role: "status",
12459
+ className: "flex flex-col shrink-0 w-full",
12460
+ style: {
12461
+ gap: 4,
12462
+ padding: 16,
12463
+ backgroundColor: "var(--alias-surface-base)",
12464
+ border: "1px solid var(--alias-border-subtle)",
12465
+ borderRadius: "var(--radius-3xl)"
12466
+ },
12467
+ children: [
12468
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
12469
+ "span",
12470
+ {
12471
+ style: {
12472
+ fontFamily: "var(--font-family-font-family-body)",
12473
+ fontSize: "var(--font-size-font-size-sm)",
12474
+ fontWeight: 500,
12475
+ lineHeight: "var(--line-height-line-height-sm)",
12476
+ color: "var(--alias-text---icons-primary)"
12477
+ },
12478
+ children: sponsoredCreditsText(entitlement)
12479
+ }
12480
+ ),
12481
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
12482
+ "span",
12483
+ {
12484
+ style: {
12485
+ fontFamily: "var(--font-family-font-family-body)",
12486
+ fontSize: "var(--font-size-font-size-sm)",
12487
+ lineHeight: "var(--line-height-line-height-sm)",
12488
+ color: "var(--alias-text---icons-muted)"
12489
+ },
12490
+ children: ownCreditsFirstText()
12491
+ }
12492
+ )
12493
+ ]
12494
+ }
12495
+ )
12197
12496
  ) : null,
12198
12497
  usage ? /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
12199
12498
  "div",
@@ -12436,7 +12735,7 @@ var import_react24 = require("react");
12436
12735
  var import_ui18 = require("@streamoid/ui");
12437
12736
  var import_icons16 = require("@streamoid/icons");
12438
12737
  var import_jsx_runtime30 = require("react/jsx-runtime");
12439
- var EXPIRY_OPTIONS = [
12738
+ var EXPIRY_OPTIONS2 = [
12440
12739
  { label: "30 days", value: "30d" },
12441
12740
  { label: "60 days", value: "60d" },
12442
12741
  { label: "90 days", value: "90d" },
@@ -12627,7 +12926,7 @@ function CreateTokenModal({ onClose, onCreated }) {
12627
12926
  children: "Expiry"
12628
12927
  }
12629
12928
  ) }),
12630
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { style: { display: "flex", gap: "var(--spacing-md)", flexWrap: "wrap" }, children: EXPIRY_OPTIONS.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
12929
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { style: { display: "flex", gap: "var(--spacing-md)", flexWrap: "wrap" }, children: EXPIRY_OPTIONS2.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
12631
12930
  import_ui18.ScButton,
12632
12931
  {
12633
12932
  text: opt.label,
@@ -16568,6 +16867,30 @@ function OrganizationPicker({
16568
16867
  );
16569
16868
  }
16570
16869
 
16870
+ // src/use-billing-entitlement.ts
16871
+ var import_react30 = require("react");
16872
+ function useBillingEntitlement(fetchCurrentPlan, key) {
16873
+ const fetchRef = (0, import_react30.useRef)(fetchCurrentPlan);
16874
+ fetchRef.current = fetchCurrentPlan;
16875
+ const [state, setState] = (0, import_react30.useState)(null);
16876
+ const [loading, setLoading] = (0, import_react30.useState)(false);
16877
+ (0, import_react30.useEffect)(() => {
16878
+ const fetch2 = fetchRef.current;
16879
+ setState(null);
16880
+ if (!key || !fetch2) {
16881
+ setLoading(false);
16882
+ return;
16883
+ }
16884
+ let live = true;
16885
+ setLoading(true);
16886
+ loadCurrentPlan(fetch2).then((next) => live && setState(next)).catch(() => live && setState(null)).finally(() => live && setLoading(false));
16887
+ return () => {
16888
+ live = false;
16889
+ };
16890
+ }, [key]);
16891
+ return { state, loading };
16892
+ }
16893
+
16571
16894
  // src/administration-navigation.ts
16572
16895
  var normalizedBase = (base) => base.replace(/\/$/, "");
16573
16896
  var returnKey = (base, entryKey) => `streamoid:settings:return:${base}:${entryKey}`;
@@ -16614,7 +16937,7 @@ function readSettingsCaller(base, state, entryKey, fallback = `${normalizedBase(
16614
16937
  }
16615
16938
 
16616
16939
  // src/app-settings-page.tsx
16617
- var import_react30 = require("react");
16940
+ var import_react31 = require("react");
16618
16941
  var import_icons20 = require("@streamoid/icons");
16619
16942
 
16620
16943
  // src/app-settings-page.module.css
@@ -16677,7 +17000,7 @@ function AppSettingsSection({ title, action, children }) {
16677
17000
  ] });
16678
17001
  }
16679
17002
  function AppSettingsRow({ title, description, summary, href, onClick, disabled, disabledReason, ariaLabel, trailing }) {
16680
- const reasonId = (0, import_react30.useId)();
17003
+ const reasonId = (0, import_react31.useId)();
16681
17004
  const content = /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
16682
17005
  /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("span", { className: app_settings_page_default.rowCopy, children: [
16683
17006
  /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: app_settings_page_default.rowTitle, children: title }),