@patientos/website-kit 0.2.0 → 0.2.2

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.
@@ -12,6 +12,7 @@ interface FunnelPublicApi {
12
12
  turnstileSiteKey?: string;
13
13
  portalUrl?: string;
14
14
  portalOrigin?: string;
15
+ portalApiOrigin?: string;
15
16
  }
16
17
  /** JSON-safe island configuration. Crosses the build→runtime boundary — see `./islands`. */
17
18
  type CertificateFunnelProps = {
@@ -3,101 +3,176 @@ import * as React4 from "react";
3
3
 
4
4
  // src/portal-shared.client.tsx
5
5
  import * as React from "react";
6
- import { jsx, jsxs } from "react/jsx-runtime";
7
- var apiOrigin = "";
8
- var apiOriginLocked = false;
6
+
7
+ // src/portal-client.ts
8
+ var PORTAL_WHOAMI_PATH = "/portal/api/whoami";
9
+ var PORTAL_MAGIC_LINK_PATH = "/portal/api/magic-link";
10
+ var PORTAL_PREFILL_PATH = "/portal/api/prefill";
11
+ var PORTAL_PUBLIC_CLAIM_PATH = "/portal/api/public-claim";
12
+ var PORTAL_RETURN_PATH = "/portal/return";
13
+ var BRIDGE_TIMEOUT_MS = 15e3;
14
+ var BRIDGE_PROBE_TIMEOUT_MS = 8e3;
15
+ var PORTAL_REQUEST_INIT = {
16
+ credentials: "include",
17
+ cache: "no-store"
18
+ };
9
19
  function normalizePortalApiOrigin(raw) {
10
- let u;
20
+ let url;
11
21
  try {
12
- u = new URL(raw.trim());
22
+ url = new URL(raw.trim());
13
23
  } catch {
14
24
  return null;
15
25
  }
16
- const loopback = u.hostname === "localhost" || u.hostname === "127.0.0.1";
17
- if (u.protocol !== "https:" && !(u.protocol === "http:" && loopback)) return null;
18
- if (u.username || u.password || u.search || u.hash) return null;
19
- if (u.pathname !== "/" && u.pathname !== "") return null;
20
- return u.origin;
21
- }
22
- function configurePortalApiOrigin(raw) {
23
- const wanted = raw?.trim();
24
- if (!wanted) return;
25
- const origin = normalizePortalApiOrigin(wanted);
26
- if (!origin) {
27
- console.error("[portal] ignoring a malformed portal API origin", wanted);
28
- return;
26
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1";
27
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) return null;
28
+ if (url.username || url.password || url.search || url.hash) return null;
29
+ if (url.pathname !== "/" && url.pathname !== "") return null;
30
+ return url.origin;
31
+ }
32
+ function encodePortalReturnTarget(url) {
33
+ const bytes = new TextEncoder().encode(url);
34
+ let binary = "";
35
+ for (const byte of bytes) binary += String.fromCharCode(byte);
36
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
37
+ }
38
+ function sameOriginAvailable(portalOrigin) {
39
+ if (typeof window === "undefined") return false;
40
+ if (portalOrigin == null) {
41
+ return true;
29
42
  }
30
- if (apiOriginLocked) {
31
- if (origin !== apiOrigin) {
32
- console.error("[portal] refusing to re-point the portal API origin", {
33
- configured: apiOrigin,
34
- rejected: origin
35
- });
36
- }
37
- return;
38
- }
39
- apiOrigin = origin;
40
- apiOriginLocked = true;
41
- }
42
- function portalApiUrl(path) {
43
- return apiOrigin && path.startsWith("/") ? apiOrigin + path : path;
43
+ const normalizedOrigin = portalOrigin.trim().replace(/\/+$/, "");
44
+ return normalizedOrigin !== "" && window.location.origin === normalizedOrigin;
44
45
  }
45
- var PORTAL_MAGIC_LINK_URL = "/portal/api/magic-link";
46
- var PORTAL_SIGN_OUT_URL = "/portal/api/sign-out";
47
- var PORTAL_REQUEST_INIT = { credentials: "include", cache: "no-store" };
48
- function portalFetch(fetchImpl) {
49
- return fetchImpl ?? ((input, init) => fetch(input, init));
50
- }
51
- async function readPortalResult(res) {
52
- if (!res.ok) {
46
+ async function readPortalResult(response) {
47
+ if (!response.ok) {
53
48
  let error = null;
54
49
  let body;
55
50
  try {
56
- const parsed = await res.json();
51
+ const parsed = await response.json();
57
52
  if (typeof parsed?.error === "string") error = parsed.error;
58
53
  body = parsed;
59
54
  } catch {
60
55
  }
61
- return { ok: false, status: res.status, error, body };
56
+ return { ok: false, status: response.status, error, body };
62
57
  }
63
58
  try {
64
- return { ok: true, data: await res.json() };
59
+ return { ok: true, data: await response.json() };
65
60
  } catch {
66
61
  return { ok: false, status: null, error: null };
67
62
  }
68
63
  }
69
- async function portalGet(url, fetchImpl) {
70
- try {
71
- return await readPortalResult(
72
- await portalFetch(fetchImpl)(portalApiUrl(url), PORTAL_REQUEST_INIT)
73
- );
74
- } catch {
75
- return { ok: false, status: null, error: null };
64
+ function createPortalClient(config = {}) {
65
+ const rawOrigin = config.apiOrigin?.trim() ?? "";
66
+ const normalizedOrigin = rawOrigin ? normalizePortalApiOrigin(rawOrigin) : "";
67
+ if (rawOrigin && !normalizedOrigin) {
68
+ console.error("[portal] ignoring a malformed portal API origin", rawOrigin);
69
+ }
70
+ const apiOrigin = normalizedOrigin ?? "";
71
+ const portalHref = config.portalUrl?.trim() ?? "";
72
+ const available = apiOrigin !== "" || sameOriginAvailable(config.portalOrigin);
73
+ const fetchImpl = config.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
74
+ const url = (path) => apiOrigin && path.startsWith("/") ? apiOrigin + path : path;
75
+ async function request(path, init, timeoutMs = BRIDGE_TIMEOUT_MS) {
76
+ const controller = new AbortController();
77
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
78
+ try {
79
+ const response = await fetchImpl(url(path), {
80
+ ...PORTAL_REQUEST_INIT,
81
+ ...init,
82
+ signal: controller.signal
83
+ });
84
+ return await readPortalResult(response);
85
+ } catch {
86
+ return { ok: false, status: null, error: null };
87
+ } finally {
88
+ clearTimeout(timer);
89
+ }
76
90
  }
91
+ const get = (path) => request(path, { headers: { accept: "application/json" } });
92
+ const send = (path, method, body) => request(path, {
93
+ method,
94
+ headers: { accept: "application/json", "content-type": "application/json" },
95
+ body: JSON.stringify(body ?? {})
96
+ });
97
+ const del = (path) => request(path, { method: "DELETE", headers: { accept: "application/json" } });
98
+ const returnPath = () => {
99
+ if (typeof window === "undefined") return "/";
100
+ const path = `${window.location.pathname || "/"}${window.location.search}${window.location.hash}`;
101
+ if (!apiOrigin) return path;
102
+ return `${PORTAL_RETURN_PATH}?to=${encodePortalReturnTarget(window.location.origin + path)}`;
103
+ };
104
+ return {
105
+ available,
106
+ apiOrigin,
107
+ portalHref,
108
+ url,
109
+ fetch: fetchImpl,
110
+ async whoAmI() {
111
+ if (!available) return { signedIn: false };
112
+ const result = await request(
113
+ PORTAL_WHOAMI_PATH,
114
+ { headers: { accept: "application/json" } },
115
+ BRIDGE_PROBE_TIMEOUT_MS
116
+ );
117
+ return result.ok && result.data?.signedIn === true ? result.data : { signedIn: false };
118
+ },
119
+ async requestMagicLink(email, redirect) {
120
+ if (!available) return false;
121
+ const result = await send(PORTAL_MAGIC_LINK_PATH, "POST", {
122
+ email,
123
+ redirect: redirect ?? returnPath()
124
+ });
125
+ return result.ok && result.data?.ok === true;
126
+ },
127
+ async prefill() {
128
+ if (!available) return null;
129
+ const result = await get(PORTAL_PREFILL_PATH);
130
+ return result.ok ? result.data : null;
131
+ },
132
+ async mintPublicClaim() {
133
+ if (!available) return null;
134
+ const result = await send(PORTAL_PUBLIC_CLAIM_PATH, "POST", {});
135
+ return result.ok ? result.data : null;
136
+ },
137
+ returnPath,
138
+ get,
139
+ send,
140
+ delete: del
141
+ };
142
+ }
143
+
144
+ // src/portal-shared.client.tsx
145
+ import { jsx, jsxs } from "react/jsx-runtime";
146
+ var defaultPortalClient = createPortalClient();
147
+ var PORTAL_SIGN_OUT_URL = "/portal/api/sign-out";
148
+ function portalFetch(fetchImpl) {
149
+ return fetchImpl ?? ((input, init) => fetch(input, init));
150
+ }
151
+ var BOUND_PORTAL_CLIENT = /* @__PURE__ */ Symbol("boundPortalClient");
152
+ function portalFetchForClient(client2, fetchImpl) {
153
+ const fetcher = portalFetch(fetchImpl);
154
+ const bound = (input, init) => fetcher(client2.url(input), init);
155
+ bound[BOUND_PORTAL_CLIENT] = client2;
156
+ return bound;
157
+ }
158
+ function defaultClientWith(fetchImpl) {
159
+ const boundClient = fetchImpl?.[BOUND_PORTAL_CLIENT];
160
+ if (boundClient) return boundClient;
161
+ if (!fetchImpl) return defaultPortalClient;
162
+ return createPortalClient({
163
+ apiOrigin: defaultPortalClient.apiOrigin,
164
+ portalUrl: defaultPortalClient.portalHref,
165
+ fetchImpl
166
+ });
167
+ }
168
+ async function portalGet(url, fetchImpl) {
169
+ return defaultClientWith(fetchImpl).get(url);
77
170
  }
78
171
  async function portalSend(url, method, body, fetchImpl) {
79
- try {
80
- const res = await portalFetch(fetchImpl)(portalApiUrl(url), {
81
- ...PORTAL_REQUEST_INIT,
82
- method,
83
- headers: { "content-type": "application/json" },
84
- body: JSON.stringify(body ?? {})
85
- });
86
- return await readPortalResult(res);
87
- } catch {
88
- return { ok: false, status: null, error: null };
89
- }
172
+ return defaultClientWith(fetchImpl).send(url, method, body);
90
173
  }
91
174
  async function portalDelete(url, fetchImpl) {
92
- try {
93
- const res = await portalFetch(fetchImpl)(portalApiUrl(url), {
94
- ...PORTAL_REQUEST_INIT,
95
- method: "DELETE"
96
- });
97
- return await readPortalResult(res);
98
- } catch {
99
- return { ok: false, status: null, error: null };
100
- }
175
+ return defaultClientWith(fetchImpl).delete(url);
101
176
  }
102
177
  function hasPortalIdentity(body) {
103
178
  return typeof body?.signedIn === "boolean";
@@ -119,26 +194,10 @@ function writePortalSignedInHint(signedIn) {
119
194
  } catch {
120
195
  }
121
196
  }
122
- async function requestPortalSignInLink(email, redirect, fetchImpl) {
123
- const res = await portalSend(
124
- PORTAL_MAGIC_LINK_URL,
125
- "POST",
126
- { email, redirect },
127
- fetchImpl
128
- );
129
- return { ok: res.ok };
130
- }
131
197
  async function signOutOfPortal(fetchImpl) {
132
198
  const res = await portalSend(PORTAL_SIGN_OUT_URL, "POST", {}, fetchImpl);
133
199
  return { ok: res.ok };
134
200
  }
135
- var PORTAL_RETURN_PATH = "/portal/return";
136
- function currentPagePath() {
137
- if (typeof window === "undefined") return "/";
138
- const path = `${window.location.pathname || "/"}${window.location.search}`;
139
- if (!apiOrigin) return path;
140
- return `${PORTAL_RETURN_PATH}?to=${encodeURIComponent(window.location.origin + path)}`;
141
- }
142
201
  var LOCALE = "en-AU";
143
202
  function parseInstant(iso) {
144
203
  if (!iso) return null;
@@ -1218,6 +1277,7 @@ export {
1218
1277
  FlowErrorSummary,
1219
1278
  focusField,
1220
1279
  FlowLoading,
1280
+ createPortalClient,
1221
1281
  PatientOSApiError,
1222
1282
  patientos,
1223
1283
  persistClaimToken,
@@ -1244,18 +1304,14 @@ export {
1244
1304
  formatSlotTime,
1245
1305
  zoneAbbr,
1246
1306
  SlotPicker,
1247
- configurePortalApiOrigin,
1248
- portalApiUrl,
1249
- PORTAL_REQUEST_INIT,
1307
+ portalFetchForClient,
1250
1308
  portalGet,
1251
1309
  portalSend,
1252
1310
  portalDelete,
1253
1311
  hasPortalIdentity,
1254
1312
  readPortalSignedInHint,
1255
1313
  writePortalSignedInHint,
1256
- requestPortalSignInLink,
1257
1314
  signOutOfPortal,
1258
- currentPagePath,
1259
1315
  formatPortalDate,
1260
1316
  formatPortalDateTime,
1261
1317
  formatPortalTime,
@@ -6,7 +6,7 @@ import {
6
6
  PortalAccountClient,
7
7
  PortalPanelClient,
8
8
  StoreClient
9
- } from "./chunk-CRCCBM4F.js";
9
+ } from "./chunk-UTCKIWE7.js";
10
10
  import {
11
11
  mountIslandsWith
12
12
  } from "./chunk-THMT43MV.js";