@the-portland-company/shell 0.75.2 → 0.76.1

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.
@@ -538,6 +538,9 @@ function useChromeMe() {
538
538
  const ctx = react.useContext(NavChromeContext);
539
539
  return { me: ctx?.me ?? null, loaded: ctx?.meLoaded ?? false };
540
540
  }
541
+ function useNavChromeDedupedFetch() {
542
+ return react.useContext(NavChromeContext)?.dedupedFetch ?? null;
543
+ }
541
544
  function useNavChromeFetcher() {
542
545
  return react.useContext(NavChromeContext)?.fetchJson ?? null;
543
546
  }
@@ -550,12 +553,16 @@ function useNavChrome(component) {
550
553
  }
551
554
  return ctx;
552
555
  }
553
- function useChromeOrgs() {
556
+ function useChromeOrgs({ enabled = true } = {}) {
554
557
  const { dedupedFetch } = useNavChrome("useChromeOrgs");
555
558
  const [orgs, setOrgs] = react.useState(null);
556
- const [loading, setLoading] = react.useState(true);
559
+ const [loading, setLoading] = react.useState(enabled);
557
560
  const [error, setError] = react.useState(false);
558
561
  react.useEffect(() => {
562
+ if (!enabled) {
563
+ setLoading(false);
564
+ return;
565
+ }
559
566
  let cancelled = false;
560
567
  setLoading(true);
561
568
  setError(false);
@@ -571,7 +578,7 @@ function useChromeOrgs() {
571
578
  return () => {
572
579
  cancelled = true;
573
580
  };
574
- }, [dedupedFetch]);
581
+ }, [dedupedFetch, enabled]);
575
582
  return { orgs, loading, error };
576
583
  }
577
584
  function ValueSkeleton({ className }) {
@@ -861,6 +868,10 @@ function FuelAction({ orgId }) {
861
868
  const [error, setError] = react.useState(false);
862
869
  react.useEffect(() => {
863
870
  let cancelled = false;
871
+ if (!orgId) {
872
+ setLoading(false);
873
+ return;
874
+ }
864
875
  setLoading(true);
865
876
  setError(false);
866
877
  setFuel(null);
@@ -1096,6 +1107,10 @@ function NotificationsAction({ orgId }) {
1096
1107
  }, [orgId]);
1097
1108
  react.useEffect(() => {
1098
1109
  let cancelled = false;
1110
+ if (!orgId) {
1111
+ setLoading(false);
1112
+ return;
1113
+ }
1099
1114
  setLoading(true);
1100
1115
  setError(false);
1101
1116
  setPayload(null);
@@ -1335,10 +1350,14 @@ function composeSupportMessage(input) {
1335
1350
  ---
1336
1351
  ${ctx.join("\n")}`;
1337
1352
  }
1338
- function SupportAction({ orgId } = {}) {
1353
+ function SupportAction({
1354
+ orgId,
1355
+ orgs: knownOrgs
1356
+ } = {}) {
1339
1357
  const { fetchJson } = useNavChrome("SupportAction");
1340
1358
  const { me, loaded } = useChromeMe();
1341
- const { orgs } = useChromeOrgs();
1359
+ const { orgs: fetchedOrgs } = useChromeOrgs({ enabled: !knownOrgs });
1360
+ const orgs = knownOrgs ?? fetchedOrgs;
1342
1361
  const [open, setOpen] = react.useState(false);
1343
1362
  const [message, setMessage] = react.useState("");
1344
1363
  const [email, setEmail] = react.useState("");
@@ -3855,6 +3874,85 @@ function resolveActiveOrg(search) {
3855
3874
  if (fromUrl) return fromUrl;
3856
3875
  return readActiveOrgCookie();
3857
3876
  }
3877
+ var ORG_ID_QUERY_PARAM = "org_id";
3878
+ var ORG_ID_TAIL_LENGTH = 7;
3879
+ var LEGACY_ORG_TAIL_LENGTH = 6;
3880
+ var TAIL_RE = /^[0-9a-f]{6,7}$/i;
3881
+ var AUTH_BASE_URL = "https://auth.politogyvrm.com";
3882
+ var AUTH_BASE_URL_STAGING = "https://auth-staging.politogyvrm.com";
3883
+ function orgIdTail(orgId) {
3884
+ return isOrgId(orgId) ? orgId.trim().toLowerCase().slice(-ORG_ID_TAIL_LENGTH) : null;
3885
+ }
3886
+ function isOrgSelector(v) {
3887
+ return typeof v === "string" && (UUID_RE.test(v.trim()) || TAIL_RE.test(v.trim()));
3888
+ }
3889
+ function matchOrgSelector(selector, orgs) {
3890
+ if (!isOrgSelector(selector)) return null;
3891
+ const sel = selector.trim().toLowerCase();
3892
+ if (UUID_RE.test(sel)) return orgs.find((o) => o.id.toLowerCase() === sel) ?? null;
3893
+ if (sel.length !== ORG_ID_TAIL_LENGTH && sel.length !== LEGACY_ORG_TAIL_LENGTH) return null;
3894
+ const hits = orgs.filter((o) => o.id.toLowerCase().endsWith(sel));
3895
+ return hits.length === 1 ? hits[0] : null;
3896
+ }
3897
+ function readOrgSelectorFromUrl(search) {
3898
+ if (!search) return null;
3899
+ try {
3900
+ const qs = search.includes("?") ? search.slice(search.indexOf("?") + 1) : search;
3901
+ const params = new URLSearchParams(qs);
3902
+ for (const key of [ORG_ID_QUERY_PARAM, ACTIVE_ORG_QUERY_PARAM]) {
3903
+ const v = params.get(key);
3904
+ if (isOrgSelector(v)) return v.trim().toLowerCase();
3905
+ }
3906
+ return null;
3907
+ } catch {
3908
+ return null;
3909
+ }
3910
+ }
3911
+ function orgClaimsFrom(claims) {
3912
+ if (!claims || typeof claims !== "object") return null;
3913
+ const c = claims;
3914
+ if (!isOrgId(c.org_id)) return null;
3915
+ return {
3916
+ orgId: c.org_id.trim().toLowerCase(),
3917
+ orgRole: typeof c.org_role === "string" ? c.org_role : null
3918
+ };
3919
+ }
3920
+ function authBaseUrl() {
3921
+ if (typeof window !== "undefined" && window.location.hostname.toLowerCase().includes("staging")) {
3922
+ return AUTH_BASE_URL_STAGING;
3923
+ }
3924
+ return AUTH_BASE_URL;
3925
+ }
3926
+ var ActiveOrgSwitchError = class extends Error {
3927
+ constructor(status, message) {
3928
+ super(message);
3929
+ this.status = status;
3930
+ this.name = "ActiveOrgSwitchError";
3931
+ }
3932
+ };
3933
+ async function switchActiveOrg(orgId, opts = {}) {
3934
+ if (!isOrgId(orgId)) throw new ActiveOrgSwitchError(400, "org_id must be a full uuid");
3935
+ const doFetch = opts.fetch ?? fetch;
3936
+ const res = await doFetch(`${opts.authBaseUrl ?? authBaseUrl()}/api/active-org`, {
3937
+ method: "POST",
3938
+ credentials: "include",
3939
+ headers: { "Content-Type": "application/json" },
3940
+ body: JSON.stringify({ org_id: orgId })
3941
+ });
3942
+ if (!res.ok) {
3943
+ let msg = `active-org switch failed (${res.status})`;
3944
+ try {
3945
+ const body = await res.json();
3946
+ if (body?.error) msg = body.error;
3947
+ } catch {
3948
+ }
3949
+ throw new ActiveOrgSwitchError(res.status, msg);
3950
+ }
3951
+ const claims = orgClaimsFrom(await res.json().catch(() => null));
3952
+ if (!claims) throw new ActiveOrgSwitchError(502, "active-org switch returned no org_id");
3953
+ writeActiveOrgCookie(claims.orgId);
3954
+ return claims;
3955
+ }
3858
3956
 
3859
3957
  // src/lib/breadcrumbs.ts
3860
3958
  function deriveBreadcrumbs(items, pathname, opts) {
@@ -4533,6 +4631,7 @@ function PolitogyAppFrame({
4533
4631
  fetchJson: data?.fetchJson,
4534
4632
  devNotes: data?.devNotes,
4535
4633
  org: data?.org,
4634
+ tokenOrg: data?.tokenOrg,
4536
4635
  identity: {
4537
4636
  userId: session.userId,
4538
4637
  userName: session.userName ?? null,
@@ -4813,6 +4912,107 @@ var ChromeStateContext = react.createContext(null);
4813
4912
  function useChromeState() {
4814
4913
  return react.useContext(ChromeStateContext);
4815
4914
  }
4915
+ function TokenChromeStateProvider({
4916
+ tokenOrg,
4917
+ children
4918
+ }) {
4919
+ const { orgs: fetchedOrgs, loading: orgsFetching, error: orgsError } = useChromeOrgs({
4920
+ enabled: !tokenOrg.orgs
4921
+ });
4922
+ const [navDisplay, setNavDisplayState] = react.useState(NAV_DISPLAY_DEFAULT);
4923
+ const [pendingOrg, setPendingOrg] = react.useState(null);
4924
+ const tokenOrgRef = react.useRef(tokenOrg);
4925
+ tokenOrgRef.current = tokenOrg;
4926
+ react.useEffect(() => {
4927
+ setNavDisplayState(resolveNavDisplay());
4928
+ }, []);
4929
+ const orgs = react.useMemo(
4930
+ () => tokenOrg.orgs ?? (fetchedOrgs ?? []).map((o) => ({
4931
+ id: o.id,
4932
+ name: o.name,
4933
+ logoUrl: o.logoUrl,
4934
+ brandColor: o.brandColor,
4935
+ role: o.role,
4936
+ rawRole: o.rawRole ?? null
4937
+ })),
4938
+ [tokenOrg.orgs, fetchedOrgs]
4939
+ );
4940
+ const [urlSelector] = react.useState(
4941
+ () => typeof window === "undefined" ? null : readOrgSelectorFromUrl(window.location.search)
4942
+ );
4943
+ const overrideOrg = react.useMemo(
4944
+ () => urlSelector ? matchOrgSelector(urlSelector, orgs)?.id ?? null : null,
4945
+ [urlSelector, orgs]
4946
+ );
4947
+ const orgId = pendingOrg ?? (tokenOrg.orgId && overrideOrg ? overrideOrg : tokenOrg.orgId);
4948
+ const reexchangedFor = react.useRef(null);
4949
+ react.useEffect(() => {
4950
+ if (!overrideOrg || !tokenOrg.orgId || overrideOrg === tokenOrg.orgId) return;
4951
+ if (reexchangedFor.current === overrideOrg) return;
4952
+ reexchangedFor.current = overrideOrg;
4953
+ void Promise.resolve(tokenOrgRef.current.onReexchange(overrideOrg, { perTab: true })).catch(
4954
+ () => {
4955
+ }
4956
+ );
4957
+ }, [overrideOrg, tokenOrg.orgId]);
4958
+ react.useEffect(() => {
4959
+ if (pendingOrg && tokenOrg.orgId === pendingOrg) setPendingOrg(null);
4960
+ }, [pendingOrg, tokenOrg.orgId]);
4961
+ const setOrgId = react.useCallback((id) => {
4962
+ const current = tokenOrgRef.current;
4963
+ if (!isOrgId(id) || id === (current.orgId ?? null)) return;
4964
+ setPendingOrg(id);
4965
+ void (async () => {
4966
+ try {
4967
+ const claims = await switchActiveOrg(id, { authBaseUrl: current.authBaseUrl });
4968
+ try {
4969
+ const url = new URL(window.location.href);
4970
+ url.searchParams.delete(ACTIVE_ORG_QUERY_PARAM);
4971
+ url.searchParams.set(ORG_ID_QUERY_PARAM, orgIdTail(claims.orgId));
4972
+ window.history.replaceState(window.history.state, "", url.toString());
4973
+ } catch {
4974
+ }
4975
+ await tokenOrgRef.current.onReexchange(claims.orgId, { perTab: false });
4976
+ } catch (err) {
4977
+ setPendingOrg(null);
4978
+ tokenOrgRef.current.onSwitchError?.(err);
4979
+ }
4980
+ })();
4981
+ }, []);
4982
+ const setNavDisplay = react.useCallback((m) => {
4983
+ setNavDisplayState(m);
4984
+ writeNavDisplayLocal(m);
4985
+ writeNavDisplayCookie(m);
4986
+ }, []);
4987
+ react.useEffect(() => {
4988
+ if (!isOrgId(orgId)) return;
4989
+ try {
4990
+ const url = new URL(window.location.href);
4991
+ const tail = orgIdTail(orgId);
4992
+ if (url.searchParams.has(ACTIVE_ORG_QUERY_PARAM) || url.searchParams.get(ORG_ID_QUERY_PARAM) !== tail) {
4993
+ url.searchParams.delete(ACTIVE_ORG_QUERY_PARAM);
4994
+ url.searchParams.set(ORG_ID_QUERY_PARAM, tail);
4995
+ window.history.replaceState(window.history.state, "", url.toString());
4996
+ }
4997
+ } catch {
4998
+ }
4999
+ }, [orgId]);
5000
+ react.useEffect(() => {
5001
+ if (isOrgId(tokenOrg.orgId) && !pendingOrg) writeActiveOrgCookie(tokenOrg.orgId);
5002
+ }, [tokenOrg.orgId, pendingOrg]);
5003
+ const value = react.useMemo(
5004
+ () => ({
5005
+ orgId,
5006
+ setOrgId,
5007
+ orgs,
5008
+ orgsLoading: !tokenOrg.orgs && orgsFetching && !orgsError && !fetchedOrgs,
5009
+ navDisplay,
5010
+ setNavDisplay
5011
+ }),
5012
+ [orgId, setOrgId, orgs, tokenOrg.orgs, orgsFetching, orgsError, fetchedOrgs, navDisplay, setNavDisplay]
5013
+ );
5014
+ return /* @__PURE__ */ jsxRuntime.jsx(ChromeStateContext.Provider, { value, children });
5015
+ }
4816
5016
  function ChromeStateProvider({
4817
5017
  controlledOrg,
4818
5018
  children
@@ -5116,6 +5316,7 @@ function PolitogyAppChrome({
5116
5316
  fetchJson: fetchJsonOverride,
5117
5317
  devNotes,
5118
5318
  org,
5319
+ tokenOrg,
5119
5320
  identity,
5120
5321
  children
5121
5322
  }) {
@@ -5134,7 +5335,7 @@ function PolitogyAppChrome({
5134
5335
  [proxyBasePath]
5135
5336
  );
5136
5337
  const fetchJson = fetchJsonOverride ?? proxyFetchJson;
5137
- return /* @__PURE__ */ jsxRuntime.jsx(NavChromeProvider, { fetchJson, children: /* @__PURE__ */ jsxRuntime.jsx(ChromeStateProvider, { controlledOrg: org, children: /* @__PURE__ */ jsxRuntime.jsx(
5338
+ const devNotesTree = /* @__PURE__ */ jsxRuntime.jsx(
5138
5339
  AppDevNotes,
5139
5340
  {
5140
5341
  storagePrefix,
@@ -5143,11 +5344,13 @@ function PolitogyAppChrome({
5143
5344
  identity,
5144
5345
  children
5145
5346
  }
5146
- ) }) });
5347
+ );
5348
+ return /* @__PURE__ */ jsxRuntime.jsx(NavChromeProvider, { fetchJson, children: tokenOrg ? /* @__PURE__ */ jsxRuntime.jsx(TokenChromeStateProvider, { tokenOrg, children: devNotesTree }) : /* @__PURE__ */ jsxRuntime.jsx(ChromeStateProvider, { controlledOrg: org, children: devNotesTree }) });
5147
5349
  }
5148
5350
  function ChromeTopBarActions() {
5149
5351
  const state = useChromeState();
5150
5352
  if (!state) return null;
5353
+ const settledOrgId = state.orgsLoading ? null : state.orgId;
5151
5354
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5152
5355
  /* @__PURE__ */ jsxRuntime.jsx(
5153
5356
  TopBarWidgetSlot,
@@ -5157,10 +5360,10 @@ function ChromeTopBarActions() {
5157
5360
  children: /* @__PURE__ */ jsxRuntime.jsx(ChromeDevNotesMenu, {})
5158
5361
  }
5159
5362
  ),
5160
- /* @__PURE__ */ jsxRuntime.jsx(FuelAction, { orgId: state.orgId }),
5363
+ /* @__PURE__ */ jsxRuntime.jsx(FuelAction, { orgId: settledOrgId }),
5161
5364
  /* @__PURE__ */ jsxRuntime.jsx(SpecOverlayAction, { orgId: state.orgId, orgName: state.orgs.find((o) => o.id === state.orgId)?.name ?? null }),
5162
- /* @__PURE__ */ jsxRuntime.jsx(NotificationsAction, { orgId: state.orgId }),
5163
- /* @__PURE__ */ jsxRuntime.jsx(SupportAction, { orgId: state.orgId })
5365
+ /* @__PURE__ */ jsxRuntime.jsx(NotificationsAction, { orgId: settledOrgId }),
5366
+ /* @__PURE__ */ jsxRuntime.jsx(SupportAction, { orgId: state.orgId, orgs: state.orgs.length ? state.orgs : void 0 })
5164
5367
  ] });
5165
5368
  }
5166
5369
  function ChromeDevNotesMenu() {
@@ -5246,7 +5449,8 @@ var MODE_DEFS = [
5246
5449
  function useChromeMode(arg = {}) {
5247
5450
  const { fallbackModeId = "relationship", pinnedModeId } = typeof arg === "string" ? { fallbackModeId: arg, pinnedModeId: void 0 } : arg;
5248
5451
  const ctx = react.useContext(ChromeStateContext);
5249
- const nav = useNavChromeFetcher();
5452
+ const dedupedFetch = useNavChromeDedupedFetch();
5453
+ const { me, loaded: meLoaded } = useChromeMe();
5250
5454
  const orgId = ctx?.orgId ?? null;
5251
5455
  const [entitled, setEntitled] = react.useState(null);
5252
5456
  const [selectedModeId, setSelectedModeId] = react.useState(null);
@@ -5258,9 +5462,14 @@ function useChromeMode(arg = {}) {
5258
5462
  }
5259
5463
  }, []);
5260
5464
  react.useEffect(() => {
5261
- if (!nav || !orgId) return;
5465
+ if (!dedupedFetch || !orgId || !meLoaded) return;
5466
+ const bootMe = me;
5467
+ if (bootMe && bootMe.account === orgId && bootMe.modes !== void 0) {
5468
+ setEntitled(bootMe.modes ?? null);
5469
+ return;
5470
+ }
5262
5471
  let cancelled = false;
5263
- nav("me", { headers: { "x-contacts-org": orgId } }).then((data) => {
5472
+ dedupedFetch("me", { headers: { "x-contacts-org": orgId } }, { orgId }).then((data) => {
5264
5473
  if (cancelled) return;
5265
5474
  const modes = data?.modes ?? null;
5266
5475
  setEntitled(modes);
@@ -5269,7 +5478,7 @@ function useChromeMode(arg = {}) {
5269
5478
  return () => {
5270
5479
  cancelled = true;
5271
5480
  };
5272
- }, [nav, orgId]);
5481
+ }, [dedupedFetch, me, meLoaded, orgId]);
5273
5482
  const options = MODE_DEFS.filter((m) => entitled?.[m.id]);
5274
5483
  const pinned = pinnedModeId ? MODE_DEFS.find((m) => m.id === pinnedModeId) ?? null : null;
5275
5484
  const selected = selectedModeId ? options.find((m) => m.id === selectedModeId) ?? null : null;