@patientos/website-kit 0.2.6 → 0.2.8

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.
Files changed (44) hide show
  1. package/README.md +54 -2
  2. package/dist/booking-block-D-UOAPx3.d.ts +74 -0
  3. package/dist/cart-block-B-ytc9us.d.ts +20 -0
  4. package/dist/cart-button-BCLFittB.d.ts +24 -0
  5. package/dist/cart-storage.d.ts +110 -0
  6. package/dist/cart-storage.js +39 -0
  7. package/dist/checkout-block-BeXwpqYe.d.ts +24 -0
  8. package/dist/chunk-2CZFJOKK.js +79 -0
  9. package/dist/{chunk-ZYX4TXBN.js → chunk-3T7UWKAH.js} +4 -310
  10. package/dist/{chunk-7ZOQ6UKG.js → chunk-4GKSSM5N.js} +578 -3163
  11. package/dist/chunk-4NF7SSIX.js +367 -0
  12. package/dist/chunk-7BUDUYXN.js +80 -0
  13. package/dist/chunk-FZ4WKWIE.js +310 -0
  14. package/dist/{chunk-ZZFBTLR4.js → chunk-HTVKKLWI.js} +20 -5
  15. package/dist/chunk-IOC6QLB7.js +159 -0
  16. package/dist/chunk-S3ZQQOQT.js +2057 -0
  17. package/dist/chunk-UTAMB2SK.js +214 -0
  18. package/dist/chunk-WQSJ46TP.js +19 -0
  19. package/dist/chunk-ZNH6TSYP.js +167 -0
  20. package/dist/index.d.ts +12 -32
  21. package/dist/index.js +88 -26
  22. package/dist/islands-impl/cart-button.d.ts +19 -0
  23. package/dist/islands-impl/cart-button.js +13 -0
  24. package/dist/islands-impl/cart.d.ts +31 -0
  25. package/dist/islands-impl/cart.js +11 -0
  26. package/dist/islands-impl/checkout.d.ts +6 -0
  27. package/dist/islands-impl/checkout.js +13 -0
  28. package/dist/islands-impl/store.d.ts +30 -0
  29. package/dist/islands-impl/store.js +16 -0
  30. package/dist/islands-impl.d.ts +14 -10
  31. package/dist/islands-impl.js +22 -7
  32. package/dist/islands-registry.js +13 -4
  33. package/dist/{portal-account-8uQc_Ncx.d.ts → portal-account-DRJyr3nz.d.ts} +1 -82
  34. package/dist/{portal-account.client-BBe_q9yC.d.ts → portal-account.client-Bke4NsgW.d.ts} +2 -1
  35. package/dist/portal-booking-client.d.ts +3 -2
  36. package/dist/portal-booking-client.js +2 -1
  37. package/dist/portal-client-my3q5GME.d.ts +83 -0
  38. package/dist/store-block-CFxnsfKU.d.ts +37 -0
  39. package/dist/store-catalog-p7TFOpfk.d.ts +78 -0
  40. package/dist/store-catalog.d.ts +3 -0
  41. package/dist/store-catalog.js +11 -0
  42. package/dist/website-kit.css +193 -0
  43. package/package.json +31 -1
  44. package/dist/checkout-block-BdpPIuQ5.d.ts +0 -136
@@ -0,0 +1,367 @@
1
+ // src/cart-storage.ts
2
+ var CART_STORAGE_KEY = "patientos.cart.v1";
3
+ var MAX_LINES = 50;
4
+ var MAX_QUANTITY = 99;
5
+ function emptyCart() {
6
+ return { v: 1, lines: [] };
7
+ }
8
+ var EMPTY_CART = Object.freeze({
9
+ v: 1,
10
+ lines: Object.freeze([])
11
+ });
12
+ function cartLineId(value) {
13
+ if (typeof value !== "string") return null;
14
+ const normalized = value.trim();
15
+ return normalized.length > 0 && normalized.length <= 128 ? normalized : null;
16
+ }
17
+ function legacyCartLineId(kind, variantId) {
18
+ const input = `${kind}\0${variantId}`;
19
+ let first = 2166136261;
20
+ let second = 2246822507;
21
+ for (let index = 0; index < input.length; index += 1) {
22
+ const code = input.charCodeAt(index);
23
+ first = Math.imul(first ^ code, 16777619);
24
+ second = Math.imul(second ^ code, 3266489909);
25
+ }
26
+ return `legacy-${(first >>> 0).toString(16).padStart(8, "0")}${(second >>> 0).toString(16).padStart(8, "0")}`;
27
+ }
28
+ function newCartLineId() {
29
+ return crypto.randomUUID();
30
+ }
31
+ function storage() {
32
+ try {
33
+ if (typeof window === "undefined" || !window.localStorage) return null;
34
+ return window.localStorage;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ function parseLine(raw) {
40
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
41
+ const line = raw;
42
+ const variantId = typeof line.variantId === "string" ? line.variantId.trim() : "";
43
+ if (!variantId) return null;
44
+ const kind = line.kind === "retail" ? "retail" : "fill";
45
+ const n = typeof line.quantity === "number" ? line.quantity : Number(line.quantity);
46
+ if (!Number.isFinite(n)) return null;
47
+ const quantity = Math.min(Math.max(Math.floor(n), 1), MAX_QUANTITY);
48
+ const lineId = cartLineId(line.lineId) ?? legacyCartLineId(kind, variantId);
49
+ return { lineId, kind, variantId, quantity };
50
+ }
51
+ function normalizeCartLines(entries) {
52
+ const lines = [];
53
+ const byIdentity = /* @__PURE__ */ new Map();
54
+ for (const entry of entries) {
55
+ const line = parseLine(entry);
56
+ if (!line) continue;
57
+ const identity = `${line.kind}\0${line.variantId}`;
58
+ const existing = byIdentity.get(identity);
59
+ if (existing) {
60
+ existing.quantity = Math.min(
61
+ existing.quantity + line.quantity,
62
+ MAX_QUANTITY
63
+ );
64
+ continue;
65
+ }
66
+ if (lines.length >= MAX_LINES) continue;
67
+ lines.push(line);
68
+ byIdentity.set(identity, line);
69
+ }
70
+ return lines;
71
+ }
72
+ function normalizeSettledAttemptTokens(entries) {
73
+ if (!Array.isArray(entries)) return [];
74
+ return Array.from(
75
+ new Set(
76
+ entries.filter(
77
+ (entry) => typeof entry === "string" && entry.length > 0 && entry.length <= 128
78
+ )
79
+ )
80
+ ).slice(-32);
81
+ }
82
+ function readCart() {
83
+ const store = storage();
84
+ if (!store) return emptyCart();
85
+ let raw;
86
+ try {
87
+ raw = store.getItem(CART_STORAGE_KEY);
88
+ } catch {
89
+ return emptyCart();
90
+ }
91
+ if (!raw) return emptyCart();
92
+ try {
93
+ const parsed = JSON.parse(raw);
94
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
95
+ return emptyCart();
96
+ const bag = parsed;
97
+ if (bag.v !== 1 || !Array.isArray(bag.lines)) return emptyCart();
98
+ const settledAttemptTokens = normalizeSettledAttemptTokens(
99
+ bag.settledAttemptTokens
100
+ );
101
+ return {
102
+ v: 1,
103
+ lines: normalizeCartLines(bag.lines),
104
+ ...settledAttemptTokens.length > 0 ? { settledAttemptTokens } : {}
105
+ };
106
+ } catch {
107
+ return emptyCart();
108
+ }
109
+ }
110
+ var CART_MUTATION_LOCK = "patientos-cart-mutation-v1";
111
+ var CART_MUTATION_VALIDATION_TIMEOUT_MS = 5e3;
112
+ function persistCart(cart) {
113
+ const settledAttemptTokens = normalizeSettledAttemptTokens(
114
+ cart.settledAttemptTokens
115
+ );
116
+ const normalised = {
117
+ v: 1,
118
+ lines: normalizeCartLines(cart.lines),
119
+ ...settledAttemptTokens.length > 0 ? { settledAttemptTokens } : {}
120
+ };
121
+ const store = storage();
122
+ if (!store)
123
+ return {
124
+ cart: readCart(),
125
+ persisted: false,
126
+ status: "cart_storage_unavailable"
127
+ };
128
+ try {
129
+ store.setItem(CART_STORAGE_KEY, JSON.stringify(normalised));
130
+ } catch {
131
+ return {
132
+ cart: readCart(),
133
+ persisted: false,
134
+ status: "cart_storage_unavailable"
135
+ };
136
+ }
137
+ notify(normalised);
138
+ return { cart: normalised, persisted: true, status: "updated" };
139
+ }
140
+ function unavailableMutation() {
141
+ return {
142
+ cart: readCart(),
143
+ persisted: false,
144
+ status: "cart_storage_unavailable"
145
+ };
146
+ }
147
+ async function mutateCart(mutation, beforePersist) {
148
+ const locks = typeof navigator === "undefined" ? void 0 : navigator.locks;
149
+ if (!storage() || !locks) return unavailableMutation();
150
+ try {
151
+ return await locks.request(CART_MUTATION_LOCK, async () => {
152
+ const latest = readCart();
153
+ const mutated = mutation(latest);
154
+ const proposed = {
155
+ ...mutated,
156
+ lines: normalizeCartLines(mutated.lines),
157
+ ...mutated.settledAttemptTokens === void 0 && latest.settledAttemptTokens ? { settledAttemptTokens: latest.settledAttemptTokens } : {}
158
+ };
159
+ if (beforePersist) {
160
+ const controller = new AbortController();
161
+ let timer;
162
+ const accepted = await Promise.race([
163
+ beforePersist(proposed, controller.signal).catch(() => false),
164
+ new Promise((resolve) => {
165
+ timer = setTimeout(() => {
166
+ controller.abort();
167
+ resolve(false);
168
+ }, CART_MUTATION_VALIDATION_TIMEOUT_MS);
169
+ })
170
+ ]);
171
+ if (timer) clearTimeout(timer);
172
+ if (!accepted) {
173
+ return {
174
+ cart: readCart(),
175
+ persisted: false,
176
+ status: "not_committed"
177
+ };
178
+ }
179
+ }
180
+ return persistCart(proposed);
181
+ });
182
+ } catch {
183
+ return unavailableMutation();
184
+ }
185
+ }
186
+ async function withCartLock(operation) {
187
+ const locks = typeof navigator === "undefined" ? void 0 : navigator.locks;
188
+ if (!storage() || !locks)
189
+ return { status: "cart_storage_unavailable", cart: readCart() };
190
+ try {
191
+ return await locks.request(CART_MUTATION_LOCK, async () => {
192
+ const latest = readCart();
193
+ const controller = new AbortController();
194
+ let timer;
195
+ const outcome = await Promise.race([
196
+ operation(latest, controller.signal).then((result) => ({
197
+ completed: true,
198
+ result
199
+ })),
200
+ new Promise((resolve) => {
201
+ timer = setTimeout(() => {
202
+ controller.abort();
203
+ resolve({ completed: false });
204
+ }, CART_MUTATION_VALIDATION_TIMEOUT_MS);
205
+ })
206
+ ]);
207
+ if (timer) clearTimeout(timer);
208
+ return outcome.completed ? { status: "completed", cart: latest, result: outcome.result } : { status: "not_completed", cart: latest };
209
+ });
210
+ } catch {
211
+ return { status: "not_completed", cart: readCart() };
212
+ }
213
+ }
214
+ async function writeCart(cart) {
215
+ return mutateCart(() => cart);
216
+ }
217
+ async function addToCart(variantId, quantity = 1) {
218
+ let status = "added";
219
+ const result = await mutateCart((cart) => {
220
+ const add = Math.min(Math.max(Math.floor(quantity), 1), MAX_QUANTITY);
221
+ const existing = cart.lines.find(
222
+ (line) => line.kind === "retail" && line.variantId === variantId
223
+ );
224
+ if (existing) {
225
+ if (existing.quantity >= MAX_QUANTITY) {
226
+ status = "quantity_limit";
227
+ return cart;
228
+ }
229
+ existing.quantity = Math.min(existing.quantity + add, MAX_QUANTITY);
230
+ } else {
231
+ if (cart.lines.length >= MAX_LINES) {
232
+ status = "line_limit";
233
+ return cart;
234
+ }
235
+ cart.lines.push({
236
+ lineId: newCartLineId(),
237
+ kind: "retail",
238
+ variantId,
239
+ quantity: add
240
+ });
241
+ }
242
+ return cart;
243
+ });
244
+ return {
245
+ cart: result.cart,
246
+ status: result.status === "cart_storage_unavailable" ? result.status : status
247
+ };
248
+ }
249
+ async function setCartQuantity(kind, variantId, quantity) {
250
+ const result = await mutateCart((cart) => {
251
+ const next = Math.floor(quantity);
252
+ return {
253
+ v: 1,
254
+ lines: next < 1 ? cart.lines.filter(
255
+ (line) => line.kind !== kind || line.variantId !== variantId
256
+ ) : cart.lines.map(
257
+ (line) => line.kind === kind && line.variantId === variantId ? { ...line, quantity: Math.min(next, MAX_QUANTITY) } : line
258
+ )
259
+ };
260
+ });
261
+ return result;
262
+ }
263
+ async function adjustCartQuantity(kind, variantId, delta, maximumBaseline) {
264
+ const result = await mutateCart((cart) => ({
265
+ v: 1,
266
+ lines: cart.lines.flatMap((line) => {
267
+ if (line.kind !== kind || line.variantId !== variantId) return [line];
268
+ const baseline = maximumBaseline === void 0 ? line.quantity : Math.min(line.quantity, Math.max(0, Math.floor(maximumBaseline)));
269
+ const quantity = Math.min(baseline + Math.floor(delta), MAX_QUANTITY);
270
+ return quantity > 0 ? [{ ...line, quantity }] : [];
271
+ })
272
+ }));
273
+ return result;
274
+ }
275
+ async function removeFromCart(variantId, kind) {
276
+ const result = await mutateCart((cart) => ({
277
+ v: 1,
278
+ lines: cart.lines.filter(
279
+ (line) => line.variantId !== variantId || kind && line.kind !== kind
280
+ )
281
+ }));
282
+ return result;
283
+ }
284
+ async function clearCart() {
285
+ return writeCart(emptyCart());
286
+ }
287
+ async function subtractPurchasedCartLines(purchasedLines, attemptToken) {
288
+ const purchased = new Map(
289
+ normalizeCartLines(
290
+ purchasedLines.filter(
291
+ (line) => Boolean(line) && typeof line === "object" && !Array.isArray(line) && cartLineId(line.lineId) !== null
292
+ )
293
+ ).map((line) => [line.lineId, line])
294
+ );
295
+ return mutateCart((cart) => {
296
+ if (attemptToken && cart.settledAttemptTokens?.includes(attemptToken))
297
+ return cart;
298
+ return {
299
+ v: 1,
300
+ lines: cart.lines.flatMap((line) => {
301
+ const purchasedLine = purchased.get(line.lineId);
302
+ const purchasedQuantity = purchasedLine?.kind === line.kind && purchasedLine.variantId === line.variantId ? purchasedLine.quantity : 0;
303
+ const quantity = line.quantity - purchasedQuantity;
304
+ return quantity > 0 ? [{ ...line, quantity }] : [];
305
+ }),
306
+ ...attemptToken ? {
307
+ settledAttemptTokens: [
308
+ ...cart.settledAttemptTokens ?? [],
309
+ attemptToken
310
+ ]
311
+ } : {}
312
+ };
313
+ });
314
+ }
315
+ function cartItemCount(cart = readCart()) {
316
+ return cart.lines.reduce((sum, l) => sum + l.quantity, 0);
317
+ }
318
+ function cartToWireLines(cart) {
319
+ return cart.lines.map((l) => ({
320
+ kind: l.kind,
321
+ variantId: l.variantId,
322
+ quantity: l.quantity
323
+ }));
324
+ }
325
+ var listeners = /* @__PURE__ */ new Set();
326
+ function notify(cart) {
327
+ for (const fn of listeners) {
328
+ try {
329
+ fn(cart);
330
+ } catch {
331
+ }
332
+ }
333
+ }
334
+ function subscribeToCart(fn) {
335
+ listeners.add(fn);
336
+ const onStorage = (e) => {
337
+ if (e.key !== null && e.key !== CART_STORAGE_KEY) return;
338
+ fn(readCart());
339
+ };
340
+ if (typeof window !== "undefined")
341
+ window.addEventListener("storage", onStorage);
342
+ return () => {
343
+ listeners.delete(fn);
344
+ if (typeof window !== "undefined")
345
+ window.removeEventListener("storage", onStorage);
346
+ };
347
+ }
348
+
349
+ export {
350
+ CART_STORAGE_KEY,
351
+ emptyCart,
352
+ EMPTY_CART,
353
+ normalizeCartLines,
354
+ readCart,
355
+ mutateCart,
356
+ withCartLock,
357
+ writeCart,
358
+ addToCart,
359
+ setCartQuantity,
360
+ adjustCartQuantity,
361
+ removeFromCart,
362
+ clearCart,
363
+ subtractPurchasedCartLines,
364
+ cartItemCount,
365
+ cartToWireLines,
366
+ subscribeToCart
367
+ };
@@ -0,0 +1,80 @@
1
+ import {
2
+ encodePortalReturnTarget,
3
+ normalizePortalApiOrigin
4
+ } from "./chunk-FZ4WKWIE.js";
5
+
6
+ // src/store-client.ts
7
+ var STORE_REQUEST_INIT = {
8
+ credentials: "include",
9
+ cache: "no-store"
10
+ };
11
+ async function readResult(response) {
12
+ let body;
13
+ try {
14
+ body = await response.json();
15
+ } catch {
16
+ return response.ok ? { ok: false, status: null, error: null, message: null } : { ok: false, status: response.status, error: null, message: null };
17
+ }
18
+ if (response.ok) return { ok: true, data: body };
19
+ const failure = body;
20
+ return {
21
+ ok: false,
22
+ status: response.status,
23
+ error: typeof failure?.error === "string" ? failure.error : null,
24
+ message: typeof failure?.message === "string" ? failure.message : null
25
+ };
26
+ }
27
+ function createStoreClient(config = {}) {
28
+ const rawOrigin = config.apiOrigin?.trim() ?? "";
29
+ const normalizedOrigin = rawOrigin ? normalizePortalApiOrigin(rawOrigin) : "";
30
+ if (rawOrigin && !normalizedOrigin) {
31
+ console.error("[store] ignoring a malformed store API origin", rawOrigin);
32
+ }
33
+ const apiOrigin = normalizedOrigin ?? "";
34
+ const fetchImpl = config.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
35
+ const url = (path) => apiOrigin && path.startsWith("/") ? apiOrigin + path : path;
36
+ async function request(path, init) {
37
+ try {
38
+ return await readResult(
39
+ await fetchImpl(url(path), {
40
+ ...STORE_REQUEST_INIT,
41
+ ...init
42
+ })
43
+ );
44
+ } catch {
45
+ return { ok: false, status: null, error: null, message: null };
46
+ }
47
+ }
48
+ return {
49
+ apiOrigin,
50
+ url,
51
+ get: (path) => request(path, { headers: { accept: "application/json" } }),
52
+ post: (path, body, options) => request(path, {
53
+ signal: options?.signal,
54
+ method: "POST",
55
+ headers: {
56
+ accept: "application/json",
57
+ "content-type": "application/json"
58
+ },
59
+ body: JSON.stringify(body ?? {})
60
+ }),
61
+ signInHref(returnPath = "/checkout") {
62
+ if (typeof window === "undefined") return `${apiOrigin}/portal/sign-in`;
63
+ const target = new URL(returnPath, window.location.origin).toString();
64
+ const redirect = `/portal/return?to=${encodePortalReturnTarget(target)}`;
65
+ return `${apiOrigin}/portal/sign-in?redirect=${encodeURIComponent(redirect)}`;
66
+ }
67
+ };
68
+ }
69
+
70
+ // src/money.ts
71
+ function formatMoney(value) {
72
+ const n = Number(value);
73
+ if (!Number.isFinite(n)) return value;
74
+ return `$${n.toFixed(2)}`;
75
+ }
76
+
77
+ export {
78
+ createStoreClient,
79
+ formatMoney
80
+ };