@base316/react 0.2.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @base316/react
2
2
 
3
+ ## 0.3.0 — 2026-10-03
4
+
5
+ Requires `@base316/sdk` 0.3.0.
6
+
7
+ ### Added
8
+
9
+ - **White-label for a tenant**: `Base316Provider` takes `tenant` —
10
+ `{ organizationId }` or `{ organizationExternalId }` — and every component
11
+ under it shows that tenant's branding (its own, then its parent's, then the
12
+ environment's). Changing it restyles without a reload; an inline object does
13
+ not refetch.
14
+ - `TeamMembers` says when a team is reached through the organization that
15
+ manages it rather than by membership.
16
+ - `SubscriptionManager` and `PricingTable` know when an account's plan is
17
+ provided by the organization that manages it: the plan is shown with the
18
+ provider named, and nothing can be bought, changed or cancelled.
19
+ `usePlans()` returns `managedBy`.
20
+
3
21
  ## 0.2.0
4
22
 
5
23
  The first release since `0.1.0` on 13 September. The package shipped **8
package/README.md CHANGED
@@ -79,6 +79,20 @@ inherits your application's styles automatically — silently absorbing whatever
79
79
  `body { font }` and `button { }` rules happen to exist produces components that
80
80
  look broken in ways neither side can predict.
81
81
 
82
+ ## White-label for a tenant
83
+
84
+ When one business runs your product for others — a firm and its clients — show
85
+ the firm's brand on the screens its clients see:
86
+
87
+ ```tsx
88
+ <Base316Provider publishableKey={key} tenant={{ organizationExternalId: firm.id }}>
89
+ ```
90
+
91
+ The tenant's own branding, then its parent's, then your environment's, field
92
+ by field. Change `tenant` when the person switches team and every component
93
+ restyles. `TeamMembers` says so when a team is reached through the firm rather
94
+ than by membership.
95
+
82
96
  ## Reading the session
83
97
 
84
98
  ```tsx
package/dist/index.cjs CHANGED
@@ -204,6 +204,10 @@ var STYLESHEET = `
204
204
  align-items: stretch;
205
205
  }
206
206
 
207
+ /* Messages above the cards span the row rather than taking a card's column. */
208
+ .b316-plans-error,
209
+ .b316-plans-managed { grid-column: 1 / -1; }
210
+
207
211
  .b316-plan {
208
212
  display: flex;
209
213
  flex-direction: column;
@@ -666,6 +670,7 @@ function Base316Provider({
666
670
  token: controlledToken,
667
671
  onTokenChange,
668
672
  appearance: appearance2,
673
+ tenant,
669
674
  className,
670
675
  children
671
676
  }) {
@@ -694,16 +699,20 @@ function Base316Provider({
694
699
  }
695
700
  setInternalToken(readStoredToken());
696
701
  }, [controlled]);
702
+ const tenantKey = tenant ? "organizationId" in tenant ? `id:${tenant.organizationId}` : `ext:${tenant.organizationExternalId}` : null;
697
703
  (0, import_react.useEffect)(() => {
698
704
  let cancelled = false;
699
- client.billing.branding().then((result) => {
705
+ const request = tenantKey ? client.billing.tenantBranding(
706
+ tenantKey.startsWith("id:") ? { organizationId: tenantKey.slice(3) } : { organizationExternalId: tenantKey.slice(4) }
707
+ ) : client.billing.branding();
708
+ request.then((result) => {
700
709
  if (!cancelled) setBranding(result);
701
710
  }).catch(() => {
702
711
  });
703
712
  return () => {
704
713
  cancelled = true;
705
714
  };
706
- }, [client]);
715
+ }, [client, tenantKey]);
707
716
  const setToken = (0, import_react.useCallback)(
708
717
  (next) => {
709
718
  if (controlled) {
@@ -1002,6 +1011,7 @@ function SignIn({
1002
1011
  onSignedIn,
1003
1012
  passkeys = true,
1004
1013
  magicLinkRedirectUrl,
1014
+ phone = false,
1005
1015
  className,
1006
1016
  style
1007
1017
  }) {
@@ -1010,6 +1020,11 @@ function SignIn({
1010
1020
  const [password, setPassword] = (0, import_react3.useState)("");
1011
1021
  const [code, setCode] = (0, import_react3.useState)("");
1012
1022
  const [pendingChallenge, setPendingChallenge] = (0, import_react3.useState)(null);
1023
+ const [pendingFactors, setPendingFactors] = (0, import_react3.useState)([]);
1024
+ const [phoneNumber, setPhoneNumber] = (0, import_react3.useState)("");
1025
+ const [phoneChallenge, setPhoneChallenge] = (0, import_react3.useState)(
1026
+ null
1027
+ );
1013
1028
  const [error, setError] = (0, import_react3.useState)(null);
1014
1029
  const [busy, setBusy] = (0, import_react3.useState)(false);
1015
1030
  const [passkeyReady, setPasskeyReady] = (0, import_react3.useState)(false);
@@ -1116,9 +1131,30 @@ function SignIn({
1116
1131
  onSignedIn?.();
1117
1132
  return;
1118
1133
  }
1134
+ if (mode === "phone") {
1135
+ if (!phoneChallenge) {
1136
+ const started = await client.auth.phone.start({ phone: phoneNumber });
1137
+ setPhoneChallenge({ token: started.challengeToken, hint: started.destinationHint });
1138
+ return;
1139
+ }
1140
+ const signedIn = await client.auth.phone.verify({
1141
+ challengeToken: phoneChallenge.token,
1142
+ code
1143
+ });
1144
+ if ((0, import_sdk2.needsSecondFactor)(signedIn)) {
1145
+ setPendingChallenge(signedIn.challengeToken);
1146
+ setPendingFactors(signedIn.factors);
1147
+ setCode("");
1148
+ return;
1149
+ }
1150
+ setToken(signedIn.token);
1151
+ onSignedIn?.();
1152
+ return;
1153
+ }
1119
1154
  const result = await client.auth.signIn({ email, password });
1120
1155
  if ((0, import_sdk2.needsSecondFactor)(result)) {
1121
1156
  setPendingChallenge(result.challengeToken);
1157
+ setPendingFactors(result.factors);
1122
1158
  return;
1123
1159
  }
1124
1160
  setToken(result.token);
@@ -1126,13 +1162,20 @@ function SignIn({
1126
1162
  } catch (cause) {
1127
1163
  console.error("[base316] sign-in failed", cause);
1128
1164
  setError(
1129
- pendingChallenge ? "That code was not accepted." : "That email and password did not match."
1165
+ pendingChallenge ? "That code was not accepted." : mode === "phone" ? phoneChallenge ? "That code did not work. Check it, or ask for a new one." : (
1166
+ // Why a text could not be sent is worth saying — a number in the
1167
+ // wrong format, or too many requests — and the server words it
1168
+ // without saying anything about accounts.
1169
+ cause instanceof Error && cause.message ? cause.message : "We could not send a code. Try again."
1170
+ ) : "That email and password did not match."
1130
1171
  );
1131
1172
  } finally {
1132
1173
  setBusy(false);
1133
1174
  }
1134
1175
  }
1135
1176
  const magicAvailable = Boolean(magicLinkRedirectUrl) && !pendingChallenge;
1177
+ const tabsShown = (magicAvailable || phone) && !pendingChallenge;
1178
+ const deliveredFactor = pendingFactors.find((factor) => factor.destinationHint);
1136
1179
  if (linkSent) {
1137
1180
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: cls("b316-card", className), style, children: [
1138
1181
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontWeight: 600, marginBottom: 6 }, children: "Check your email" }),
@@ -1155,7 +1198,7 @@ function SignIn({
1155
1198
  }
1156
1199
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("form", { onSubmit: submit, className: cls("b316-card", className), style, children: [
1157
1200
  error ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "b316-error", role: "alert", children: error }) : null,
1158
- magicAvailable ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "b316-tabs", children: [
1201
+ tabsShown ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "b316-tabs", children: [
1159
1202
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1160
1203
  "button",
1161
1204
  {
@@ -1165,7 +1208,7 @@ function SignIn({
1165
1208
  children: "Password"
1166
1209
  }
1167
1210
  ),
1168
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1211
+ magicAvailable ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1169
1212
  "button",
1170
1213
  {
1171
1214
  type: "button",
@@ -1173,7 +1216,16 @@ function SignIn({
1173
1216
  onClick: () => setMode("magic"),
1174
1217
  children: "Magic link"
1175
1218
  }
1176
- )
1219
+ ) : null,
1220
+ phone ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1221
+ "button",
1222
+ {
1223
+ type: "button",
1224
+ className: cls("b316-tab", mode === "phone" ? "b316-tab-on" : void 0),
1225
+ onClick: () => setMode("phone"),
1226
+ children: "Text message"
1227
+ }
1228
+ ) : null
1177
1229
  ] }) : null,
1178
1230
  pendingChallenge ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "b316-field", children: [
1179
1231
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { className: "b316-label", htmlFor: "b316-code", children: "Authentication code" }),
@@ -1189,7 +1241,55 @@ function SignIn({
1189
1241
  required: true
1190
1242
  }
1191
1243
  ),
1192
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "b316-muted", children: "From your authenticator app, or one of your recovery codes." })
1244
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "b316-muted", children: deliveredFactor ? `We sent a code to ${deliveredFactor.destinationHint}. Enter it, or one of your recovery codes.` : "From your authenticator app, or one of your recovery codes." })
1245
+ ] }) : mode === "phone" ? phoneChallenge ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "b316-field", children: [
1246
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { className: "b316-label", htmlFor: "b316-phone-code", children: "Code" }),
1247
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1248
+ "input",
1249
+ {
1250
+ id: "b316-phone-code",
1251
+ className: "b316-input",
1252
+ value: code,
1253
+ onChange: (event) => setCode(event.target.value),
1254
+ autoComplete: "one-time-code",
1255
+ inputMode: "numeric",
1256
+ required: true
1257
+ }
1258
+ ),
1259
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "b316-muted", children: [
1260
+ "We sent a code to ",
1261
+ phoneChallenge.hint,
1262
+ ". It expires in ten minutes.",
1263
+ " ",
1264
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1265
+ "button",
1266
+ {
1267
+ type: "button",
1268
+ className: "b316-button-quiet",
1269
+ onClick: () => {
1270
+ setPhoneChallenge(null);
1271
+ setCode("");
1272
+ },
1273
+ children: "Use a different number"
1274
+ }
1275
+ )
1276
+ ] })
1277
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "b316-field", children: [
1278
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { className: "b316-label", htmlFor: "b316-phone", children: "Mobile number" }),
1279
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1280
+ "input",
1281
+ {
1282
+ id: "b316-phone",
1283
+ className: "b316-input",
1284
+ type: "tel",
1285
+ value: phoneNumber,
1286
+ onChange: (event) => setPhoneNumber(event.target.value),
1287
+ autoComplete: "tel",
1288
+ placeholder: "+14155550123",
1289
+ required: true
1290
+ }
1291
+ ),
1292
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "b316-muted", children: "Include the country code." })
1193
1293
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1194
1294
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "b316-field", children: [
1195
1295
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { className: "b316-label", htmlFor: "b316-email", children: "Email" }),
@@ -1222,7 +1322,7 @@ function SignIn({
1222
1322
  )
1223
1323
  ] }) : null
1224
1324
  ] }),
1225
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "b316-button", type: "submit", disabled: busy, children: busy ? "Working\u2026" : pendingChallenge ? "Verify" : mode === "magic" ? "Email me a link" : "Sign in" }),
1325
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "b316-button", type: "submit", disabled: busy, children: busy ? "Working\u2026" : pendingChallenge ? "Verify" : mode === "magic" ? "Email me a link" : mode === "phone" && !phoneChallenge ? "Text me a code" : "Sign in" }),
1226
1326
  passkeyReady && !pendingChallenge ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1227
1327
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "b316-muted", style: { textAlign: "center", margin: "10px 0" }, children: "or" }),
1228
1328
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -1837,6 +1937,7 @@ var import_react10 = require("react");
1837
1937
  var import_jsx_runtime12 = require("react/jsx-runtime");
1838
1938
  function TwoFactorManager({
1839
1939
  enabled = false,
1940
+ allowSms = false,
1840
1941
  onEnabled,
1841
1942
  onDisabled,
1842
1943
  className,
@@ -1846,6 +1947,7 @@ function TwoFactorManager({
1846
1947
  const [stage, setStage] = (0, import_react10.useState)({ name: "idle" });
1847
1948
  const [on, setOn] = (0, import_react10.useState)(enabled);
1848
1949
  const [code, setCode] = (0, import_react10.useState)("");
1950
+ const [phone, setPhone] = (0, import_react10.useState)("");
1849
1951
  const [error, setError] = (0, import_react10.useState)(null);
1850
1952
  const [busy, setBusy] = (0, import_react10.useState)(false);
1851
1953
  if (!token) {
@@ -1875,6 +1977,21 @@ function TwoFactorManager({
1875
1977
  setBusy(false);
1876
1978
  }
1877
1979
  }
1980
+ async function beginSms() {
1981
+ setBusy(true);
1982
+ setError(null);
1983
+ try {
1984
+ const enrollment = await client.auth.mfa.enrollSms(
1985
+ token,
1986
+ phone.trim() ? { phone: phone.trim() } : {}
1987
+ );
1988
+ setStage({ name: "enrollingEmail", ...enrollment });
1989
+ } catch (cause) {
1990
+ setError(cause instanceof Error ? cause.message : "Could not send a code.");
1991
+ } finally {
1992
+ setBusy(false);
1993
+ }
1994
+ }
1878
1995
  async function confirm() {
1879
1996
  if (stage.name !== "enrolling" && stage.name !== "enrollingEmail") {
1880
1997
  return;
@@ -1917,6 +2034,33 @@ function TwoFactorManager({
1917
2034
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { children: "Save these recovery codes somewhere safe. Each one signs you in once if you lose your authenticator, and they are not shown again." }),
1918
2035
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ul", { className: "b316-mfa-code-list", children: stage.recoveryCodes.map((recovery) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("code", { children: recovery }) }, recovery)) }),
1919
2036
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: "b316-button", onClick: () => setStage({ name: "idle" }), children: "I have saved them" })
2037
+ ] }) : stage.name === "smsNumber" ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "b316-mfa-enrol", children: [
2038
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { children: "We will text a code to this number now, and each time you sign in." }),
2039
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("label", { className: "b316-label", htmlFor: "b316-mfa-phone", children: "Mobile number" }),
2040
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2041
+ "input",
2042
+ {
2043
+ id: "b316-mfa-phone",
2044
+ className: "b316-input",
2045
+ type: "tel",
2046
+ autoComplete: "tel",
2047
+ placeholder: "+14155550123",
2048
+ value: phone,
2049
+ onChange: (event) => setPhone(event.target.value)
2050
+ }
2051
+ ),
2052
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "b316-mfa-actions", children: [
2053
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2054
+ "button",
2055
+ {
2056
+ type: "button",
2057
+ className: "b316-button-quiet",
2058
+ onClick: () => setStage({ name: "idle" }),
2059
+ children: "Cancel"
2060
+ }
2061
+ ),
2062
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: "b316-button", disabled: busy, onClick: beginSms, children: busy ? "Sending\u2026" : "Send code" })
2063
+ ] })
1920
2064
  ] }) : stage.name === "enrollingEmail" ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "b316-mfa-enrol", children: [
1921
2065
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("p", { children: [
1922
2066
  "We sent a code to ",
@@ -2018,7 +2162,17 @@ function TwoFactorManager({
2018
2162
  onClick: beginEmail,
2019
2163
  children: "Email me a code"
2020
2164
  }
2021
- )
2165
+ ),
2166
+ allowSms ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2167
+ "button",
2168
+ {
2169
+ type: "button",
2170
+ className: "b316-button-quiet",
2171
+ disabled: busy,
2172
+ onClick: () => setStage({ name: "smsNumber" }),
2173
+ children: "Text me a code"
2174
+ }
2175
+ ) : null
2022
2176
  ] })
2023
2177
  ] })
2024
2178
  ] });
@@ -2350,6 +2504,16 @@ function TeamMembers({
2350
2504
  }
2351
2505
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: cls("b316-card b316-team", className), style, children: [
2352
2506
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h3", { className: "b316-team-title", children: team?.name ?? "Team" }),
2507
+ team?.inherited ? (
2508
+ // Not a member of this team, so not in the list below and with nothing
2509
+ // to leave: access comes from the team that manages it, and ends there.
2510
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "b316-muted", children: [
2511
+ "You have access through",
2512
+ " ",
2513
+ teams.find((candidate) => candidate.id === team.via)?.name ?? "the organization that manages this team",
2514
+ "."
2515
+ ] })
2516
+ ) : null,
2353
2517
  error ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "b316-error", role: "alert", children: error }) : null,
2354
2518
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("ul", { className: "b316-team-list", children: (members ?? []).map((member) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("li", { className: "b316-team-member", children: [
2355
2519
  /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { children: [
@@ -2706,6 +2870,7 @@ function usePlans(options = {}) {
2706
2870
  const [busyPlanVersionId, setBusy] = (0, import_react18.useState)(null);
2707
2871
  const [lastError, setLastError] = (0, import_react18.useState)(null);
2708
2872
  const [ownPlanVersionId, setOwnPlanVersionId] = (0, import_react18.useState)(null);
2873
+ const [managedBy, setManagedBy] = (0, import_react18.useState)(null);
2709
2874
  const [reloadCount, setReloadCount] = (0, import_react18.useState)(0);
2710
2875
  const supplied = options.currentPlanVersionId;
2711
2876
  const discover = supplied === void 0;
@@ -2726,13 +2891,16 @@ function usePlans(options = {}) {
2726
2891
  };
2727
2892
  }, [client, reloadCount]);
2728
2893
  (0, import_react18.useEffect)(() => {
2729
- if (!discover || !token) {
2894
+ if (!token) {
2730
2895
  return;
2731
2896
  }
2732
2897
  let cancelled = false;
2733
2898
  client.billing.subscription(token).then((result) => {
2734
2899
  if (!cancelled) {
2735
- setOwnPlanVersionId(result.subscription?.planVersion.id ?? null);
2900
+ setManagedBy(result.managedBy ?? null);
2901
+ if (discover) {
2902
+ setOwnPlanVersionId(result.subscription?.planVersion.id ?? null);
2903
+ }
2736
2904
  }
2737
2905
  }).catch(() => null);
2738
2906
  return () => {
@@ -2746,6 +2914,11 @@ function usePlans(options = {}) {
2746
2914
  const selectPlan = (0, import_react18.useCallback)(
2747
2915
  async (plan) => {
2748
2916
  setLastError(null);
2917
+ if (managedBy) {
2918
+ const reason = `Your plan is provided by ${managedBy.name ?? "the organization that manages your account"}.`;
2919
+ setLastError(reason);
2920
+ return { status: "failed", reason };
2921
+ }
2749
2922
  if (!token) {
2750
2923
  return { status: "needs-sign-in" };
2751
2924
  }
@@ -2782,7 +2955,7 @@ function usePlans(options = {}) {
2782
2955
  setBusy(null);
2783
2956
  }
2784
2957
  },
2785
- [client, token, options.navigate]
2958
+ [client, token, options.navigate, managedBy]
2786
2959
  );
2787
2960
  return {
2788
2961
  plans,
@@ -2792,6 +2965,7 @@ function usePlans(options = {}) {
2792
2965
  busyPlanVersionId,
2793
2966
  lastError,
2794
2967
  signedIn: token !== null,
2968
+ managedBy,
2795
2969
  selectPlan,
2796
2970
  reload
2797
2971
  };
@@ -2831,6 +3005,7 @@ function PricingTable({
2831
3005
  busyPlanVersionId: busy,
2832
3006
  lastError,
2833
3007
  signedIn,
3008
+ managedBy,
2834
3009
  selectPlan
2835
3010
  } = usePlans({ currentPlanVersionId });
2836
3011
  async function choose(plan) {
@@ -2856,7 +3031,16 @@ function PricingTable({
2856
3031
  const anyDescription = plans.some((plan) => Boolean(plan.description));
2857
3032
  const anyFeatured = plans.some((plan) => plan.key === featuredPlanKey);
2858
3033
  return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: cls("b316-plans", className), style, children: [
2859
- lastError ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "b316-plans-error", role: "alert", children: lastError }) : null,
3034
+ managedBy ? (
3035
+ // A client company's plan comes from the firm that runs it. The table
3036
+ // still shows what is on offer, with the current plan marked, but
3037
+ // nothing here can be bought: it would grant this account nothing.
3038
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "b316-plans-managed b316-muted", children: [
3039
+ "Your plan is provided by ",
3040
+ managedBy.name ?? "the organization that manages your account",
3041
+ ". Ask them to change it."
3042
+ ] })
3043
+ ) : lastError ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "b316-plans-error", role: "alert", children: lastError }) : null,
2860
3044
  plans.map((plan) => {
2861
3045
  const base = plan.prices.find((price) => price.featureId === null) ?? plan.prices[0];
2862
3046
  const isCurrent = current === plan.planVersionId;
@@ -2885,7 +3069,7 @@ function PricingTable({
2885
3069
  {
2886
3070
  type: "button",
2887
3071
  className: isCurrent ? "b316-button b316-button-current" : "b316-button",
2888
- disabled: isCurrent || busy !== null || !signedIn && !signInUrl && !onSelectPlan,
3072
+ disabled: isCurrent || managedBy !== null || busy !== null || !signedIn && !signInUrl && !onSelectPlan,
2889
3073
  onClick: () => {
2890
3074
  void choose(plan);
2891
3075
  },
@@ -4217,6 +4401,7 @@ function SubscriptionManager({
4217
4401
  const { client, token, loading: sessionLoading } = useBase316();
4218
4402
  const [subscription, setSubscription] = (0, import_react26.useState)(null);
4219
4403
  const [loading, setLoading] = (0, import_react26.useState)(true);
4404
+ const [managedBy, setManagedBy] = (0, import_react26.useState)(null);
4220
4405
  const [busy, setBusy] = (0, import_react26.useState)(false);
4221
4406
  const [error, setError] = (0, import_react26.useState)(null);
4222
4407
  const load = (0, import_react26.useCallback)(async () => {
@@ -4228,6 +4413,7 @@ function SubscriptionManager({
4228
4413
  try {
4229
4414
  const result = await client.billing.subscription(token);
4230
4415
  setSubscription(result.subscription);
4416
+ setManagedBy(result.managedBy ?? null);
4231
4417
  } catch (cause) {
4232
4418
  console.error("[base316] could not load the subscription", cause);
4233
4419
  setError("Could not load your subscription.");
@@ -4259,7 +4445,11 @@ function SubscriptionManager({
4259
4445
  " \xB7 ",
4260
4446
  subscription.cancelAtPeriodEnd ? `Ends ${renews}` : `Renews ${renews}`
4261
4447
  ] }),
4262
- subscription.cancelAtPeriodEnd ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { className: "b316-muted", children: [
4448
+ managedBy ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { className: "b316-muted", children: [
4449
+ "Provided by ",
4450
+ managedBy.name ?? "the organization that manages your account",
4451
+ ". Ask them to change or cancel it."
4452
+ ] }) : subscription.cancelAtPeriodEnd ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("p", { className: "b316-muted", children: [
4263
4453
  "This subscription will not renew. You keep access until ",
4264
4454
  renews,
4265
4455
  "."
@@ -4596,6 +4786,11 @@ var MANIFEST = [
4596
4786
  summary: "Colours, radius and font as tokens. Restyles everything at once.",
4597
4787
  required: false
4598
4788
  },
4789
+ {
4790
+ name: "tenant",
4791
+ summary: "White-label everything inside for one tenant \u2014 `{ organizationId }` or `{ organizationExternalId }`. A firm's name, logo and colours on the screens it runs its clients on; its own branding, then its parent's, then the environment's. Change it when the user switches team.",
4792
+ required: false
4793
+ },
4599
4794
  ...appearance
4600
4795
  ],
4601
4796
  seeAlso: ["useBase316", "useSession"]
@@ -4621,7 +4816,7 @@ var MANIFEST = [
4621
4816
  {
4622
4817
  name: "SignIn",
4623
4818
  kind: "component",
4624
- summary: "Password, passkey and magic-link sign-in in one form.",
4819
+ summary: "Password, passkey, magic-link and text-message sign-in in one form.",
4625
4820
  useWhen: "The application renders its own sign-in rather than redirecting to the hosted page.",
4626
4821
  requires: [],
4627
4822
  props: [
@@ -4636,6 +4831,11 @@ var MANIFEST = [
4636
4831
  summary: "Where an emailed link lands, which must be registered on the environment. Without it, magic link is not offered at all.",
4637
4832
  required: false
4638
4833
  },
4834
+ {
4835
+ name: "phone",
4836
+ summary: "Offer signing in with a code sent by text message. Needs text messages switched on for the environment, and its own Twilio account in production.",
4837
+ required: false
4838
+ },
4639
4839
  ...appearance
4640
4840
  ],
4641
4841
  seeAlso: ["SignUp", "ForgotPassword"]
@@ -4769,8 +4969,8 @@ var MANIFEST = [
4769
4969
  {
4770
4970
  name: "TwoFactorManager",
4771
4971
  kind: "component",
4772
- summary: "Turn two-factor authentication on or off, by authenticator app or a code to the inbox.",
4773
- useWhen: "A security settings page. Offers both factors; the app is stronger, the emailed code is the one people who will not install an app actually use.",
4972
+ summary: "Turn two-factor authentication on or off, by authenticator app, a code to the inbox or a code by text message.",
4973
+ useWhen: "A security settings page. The app is strongest; the emailed or texted code is the one people who will not install an app actually use.",
4774
4974
  requires: ["session"],
4775
4975
  props: [
4776
4976
  {
@@ -4778,6 +4978,11 @@ var MANIFEST = [
4778
4978
  summary: "Whether a factor already exists, which decides which side is shown.",
4779
4979
  required: false
4780
4980
  },
4981
+ {
4982
+ name: "allowSms",
4983
+ summary: "Offer a code by text message. Off by default: it only works once the environment can send one.",
4984
+ required: false
4985
+ },
4781
4986
  { name: "onEnabled", summary: "Called once a factor is confirmed.", required: false },
4782
4987
  { name: "onDisabled", summary: "Called once it is removed.", required: false },
4783
4988
  ...appearance
@@ -4798,7 +5003,7 @@ var MANIFEST = [
4798
5003
  {
4799
5004
  name: "Branding",
4800
5005
  kind: "component",
4801
- summary: "The environment's logo, name and tagline.",
5006
+ summary: "The logo, name and tagline \u2014 the environment's, or the tenant's when the provider is given a `tenant`.",
4802
5007
  useWhen: "Above a sign-in form, so your pages match the hosted ones.",
4803
5008
  requires: [],
4804
5009
  props: [
@@ -4810,7 +5015,7 @@ var MANIFEST = [
4810
5015
  {
4811
5016
  name: "TeamMembers",
4812
5017
  kind: "component",
4813
- summary: "Who is in a team, who has been invited, and the way in and out.",
5018
+ summary: "Who is in a team, who has been invited, and the way in and out. A team reached only through the organization that manages it says so, and offers nothing to leave.",
4814
5019
  useWhen: "A Team or Members settings page. One component covers the list, the pending invitations, the invite box and leaving.",
4815
5020
  requires: ["session"],
4816
5021
  props: [