@patientos/website-kit 0.2.4 → 0.2.6

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.
@@ -9,9 +9,135 @@ var PORTAL_WHOAMI_PATH = "/portal/api/whoami";
9
9
  var PORTAL_MAGIC_LINK_PATH = "/portal/api/magic-link";
10
10
  var PORTAL_PREFILL_PATH = "/portal/api/prefill";
11
11
  var PORTAL_PUBLIC_CLAIM_PATH = "/portal/api/public-claim";
12
+ var PORTAL_APPLICATION_HANDOFF_PATH = "/portal/api/application-run-handoffs";
13
+ var PORTAL_SESSION_EXCHANGE_PATH = "/portal/api/session-exchange";
12
14
  var PORTAL_RETURN_PATH = "/portal/return";
15
+ var PORTAL_HANDOFF_FRAGMENT_PREFIX = "#patientos-handoff=";
13
16
  var BRIDGE_TIMEOUT_MS = 15e3;
14
17
  var BRIDGE_PROBE_TIMEOUT_MS = 8e3;
18
+ var PORTAL_BEARER_STORAGE_PREFIX = "patientos.portal.bearer:";
19
+ var parkedHandoffPayload = null;
20
+ var pendingHandoffTokens = /* @__PURE__ */ new Map();
21
+ var pendingHandoffExchanges = /* @__PURE__ */ new Map();
22
+ var memoryBearers = /* @__PURE__ */ new Map();
23
+ function onLocalhostDevPage() {
24
+ if (typeof window === "undefined") return false;
25
+ return window.location.protocol === "http:" && window.location.hostname === "localhost" && window.location.port !== "";
26
+ }
27
+ function localhostHandoffEnabled(apiOrigin) {
28
+ return apiOrigin !== "" && onLocalhostDevPage();
29
+ }
30
+ function bearerStorage() {
31
+ try {
32
+ return typeof window === "undefined" ? null : window.sessionStorage;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ function bearerStorageKey(apiOrigin) {
38
+ return `${PORTAL_BEARER_STORAGE_PREFIX}${apiOrigin}`;
39
+ }
40
+ function readBearer(apiOrigin) {
41
+ const memory = memoryBearers.get(apiOrigin);
42
+ if (memory) return memory;
43
+ try {
44
+ const stored = bearerStorage()?.getItem(bearerStorageKey(apiOrigin)) ?? null;
45
+ if (!stored || stored.length > 4096) {
46
+ clearBearer(apiOrigin);
47
+ return null;
48
+ }
49
+ memoryBearers.set(apiOrigin, stored);
50
+ return stored;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ function writeBearer(apiOrigin, token) {
56
+ memoryBearers.set(apiOrigin, token);
57
+ try {
58
+ bearerStorage()?.setItem(bearerStorageKey(apiOrigin), token);
59
+ } catch {
60
+ }
61
+ }
62
+ function clearBearer(apiOrigin) {
63
+ memoryBearers.delete(apiOrigin);
64
+ try {
65
+ bearerStorage()?.removeItem(bearerStorageKey(apiOrigin));
66
+ } catch {
67
+ }
68
+ }
69
+ function decodeBase64UrlJson(raw) {
70
+ const b64 = raw.replace(/-/g, "+").replace(/_/g, "/");
71
+ const binary = atob(b64.padEnd(Math.ceil(b64.length / 4) * 4, "="));
72
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
73
+ return JSON.parse(new TextDecoder().decode(bytes));
74
+ }
75
+ function consumePortalHandoffFragment() {
76
+ if (!onLocalhostDevPage()) return;
77
+ const raw = window.location.hash;
78
+ if (!raw.startsWith(PORTAL_HANDOFF_FRAGMENT_PREFIX)) return;
79
+ let payload = null;
80
+ try {
81
+ const decoded = decodeBase64UrlJson(raw.slice(PORTAL_HANDOFF_FRAGMENT_PREFIX.length));
82
+ const candidate = decoded;
83
+ if (typeof candidate?.token === "string" && candidate.token.length > 0 && candidate.token.length <= 512 && typeof candidate.returnHash === "string") {
84
+ payload = { token: candidate.token, returnHash: candidate.returnHash };
85
+ }
86
+ } catch {
87
+ }
88
+ const restoredHash = payload?.returnHash ? `#${payload.returnHash}` : "";
89
+ window.history.replaceState(
90
+ window.history.state,
91
+ "",
92
+ `${window.location.pathname}${window.location.search}${restoredHash}`
93
+ );
94
+ if (payload) parkedHandoffPayload = payload;
95
+ }
96
+ function adoptParkedHandoff(apiOrigin) {
97
+ const payload = parkedHandoffPayload;
98
+ if (!payload || !localhostHandoffEnabled(apiOrigin)) return;
99
+ parkedHandoffPayload = null;
100
+ clearBearer(apiOrigin);
101
+ pendingHandoffTokens.set(apiOrigin, payload.token);
102
+ }
103
+ async function exchangePortalHandoff(apiOrigin, fetchImpl) {
104
+ const pending = pendingHandoffExchanges.get(apiOrigin);
105
+ if (pending) return pending;
106
+ const oneTimeToken = pendingHandoffTokens.get(apiOrigin);
107
+ if (!oneTimeToken) return readBearer(apiOrigin);
108
+ pendingHandoffTokens.delete(apiOrigin);
109
+ const exchange = (async () => {
110
+ const controller = new AbortController();
111
+ const timer = setTimeout(() => controller.abort(), BRIDGE_PROBE_TIMEOUT_MS);
112
+ try {
113
+ const response = await fetchImpl(`${apiOrigin}${PORTAL_SESSION_EXCHANGE_PATH}`, {
114
+ ...PORTAL_REQUEST_INIT,
115
+ method: "POST",
116
+ headers: { accept: "application/json", "content-type": "application/json" },
117
+ body: JSON.stringify({ token: oneTimeToken }),
118
+ signal: controller.signal
119
+ });
120
+ const result = await readPortalResult(response);
121
+ const bearer = result.ok && typeof result.data?.token === "string" ? result.data.token : null;
122
+ if (!bearer) return null;
123
+ writeBearer(apiOrigin, bearer);
124
+ return bearer;
125
+ } catch {
126
+ pendingHandoffTokens.set(apiOrigin, oneTimeToken);
127
+ return null;
128
+ } finally {
129
+ clearTimeout(timer);
130
+ }
131
+ })();
132
+ pendingHandoffExchanges.set(apiOrigin, exchange);
133
+ try {
134
+ return await exchange;
135
+ } finally {
136
+ if (pendingHandoffExchanges.get(apiOrigin) === exchange) {
137
+ pendingHandoffExchanges.delete(apiOrigin);
138
+ }
139
+ }
140
+ }
15
141
  var PORTAL_REQUEST_INIT = {
16
142
  credentials: "include",
17
143
  cache: "no-store"
@@ -71,17 +197,41 @@ function createPortalClient(config = {}) {
71
197
  const portalHref = config.portalUrl?.trim() ?? "";
72
198
  const available = apiOrigin !== "" || sameOriginAvailable(config.portalOrigin);
73
199
  const fetchImpl = config.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
200
+ const handoffEnabled = localhostHandoffEnabled(apiOrigin);
201
+ consumePortalHandoffFragment();
202
+ adoptParkedHandoff(apiOrigin);
74
203
  const url = (path) => apiOrigin && path.startsWith("/") ? apiOrigin + path : path;
204
+ const fetchWithSession = async (input, init) => {
205
+ let belongsToApiOrigin = false;
206
+ if (handoffEnabled) {
207
+ try {
208
+ belongsToApiOrigin = new URL(input, window.location.href).origin === apiOrigin;
209
+ } catch {
210
+ }
211
+ }
212
+ const bearer = belongsToApiOrigin ? await exchangePortalHandoff(apiOrigin, fetchImpl) : null;
213
+ if (!bearer) return fetchImpl(input, init);
214
+ const headers = new Headers(init?.headers);
215
+ headers.set("authorization", `Bearer ${bearer}`);
216
+ return fetchImpl(input, { ...init, headers });
217
+ };
75
218
  async function request(path, init, timeoutMs = BRIDGE_TIMEOUT_MS) {
76
219
  const controller = new AbortController();
77
220
  const timer = setTimeout(() => controller.abort(), timeoutMs);
78
221
  try {
79
- const response = await fetchImpl(url(path), {
222
+ const response = await fetchWithSession(url(path), {
80
223
  ...PORTAL_REQUEST_INIT,
81
224
  ...init,
82
225
  signal: controller.signal
83
226
  });
84
- return await readPortalResult(response);
227
+ const result = await readPortalResult(response);
228
+ if (handoffEnabled) {
229
+ const body = result.ok ? result.data : null;
230
+ if (path === "/portal/api/sign-out" && result.ok || !result.ok && result.status === 401 || body?.signedIn === false) {
231
+ clearBearer(apiOrigin);
232
+ }
233
+ }
234
+ return result;
85
235
  } catch {
86
236
  return { ok: false, status: null, error: null };
87
237
  } finally {
@@ -95,18 +245,27 @@ function createPortalClient(config = {}) {
95
245
  body: JSON.stringify(body ?? {})
96
246
  });
97
247
  const del = (path) => request(path, { method: "DELETE", headers: { accept: "application/json" } });
98
- const returnPath = () => {
248
+ const returnPath = (targetUrl) => {
99
249
  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)}`;
250
+ let target = targetUrl ?? window.location.href;
251
+ try {
252
+ const parsed2 = new URL(target, window.location.href);
253
+ if (parsed2.origin !== window.location.origin) target = window.location.href;
254
+ else target = parsed2.href;
255
+ } catch {
256
+ target = window.location.href;
257
+ }
258
+ const parsed = new URL(target);
259
+ const path = `${parsed.pathname || "/"}${parsed.search}${parsed.hash}`;
260
+ if (!apiOrigin && !targetUrl) return path;
261
+ return `${PORTAL_RETURN_PATH}?to=${encodePortalReturnTarget(target)}`;
103
262
  };
104
263
  return {
105
264
  available,
106
265
  apiOrigin,
107
266
  portalHref,
108
267
  url,
109
- fetch: fetchImpl,
268
+ fetch: fetchWithSession,
110
269
  async whoAmI() {
111
270
  if (!available) return { signedIn: false };
112
271
  const result = await request(
@@ -134,6 +293,15 @@ function createPortalClient(config = {}) {
134
293
  const result = await send(PORTAL_PUBLIC_CLAIM_PATH, "POST", {});
135
294
  return result.ok ? result.data : null;
136
295
  },
296
+ async redeemApplicationRunHandoff(handoffId) {
297
+ if (!available) return null;
298
+ const result = await send(
299
+ `${PORTAL_APPLICATION_HANDOFF_PATH}/${encodeURIComponent(handoffId)}/redeem`,
300
+ "POST",
301
+ {}
302
+ );
303
+ return result.ok ? result.data : null;
304
+ },
137
305
  returnPath,
138
306
  get,
139
307
  send,
@@ -145,13 +313,9 @@ function createPortalClient(config = {}) {
145
313
  import { jsx, jsxs } from "react/jsx-runtime";
146
314
  var defaultPortalClient = createPortalClient();
147
315
  var PORTAL_SIGN_OUT_URL = "/portal/api/sign-out";
148
- function portalFetch(fetchImpl) {
149
- return fetchImpl ?? ((input, init) => fetch(input, init));
150
- }
151
316
  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);
317
+ function portalFetchForClient(client2) {
318
+ const bound = (input, init) => client2.fetch(client2.url(input), init);
155
319
  bound[BOUND_PORTAL_CLIENT] = client2;
156
320
  return bound;
157
321
  }
@@ -563,6 +727,25 @@ function resetSession() {
563
727
  clientKey = "";
564
728
  }
565
729
  var flowKey = (requestId) => `pos:flow:${requestId}`;
730
+ var applicationRunKey = (runId) => `pos:application-run:${runId}`;
731
+ function saveApplicationRunToken(c, runId) {
732
+ const token = c.getApplicationRunToken(runId);
733
+ if (!token) return;
734
+ try {
735
+ session()?.setItem(applicationRunKey(runId), token);
736
+ } catch {
737
+ }
738
+ }
739
+ function restoreApplicationRunToken(c, runId) {
740
+ try {
741
+ const token = session()?.getItem(applicationRunKey(runId)) ?? null;
742
+ if (!token) return false;
743
+ c.setApplicationRunToken(runId, token);
744
+ return true;
745
+ } catch {
746
+ return false;
747
+ }
748
+ }
566
749
  function saveFlowRecord(requestId, record) {
567
750
  try {
568
751
  session()?.setItem(flowKey(requestId), JSON.stringify(record));
@@ -1277,12 +1460,16 @@ export {
1277
1460
  FlowErrorSummary,
1278
1461
  focusField,
1279
1462
  FlowLoading,
1463
+ normalizePortalApiOrigin,
1464
+ encodePortalReturnTarget,
1280
1465
  createPortalClient,
1281
1466
  PatientOSApiError,
1282
1467
  patientos,
1283
1468
  persistClaimToken,
1284
1469
  adoptClaimToken,
1285
1470
  resetSession,
1471
+ saveApplicationRunToken,
1472
+ restoreApplicationRunToken,
1286
1473
  saveFlowRecord,
1287
1474
  loadFlowRecord,
1288
1475
  once,
@@ -6,7 +6,7 @@ import {
6
6
  PortalAccountClient,
7
7
  PortalPanelClient,
8
8
  StoreClient
9
- } from "./chunk-S7U2HOV7.js";
9
+ } from "./chunk-7ZOQ6UKG.js";
10
10
  import {
11
11
  mountIslandsWith
12
12
  } from "./chunk-THMT43MV.js";
package/dist/index.d.ts CHANGED
@@ -2,12 +2,13 @@ export { BPOINT_SCRIPT_ORIGIN, BPOINT_SCRIPT_URL, CONSULT_CHUNK_PATH, THIRD_PART
2
2
  import * as React from 'react';
3
3
  export { ISLAND_ATTR, ISLAND_PROPS_ATTR, Island, IslandProps, IslandRegistry, mountIslandsWith, readIslandProps } from './islands.js';
4
4
  export { ISLANDS, IslandName, mountIslands } from './islands-registry.js';
5
- import { P as PortalClient, W as WhoAmI, a as PortalPrefill, b as PublicClaim } from './portal-account-pI1cWgsT.js';
6
- export { F as FetchLike, c as PortalAccount, d as PortalAccountMarkerProps, e as PortalAccountProps, f as PortalAccountSurface, g as PortalClientConfig, h as PortalPanel, i as PortalPanelMarkerProps, j as PortalPanelProps, k as PortalResult, l as PortalSurface, m as createPortalClient } from './portal-account-pI1cWgsT.js';
7
- export { B as BookingBlock, a as BookingBlockMarkerProps, b as BookingBlockProps, c as BookingTypeOption, C as Cart, d as CartMarkerProps, e as CartProps, f as CertificateFunnel, g as CertificateFunnelMarkerProps, h as CertificateFunnelProps, i as Checkout, j as CheckoutMarkerProps, k as CheckoutProps, F as FunnelPublicApi, l as FunnelServiceOption, S as Store, m as StoreMarkerProps, n as StoreProps } from './checkout-block-Cog4H4I4.js';
5
+ import { P as PortalClient, W as WhoAmI, a as PortalPrefill, b as PublicClaim, F as FetchLike } from './portal-account-8uQc_Ncx.js';
6
+ export { c as PortalAccount, d as PortalAccountMarkerProps, e as PortalAccountProps, f as PortalAccountSurface, g as PortalClientConfig, h as PortalPanel, i as PortalPanelMarkerProps, j as PortalPanelProps, k as PortalResult, l as PortalSurface, m as createPortalClient } from './portal-account-8uQc_Ncx.js';
7
+ export { B as BookingBlock, a as BookingBlockMarkerProps, b as BookingBlockProps, c as BookingTypeOption, C as Cart, d as CartMarkerProps, e as CartProps, f as CertificateFunnel, g as CertificateFunnelMarkerProps, h as CertificateFunnelProps, i as Checkout, j as CheckoutMarkerProps, k as CheckoutProps, F as FunnelPublicApi, l as FunnelServiceOption, S as Store, m as StoreMarkerProps, n as StoreProps } from './checkout-block-BdpPIuQ5.js';
8
8
  export { PatientSurfaceTheme, patientSurfaceTheme } from './patient-surface-theme.js';
9
9
  export { Rgb, contrastRatio, deriveForeground, parseHexColor, relativeLuminance } from './contrast.js';
10
10
  export { AnswerValue, Answers, ShowWhenCondition, evaluateShowWhen, visibleQuestions } from './show-when.js';
11
+ import '@patientos/public-sdk';
11
12
 
12
13
  /**
13
14
  * Theme tokens for the website-kit.
@@ -209,6 +210,33 @@ interface PortalSession {
209
210
  */
210
211
  declare function usePortalSession(): PortalSession;
211
212
 
213
+ interface StoreClientConfig {
214
+ /** The validated PatientOS patient/API origin. Empty means same-origin. */
215
+ apiOrigin?: string | null;
216
+ /** Injectable browser transport for tests. */
217
+ fetchImpl?: FetchLike;
218
+ }
219
+ type StoreResult<T> = {
220
+ ok: true;
221
+ data: T;
222
+ } | {
223
+ ok: false;
224
+ status: number | null;
225
+ error: string | null;
226
+ message: string | null;
227
+ };
228
+ interface StoreClient {
229
+ readonly apiOrigin: string;
230
+ url(path: string): string;
231
+ get<T>(path: string): Promise<StoreResult<T>>;
232
+ post<T>(path: string, body: unknown, options?: {
233
+ signal?: AbortSignal;
234
+ }): Promise<StoreResult<T>>;
235
+ /** PatientOS magic-link sign-in that returns to this site's checkout. */
236
+ signInHref(returnPath?: string): string;
237
+ }
238
+ declare function createStoreClient(config?: StoreClientConfig): StoreClient;
239
+
212
240
  declare const WEBSITE_KIT_VERSION = "0.1.0";
213
241
 
214
- export { Button, type ButtonProps, type ButtonVariant, Container, type ContainerProps, FAQ, type FAQProps, type FaqItem, Hero, type HeroProps, Hours, type HoursProps, type HoursRow, MapEmbed, type MapEmbedProps, PortalClient, PortalPrefill, PortalProvider, type PortalProviderProps, type PortalSession, type PortalSessionStatus, PublicClaim, Row, type RowProps, Section, type SectionProps, type Service, ServiceGrid, type ServiceGridProps, Stack, type StackProps, TeamGrid, type TeamGridProps, type TeamMember, ThemeProvider, type ThemeProviderProps, WEBSITE_KIT_VERSION, WebsiteShell, type WebsiteShellProps, type WebsiteTheme, WhoAmI, defaultTheme, resolveTheme, themeToCssVars, usePortalClient, usePortalSession };
242
+ export { Button, type ButtonProps, type ButtonVariant, Container, type ContainerProps, FAQ, type FAQProps, type FaqItem, FetchLike, Hero, type HeroProps, Hours, type HoursProps, type HoursRow, MapEmbed, type MapEmbedProps, PortalClient, PortalPrefill, PortalProvider, type PortalProviderProps, type PortalSession, type PortalSessionStatus, PublicClaim, Row, type RowProps, Section, type SectionProps, type Service, ServiceGrid, type ServiceGridProps, Stack, type StackProps, type StoreClient, type StoreClientConfig, type StoreResult, TeamGrid, type TeamGridProps, type TeamMember, ThemeProvider, type ThemeProviderProps, WEBSITE_KIT_VERSION, WebsiteShell, type WebsiteShellProps, type WebsiteTheme, WhoAmI, createStoreClient, defaultTheme, resolveTheme, themeToCssVars, usePortalClient, usePortalSession };
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  ISLANDS,
3
3
  mountIslands
4
- } from "./chunk-Y67WZPV5.js";
4
+ } from "./chunk-ZZFBTLR4.js";
5
5
  import {
6
6
  PortalAccount,
7
7
  PortalPanel,
8
8
  PortalProvider,
9
+ createStoreClient,
9
10
  getAppointmentTypes,
10
11
  getClinic,
11
12
  getPublicApi,
@@ -13,7 +14,7 @@ import {
13
14
  getThemeTokens,
14
15
  usePortalClient,
15
16
  usePortalSession
16
- } from "./chunk-S7U2HOV7.js";
17
+ } from "./chunk-7ZOQ6UKG.js";
17
18
  import {
18
19
  BPOINT_SCRIPT_ORIGIN,
19
20
  BPOINT_SCRIPT_URL,
@@ -35,7 +36,7 @@ import {
35
36
  } from "./chunk-THMT43MV.js";
36
37
  import {
37
38
  createPortalClient
38
- } from "./chunk-BHVDY6SC.js";
39
+ } from "./chunk-ZYX4TXBN.js";
39
40
  import {
40
41
  contrastRatio,
41
42
  deriveForeground,
@@ -403,6 +404,8 @@ function BookingBlock({
403
404
  import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
404
405
  function Store({
405
406
  categoryHandle,
407
+ productHandle,
408
+ storeApiOrigin,
406
409
  title,
407
410
  columns,
408
411
  className
@@ -411,7 +414,7 @@ function Store({
411
414
  Island,
412
415
  {
413
416
  name: "store",
414
- props: { categoryHandle, title, columns },
417
+ props: { categoryHandle, productHandle, storeApiOrigin, title, columns },
415
418
  className: ["sk-store", className].filter(Boolean).join(" "),
416
419
  children: [
417
420
  /* @__PURE__ */ jsx6("h2", { className: "sk-store__heading", children: title ?? "Shop" }),
@@ -423,20 +426,41 @@ function Store({
423
426
 
424
427
  // src/cart-block.tsx
425
428
  import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
426
- function Cart({ className }) {
427
- return /* @__PURE__ */ jsxs6(Island, { name: "cart", className: ["sk-cart", className].filter(Boolean).join(" "), children: [
428
- /* @__PURE__ */ jsx7("h2", { className: "sk-cart__heading", children: "Your cart" }),
429
- /* @__PURE__ */ jsx7("p", { className: "sk-cart__placeholder", children: "Your cart needs JavaScript to load." })
430
- ] });
429
+ function Cart({ storeApiOrigin, className }) {
430
+ return /* @__PURE__ */ jsxs6(
431
+ Island,
432
+ {
433
+ name: "cart",
434
+ props: { storeApiOrigin },
435
+ className: ["sk-cart", className].filter(Boolean).join(" "),
436
+ children: [
437
+ /* @__PURE__ */ jsx7("h2", { className: "sk-cart__heading", children: "Your cart" }),
438
+ /* @__PURE__ */ jsx7("p", { className: "sk-cart__placeholder", children: "Your cart needs JavaScript to load." })
439
+ ]
440
+ }
441
+ );
431
442
  }
432
443
 
433
444
  // src/checkout-block.tsx
434
445
  import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
435
- function Checkout({ className }) {
436
- return /* @__PURE__ */ jsxs7(Island, { name: "checkout", className: ["sk-checkout", className].filter(Boolean).join(" "), children: [
437
- /* @__PURE__ */ jsx8("h2", { className: "sk-checkout__heading", children: "Checkout" }),
438
- /* @__PURE__ */ jsx8("p", { className: "sk-checkout__placeholder", children: "Checkout needs JavaScript to load." })
439
- ] });
446
+ function Checkout({
447
+ storeApiOrigin,
448
+ ordersHref,
449
+ completionHref,
450
+ className
451
+ }) {
452
+ return /* @__PURE__ */ jsxs7(
453
+ Island,
454
+ {
455
+ name: "checkout",
456
+ props: { storeApiOrigin, ordersHref, completionHref },
457
+ className: ["sk-checkout", className].filter(Boolean).join(" "),
458
+ children: [
459
+ /* @__PURE__ */ jsx8("h2", { className: "sk-checkout__heading", children: "Checkout" }),
460
+ /* @__PURE__ */ jsx8("p", { className: "sk-checkout__placeholder", children: "Checkout needs JavaScript to load." })
461
+ ]
462
+ }
463
+ );
440
464
  }
441
465
 
442
466
  // src/index.ts
@@ -476,6 +500,7 @@ export {
476
500
  WebsiteShell,
477
501
  contrastRatio,
478
502
  createPortalClient,
503
+ createStoreClient,
479
504
  defaultTheme,
480
505
  deriveForeground,
481
506
  evaluateShowWhen,
@@ -1,7 +1,8 @@
1
1
  import * as React from 'react';
2
- import { b as BookingBlockProps, h as CertificateFunnelProps, n as StoreProps, e as CartProps, k as CheckoutProps } from './checkout-block-Cog4H4I4.js';
3
- import { j as PortalPanelProps, F as FetchLike } from './portal-account-pI1cWgsT.js';
4
- export { P as PortalAccountClient } from './portal-account.client-DPRRmfCJ.js';
2
+ import { b as BookingBlockProps, h as CertificateFunnelProps, n as StoreProps, e as CartProps, k as CheckoutProps } from './checkout-block-BdpPIuQ5.js';
3
+ import { j as PortalPanelProps, F as FetchLike } from './portal-account-8uQc_Ncx.js';
4
+ export { P as PortalAccountClient } from './portal-account.client-BBe_q9yC.js';
5
+ import '@patientos/public-sdk';
5
6
 
6
7
  declare function BookingBlockClient(props: BookingBlockProps): React.ReactElement;
7
8
 
@@ -58,10 +59,10 @@ type PortalPanelClientProps = PortalPanelProps & {
58
59
  };
59
60
  declare function PortalPanelClient({ surface, portalUrl, portalApiOrigin, initialState, fetchImpl, }: PortalPanelClientProps): React.ReactElement;
60
61
 
61
- declare function StoreClient({ categoryHandle, title, columns }: StoreProps): React.ReactElement;
62
+ declare function StoreClient({ categoryHandle, productHandle, storeApiOrigin, title, columns, }: StoreProps): React.ReactElement;
62
63
 
63
- declare function CartClient(_props: CartProps): React.ReactElement;
64
+ declare function CartClient({ storeApiOrigin }: CartProps): React.ReactElement;
64
65
 
65
- declare function CheckoutClient(_props: CheckoutProps): React.ReactElement;
66
+ declare function CheckoutClient({ storeApiOrigin, ordersHref, completionHref, }: CheckoutProps): React.ReactElement;
66
67
 
67
68
  export { BookingBlockClient, CartClient, CertificateFunnelClient, CheckoutClient, PortalPanelClient, StoreClient };
@@ -6,11 +6,11 @@ import {
6
6
  PortalAccountClient,
7
7
  PortalPanelClient,
8
8
  StoreClient
9
- } from "./chunk-S7U2HOV7.js";
9
+ } from "./chunk-7ZOQ6UKG.js";
10
10
  import "./chunk-ZOF22TJA.js";
11
11
  import "./chunk-QDO3SJWR.js";
12
12
  import "./chunk-THMT43MV.js";
13
- import "./chunk-BHVDY6SC.js";
13
+ import "./chunk-ZYX4TXBN.js";
14
14
  import "./chunk-MLKGABMK.js";
15
15
  export {
16
16
  BookingBlockClient,
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  ISLANDS,
3
3
  mountIslands
4
- } from "./chunk-Y67WZPV5.js";
5
- import "./chunk-S7U2HOV7.js";
4
+ } from "./chunk-ZZFBTLR4.js";
5
+ import "./chunk-7ZOQ6UKG.js";
6
6
  import "./chunk-ZOF22TJA.js";
7
7
  import "./chunk-QDO3SJWR.js";
8
8
  import "./chunk-THMT43MV.js";
9
- import "./chunk-BHVDY6SC.js";
9
+ import "./chunk-ZYX4TXBN.js";
10
10
  import "./chunk-MLKGABMK.js";
11
11
  export {
12
12
  ISLANDS,
@@ -1,3 +1,4 @@
1
+ import { ApplicationRun } from '@patientos/public-sdk';
1
2
  import * as React from 'react';
2
3
 
3
4
  /** The injectable browser transport. */
@@ -64,8 +65,13 @@ interface PortalClient {
64
65
  prefill(): Promise<PortalPrefill | null>;
65
66
  /** Mint a public claim only for the signed-in patient. */
66
67
  mintPublicClaim(): Promise<PublicClaim | null>;
67
- /** Current page callback, using the app-side return hop when cross-origin. */
68
- returnPath(): string;
68
+ /** Redeem a single-use magic-link continuation after patient sign-in. */
69
+ redeemApplicationRunHandoff(handoffId: string): Promise<{
70
+ run: ApplicationRun;
71
+ capabilityToken: string;
72
+ } | null>;
73
+ /** Page callback, using the app-side return hop when cross-origin. */
74
+ returnPath(targetUrl?: string): string;
69
75
  /** Back-compat account-surface transport; callers still use constant paths. */
70
76
  get<T>(path: string): Promise<PortalResult<T>>;
71
77
  /** Back-compat account-surface JSON write; callers still use constant paths. */
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { e as PortalAccountProps, F as FetchLike } from './portal-account-pI1cWgsT.js';
2
+ import { e as PortalAccountProps, F as FetchLike } from './portal-account-8uQc_Ncx.js';
3
3
 
4
4
  type PortalAddress = {
5
5
  id: string;
@@ -1,6 +1,7 @@
1
1
  import * as React from 'react';
2
- import { F as FetchLike, k as PortalResult } from './portal-account-pI1cWgsT.js';
3
- import { a as PortalAppointment, b as PortalAppointmentsBody } from './portal-account.client-DPRRmfCJ.js';
2
+ import { F as FetchLike, k as PortalResult } from './portal-account-8uQc_Ncx.js';
3
+ import { a as PortalAppointment, b as PortalAppointmentsBody } from './portal-account.client-BBe_q9yC.js';
4
+ import '@patientos/public-sdk';
4
5
 
5
6
  /** One bookable instant, collapsing the practitioners free at the same time. */
6
7
  interface TimeSlot {
@@ -22,7 +22,7 @@ import {
22
22
  releasePortalHold,
23
23
  rescheduleContextTypeId,
24
24
  reschedulePortalAppointmentAndReload
25
- } from "./chunk-BHVDY6SC.js";
25
+ } from "./chunk-ZYX4TXBN.js";
26
26
  import "./chunk-MLKGABMK.js";
27
27
  export {
28
28
  AVAILABILITY_FAILED_COPY,
@@ -2868,7 +2868,9 @@ select.sk-identity__input[data-empty] {
2868
2868
  grid-template-columns: repeat(2, minmax(0, 1fr));
2869
2869
  }
2870
2870
  }
2871
- @media (max-width: 480px) {
2871
+ /* Two catalogue columns fit ordinary 390–430px phones and keep a 54-item shop
2872
+ scannable. Below 380px one column preserves readable product names. */
2873
+ @media (max-width: 379px) {
2872
2874
  .sk-store__grid {
2873
2875
  grid-template-columns: minmax(0, 1fr);
2874
2876
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patientos/website-kit",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "PatientOS clinic website components and patient-facing interactive islands.",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -109,12 +109,13 @@
109
109
  "@types/node": "^25.6.0",
110
110
  "@types/react": "^19.2.14",
111
111
  "@types/react-dom": "^19.2.3",
112
+ "jsdom": "^29.1.1",
112
113
  "tsup": "^8.5.0",
113
114
  "typescript": "^5.9.3",
114
115
  "vitest": "^4.1.5",
115
116
  "livekit-client": "^2.20.0"
116
117
  },
117
118
  "dependencies": {
118
- "@patientos/public-sdk": "^0.2.0"
119
+ "@patientos/public-sdk": "^0.2.2"
119
120
  }
120
121
  }