@patientos/website-kit 0.2.6 → 0.2.7
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/README.md +54 -2
- package/dist/booking-block-D-UOAPx3.d.ts +74 -0
- package/dist/cart-block-B-ytc9us.d.ts +20 -0
- package/dist/cart-button-BCLFittB.d.ts +24 -0
- package/dist/cart-storage.d.ts +110 -0
- package/dist/cart-storage.js +39 -0
- package/dist/checkout-block-BeXwpqYe.d.ts +24 -0
- package/dist/chunk-2CZFJOKK.js +79 -0
- package/dist/{chunk-ZYX4TXBN.js → chunk-3T7UWKAH.js} +4 -310
- package/dist/{chunk-7ZOQ6UKG.js → chunk-4GKSSM5N.js} +578 -3163
- package/dist/chunk-4NF7SSIX.js +367 -0
- package/dist/chunk-673YEXAI.js +164 -0
- package/dist/chunk-7BUDUYXN.js +80 -0
- package/dist/chunk-FZ4WKWIE.js +310 -0
- package/dist/{chunk-ZZFBTLR4.js → chunk-HYXTDUP2.js} +20 -5
- package/dist/chunk-IOC6QLB7.js +159 -0
- package/dist/chunk-S3ZQQOQT.js +2057 -0
- package/dist/chunk-WQSJ46TP.js +19 -0
- package/dist/chunk-ZNH6TSYP.js +167 -0
- package/dist/index.d.ts +12 -32
- package/dist/index.js +88 -26
- package/dist/islands-impl/cart-button.d.ts +19 -0
- package/dist/islands-impl/cart-button.js +13 -0
- package/dist/islands-impl/cart.d.ts +31 -0
- package/dist/islands-impl/cart.js +11 -0
- package/dist/islands-impl/checkout.d.ts +6 -0
- package/dist/islands-impl/checkout.js +13 -0
- package/dist/islands-impl/store.d.ts +22 -0
- package/dist/islands-impl/store.js +14 -0
- package/dist/islands-impl.d.ts +14 -10
- package/dist/islands-impl.js +22 -7
- package/dist/islands-registry.js +13 -4
- package/dist/{portal-account-8uQc_Ncx.d.ts → portal-account-DRJyr3nz.d.ts} +1 -82
- package/dist/{portal-account.client-BBe_q9yC.d.ts → portal-account.client-Bke4NsgW.d.ts} +2 -1
- package/dist/portal-booking-client.d.ts +3 -2
- package/dist/portal-booking-client.js +2 -1
- package/dist/portal-client-my3q5GME.d.ts +83 -0
- package/dist/store-block-CFxnsfKU.d.ts +37 -0
- package/dist/store-catalog-p7TFOpfk.d.ts +78 -0
- package/dist/store-catalog.d.ts +3 -0
- package/dist/store-catalog.js +11 -0
- package/dist/website-kit.css +193 -0
- package/package.json +31 -1
- package/dist/checkout-block-BdpPIuQ5.d.ts +0 -136
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// src/portal-client.ts
|
|
2
|
+
var PORTAL_WHOAMI_PATH = "/portal/api/whoami";
|
|
3
|
+
var PORTAL_MAGIC_LINK_PATH = "/portal/api/magic-link";
|
|
4
|
+
var PORTAL_PREFILL_PATH = "/portal/api/prefill";
|
|
5
|
+
var PORTAL_PUBLIC_CLAIM_PATH = "/portal/api/public-claim";
|
|
6
|
+
var PORTAL_APPLICATION_HANDOFF_PATH = "/portal/api/application-run-handoffs";
|
|
7
|
+
var PORTAL_SESSION_EXCHANGE_PATH = "/portal/api/session-exchange";
|
|
8
|
+
var PORTAL_RETURN_PATH = "/portal/return";
|
|
9
|
+
var PORTAL_HANDOFF_FRAGMENT_PREFIX = "#patientos-handoff=";
|
|
10
|
+
var BRIDGE_TIMEOUT_MS = 15e3;
|
|
11
|
+
var BRIDGE_PROBE_TIMEOUT_MS = 8e3;
|
|
12
|
+
var PORTAL_BEARER_STORAGE_PREFIX = "patientos.portal.bearer:";
|
|
13
|
+
var parkedHandoffPayload = null;
|
|
14
|
+
var pendingHandoffTokens = /* @__PURE__ */ new Map();
|
|
15
|
+
var pendingHandoffExchanges = /* @__PURE__ */ new Map();
|
|
16
|
+
var memoryBearers = /* @__PURE__ */ new Map();
|
|
17
|
+
function onLocalhostDevPage() {
|
|
18
|
+
if (typeof window === "undefined") return false;
|
|
19
|
+
return window.location.protocol === "http:" && window.location.hostname === "localhost" && window.location.port !== "";
|
|
20
|
+
}
|
|
21
|
+
function localhostHandoffEnabled(apiOrigin) {
|
|
22
|
+
return apiOrigin !== "" && onLocalhostDevPage();
|
|
23
|
+
}
|
|
24
|
+
function bearerStorage() {
|
|
25
|
+
try {
|
|
26
|
+
return typeof window === "undefined" ? null : window.sessionStorage;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function bearerStorageKey(apiOrigin) {
|
|
32
|
+
return `${PORTAL_BEARER_STORAGE_PREFIX}${apiOrigin}`;
|
|
33
|
+
}
|
|
34
|
+
function readBearer(apiOrigin) {
|
|
35
|
+
const memory = memoryBearers.get(apiOrigin);
|
|
36
|
+
if (memory) return memory;
|
|
37
|
+
try {
|
|
38
|
+
const stored = bearerStorage()?.getItem(bearerStorageKey(apiOrigin)) ?? null;
|
|
39
|
+
if (!stored || stored.length > 4096) {
|
|
40
|
+
clearBearer(apiOrigin);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
memoryBearers.set(apiOrigin, stored);
|
|
44
|
+
return stored;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function writeBearer(apiOrigin, token) {
|
|
50
|
+
memoryBearers.set(apiOrigin, token);
|
|
51
|
+
try {
|
|
52
|
+
bearerStorage()?.setItem(bearerStorageKey(apiOrigin), token);
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function clearBearer(apiOrigin) {
|
|
57
|
+
memoryBearers.delete(apiOrigin);
|
|
58
|
+
try {
|
|
59
|
+
bearerStorage()?.removeItem(bearerStorageKey(apiOrigin));
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function decodeBase64UrlJson(raw) {
|
|
64
|
+
const b64 = raw.replace(/-/g, "+").replace(/_/g, "/");
|
|
65
|
+
const binary = atob(b64.padEnd(Math.ceil(b64.length / 4) * 4, "="));
|
|
66
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
67
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
68
|
+
}
|
|
69
|
+
function consumePortalHandoffFragment() {
|
|
70
|
+
if (!onLocalhostDevPage()) return;
|
|
71
|
+
const raw = window.location.hash;
|
|
72
|
+
if (!raw.startsWith(PORTAL_HANDOFF_FRAGMENT_PREFIX)) return;
|
|
73
|
+
let payload = null;
|
|
74
|
+
try {
|
|
75
|
+
const decoded = decodeBase64UrlJson(raw.slice(PORTAL_HANDOFF_FRAGMENT_PREFIX.length));
|
|
76
|
+
const candidate = decoded;
|
|
77
|
+
if (typeof candidate?.token === "string" && candidate.token.length > 0 && candidate.token.length <= 512 && typeof candidate.returnHash === "string") {
|
|
78
|
+
payload = { token: candidate.token, returnHash: candidate.returnHash };
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
const restoredHash = payload?.returnHash ? `#${payload.returnHash}` : "";
|
|
83
|
+
window.history.replaceState(
|
|
84
|
+
window.history.state,
|
|
85
|
+
"",
|
|
86
|
+
`${window.location.pathname}${window.location.search}${restoredHash}`
|
|
87
|
+
);
|
|
88
|
+
if (payload) parkedHandoffPayload = payload;
|
|
89
|
+
}
|
|
90
|
+
function adoptParkedHandoff(apiOrigin) {
|
|
91
|
+
const payload = parkedHandoffPayload;
|
|
92
|
+
if (!payload || !localhostHandoffEnabled(apiOrigin)) return;
|
|
93
|
+
parkedHandoffPayload = null;
|
|
94
|
+
clearBearer(apiOrigin);
|
|
95
|
+
pendingHandoffTokens.set(apiOrigin, payload.token);
|
|
96
|
+
}
|
|
97
|
+
async function exchangePortalHandoff(apiOrigin, fetchImpl) {
|
|
98
|
+
const pending = pendingHandoffExchanges.get(apiOrigin);
|
|
99
|
+
if (pending) return pending;
|
|
100
|
+
const oneTimeToken = pendingHandoffTokens.get(apiOrigin);
|
|
101
|
+
if (!oneTimeToken) return readBearer(apiOrigin);
|
|
102
|
+
pendingHandoffTokens.delete(apiOrigin);
|
|
103
|
+
const exchange = (async () => {
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timer = setTimeout(() => controller.abort(), BRIDGE_PROBE_TIMEOUT_MS);
|
|
106
|
+
try {
|
|
107
|
+
const response = await fetchImpl(`${apiOrigin}${PORTAL_SESSION_EXCHANGE_PATH}`, {
|
|
108
|
+
...PORTAL_REQUEST_INIT,
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: { accept: "application/json", "content-type": "application/json" },
|
|
111
|
+
body: JSON.stringify({ token: oneTimeToken }),
|
|
112
|
+
signal: controller.signal
|
|
113
|
+
});
|
|
114
|
+
const result = await readPortalResult(response);
|
|
115
|
+
const bearer = result.ok && typeof result.data?.token === "string" ? result.data.token : null;
|
|
116
|
+
if (!bearer) return null;
|
|
117
|
+
writeBearer(apiOrigin, bearer);
|
|
118
|
+
return bearer;
|
|
119
|
+
} catch {
|
|
120
|
+
pendingHandoffTokens.set(apiOrigin, oneTimeToken);
|
|
121
|
+
return null;
|
|
122
|
+
} finally {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
}
|
|
125
|
+
})();
|
|
126
|
+
pendingHandoffExchanges.set(apiOrigin, exchange);
|
|
127
|
+
try {
|
|
128
|
+
return await exchange;
|
|
129
|
+
} finally {
|
|
130
|
+
if (pendingHandoffExchanges.get(apiOrigin) === exchange) {
|
|
131
|
+
pendingHandoffExchanges.delete(apiOrigin);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
var PORTAL_REQUEST_INIT = {
|
|
136
|
+
credentials: "include",
|
|
137
|
+
cache: "no-store"
|
|
138
|
+
};
|
|
139
|
+
function normalizePortalApiOrigin(raw) {
|
|
140
|
+
let url;
|
|
141
|
+
try {
|
|
142
|
+
url = new URL(raw.trim());
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
147
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) return null;
|
|
148
|
+
if (url.username || url.password || url.search || url.hash) return null;
|
|
149
|
+
if (url.pathname !== "/" && url.pathname !== "") return null;
|
|
150
|
+
return url.origin;
|
|
151
|
+
}
|
|
152
|
+
function encodePortalReturnTarget(url) {
|
|
153
|
+
const bytes = new TextEncoder().encode(url);
|
|
154
|
+
let binary = "";
|
|
155
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
156
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
157
|
+
}
|
|
158
|
+
function sameOriginAvailable(portalOrigin) {
|
|
159
|
+
if (typeof window === "undefined") return false;
|
|
160
|
+
if (portalOrigin == null) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
const normalizedOrigin = portalOrigin.trim().replace(/\/+$/, "");
|
|
164
|
+
return normalizedOrigin !== "" && window.location.origin === normalizedOrigin;
|
|
165
|
+
}
|
|
166
|
+
async function readPortalResult(response) {
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
let error = null;
|
|
169
|
+
let body;
|
|
170
|
+
try {
|
|
171
|
+
const parsed = await response.json();
|
|
172
|
+
if (typeof parsed?.error === "string") error = parsed.error;
|
|
173
|
+
body = parsed;
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
return { ok: false, status: response.status, error, body };
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
return { ok: true, data: await response.json() };
|
|
180
|
+
} catch {
|
|
181
|
+
return { ok: false, status: null, error: null };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function createPortalClient(config = {}) {
|
|
185
|
+
const rawOrigin = config.apiOrigin?.trim() ?? "";
|
|
186
|
+
const normalizedOrigin = rawOrigin ? normalizePortalApiOrigin(rawOrigin) : "";
|
|
187
|
+
if (rawOrigin && !normalizedOrigin) {
|
|
188
|
+
console.error("[portal] ignoring a malformed portal API origin", rawOrigin);
|
|
189
|
+
}
|
|
190
|
+
const apiOrigin = normalizedOrigin ?? "";
|
|
191
|
+
const portalHref = config.portalUrl?.trim() ?? "";
|
|
192
|
+
const available = apiOrigin !== "" || sameOriginAvailable(config.portalOrigin);
|
|
193
|
+
const fetchImpl = config.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
|
|
194
|
+
const handoffEnabled = localhostHandoffEnabled(apiOrigin);
|
|
195
|
+
consumePortalHandoffFragment();
|
|
196
|
+
adoptParkedHandoff(apiOrigin);
|
|
197
|
+
const url = (path) => apiOrigin && path.startsWith("/") ? apiOrigin + path : path;
|
|
198
|
+
const fetchWithSession = async (input, init) => {
|
|
199
|
+
let belongsToApiOrigin = false;
|
|
200
|
+
if (handoffEnabled) {
|
|
201
|
+
try {
|
|
202
|
+
belongsToApiOrigin = new URL(input, window.location.href).origin === apiOrigin;
|
|
203
|
+
} catch {
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const bearer = belongsToApiOrigin ? await exchangePortalHandoff(apiOrigin, fetchImpl) : null;
|
|
207
|
+
if (!bearer) return fetchImpl(input, init);
|
|
208
|
+
const headers = new Headers(init?.headers);
|
|
209
|
+
headers.set("authorization", `Bearer ${bearer}`);
|
|
210
|
+
return fetchImpl(input, { ...init, headers });
|
|
211
|
+
};
|
|
212
|
+
async function request(path, init, timeoutMs = BRIDGE_TIMEOUT_MS) {
|
|
213
|
+
const controller = new AbortController();
|
|
214
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
215
|
+
try {
|
|
216
|
+
const response = await fetchWithSession(url(path), {
|
|
217
|
+
...PORTAL_REQUEST_INIT,
|
|
218
|
+
...init,
|
|
219
|
+
signal: controller.signal
|
|
220
|
+
});
|
|
221
|
+
const result = await readPortalResult(response);
|
|
222
|
+
if (handoffEnabled) {
|
|
223
|
+
const body = result.ok ? result.data : null;
|
|
224
|
+
if (path === "/portal/api/sign-out" && result.ok || !result.ok && result.status === 401 || body?.signedIn === false) {
|
|
225
|
+
clearBearer(apiOrigin);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
229
|
+
} catch {
|
|
230
|
+
return { ok: false, status: null, error: null };
|
|
231
|
+
} finally {
|
|
232
|
+
clearTimeout(timer);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const get = (path) => request(path, { headers: { accept: "application/json" } });
|
|
236
|
+
const send = (path, method, body) => request(path, {
|
|
237
|
+
method,
|
|
238
|
+
headers: { accept: "application/json", "content-type": "application/json" },
|
|
239
|
+
body: JSON.stringify(body ?? {})
|
|
240
|
+
});
|
|
241
|
+
const del = (path) => request(path, { method: "DELETE", headers: { accept: "application/json" } });
|
|
242
|
+
const returnPath = (targetUrl) => {
|
|
243
|
+
if (typeof window === "undefined") return "/";
|
|
244
|
+
let target = targetUrl ?? window.location.href;
|
|
245
|
+
try {
|
|
246
|
+
const parsed2 = new URL(target, window.location.href);
|
|
247
|
+
if (parsed2.origin !== window.location.origin) target = window.location.href;
|
|
248
|
+
else target = parsed2.href;
|
|
249
|
+
} catch {
|
|
250
|
+
target = window.location.href;
|
|
251
|
+
}
|
|
252
|
+
const parsed = new URL(target);
|
|
253
|
+
const path = `${parsed.pathname || "/"}${parsed.search}${parsed.hash}`;
|
|
254
|
+
if (!apiOrigin && !targetUrl) return path;
|
|
255
|
+
return `${PORTAL_RETURN_PATH}?to=${encodePortalReturnTarget(target)}`;
|
|
256
|
+
};
|
|
257
|
+
return {
|
|
258
|
+
available,
|
|
259
|
+
apiOrigin,
|
|
260
|
+
portalHref,
|
|
261
|
+
url,
|
|
262
|
+
fetch: fetchWithSession,
|
|
263
|
+
async whoAmI() {
|
|
264
|
+
if (!available) return { signedIn: false };
|
|
265
|
+
const result = await request(
|
|
266
|
+
PORTAL_WHOAMI_PATH,
|
|
267
|
+
{ headers: { accept: "application/json" } },
|
|
268
|
+
BRIDGE_PROBE_TIMEOUT_MS
|
|
269
|
+
);
|
|
270
|
+
return result.ok && result.data?.signedIn === true ? result.data : { signedIn: false };
|
|
271
|
+
},
|
|
272
|
+
async requestMagicLink(email, redirect) {
|
|
273
|
+
if (!available) return false;
|
|
274
|
+
const result = await send(PORTAL_MAGIC_LINK_PATH, "POST", {
|
|
275
|
+
email,
|
|
276
|
+
redirect: redirect ?? returnPath()
|
|
277
|
+
});
|
|
278
|
+
return result.ok && result.data?.ok === true;
|
|
279
|
+
},
|
|
280
|
+
async prefill() {
|
|
281
|
+
if (!available) return null;
|
|
282
|
+
const result = await get(PORTAL_PREFILL_PATH);
|
|
283
|
+
return result.ok ? result.data : null;
|
|
284
|
+
},
|
|
285
|
+
async mintPublicClaim() {
|
|
286
|
+
if (!available) return null;
|
|
287
|
+
const result = await send(PORTAL_PUBLIC_CLAIM_PATH, "POST", {});
|
|
288
|
+
return result.ok ? result.data : null;
|
|
289
|
+
},
|
|
290
|
+
async redeemApplicationRunHandoff(handoffId) {
|
|
291
|
+
if (!available) return null;
|
|
292
|
+
const result = await send(
|
|
293
|
+
`${PORTAL_APPLICATION_HANDOFF_PATH}/${encodeURIComponent(handoffId)}/redeem`,
|
|
294
|
+
"POST",
|
|
295
|
+
{}
|
|
296
|
+
);
|
|
297
|
+
return result.ok ? result.data : null;
|
|
298
|
+
},
|
|
299
|
+
returnPath,
|
|
300
|
+
get,
|
|
301
|
+
send,
|
|
302
|
+
delete: del
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export {
|
|
307
|
+
normalizePortalApiOrigin,
|
|
308
|
+
encodePortalReturnTarget,
|
|
309
|
+
createPortalClient
|
|
310
|
+
};
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BookingBlockClient,
|
|
3
|
-
CartClient,
|
|
4
3
|
CertificateFunnelClient,
|
|
5
|
-
CheckoutClient,
|
|
6
4
|
PortalAccountClient,
|
|
7
|
-
PortalPanelClient
|
|
8
|
-
|
|
9
|
-
} from "./chunk-7ZOQ6UKG.js";
|
|
5
|
+
PortalPanelClient
|
|
6
|
+
} from "./chunk-4GKSSM5N.js";
|
|
10
7
|
import {
|
|
11
8
|
mountIslandsWith
|
|
12
9
|
} from "./chunk-THMT43MV.js";
|
|
10
|
+
import {
|
|
11
|
+
CartClient
|
|
12
|
+
} from "./chunk-2CZFJOKK.js";
|
|
13
|
+
import {
|
|
14
|
+
CartButtonClient
|
|
15
|
+
} from "./chunk-ZNH6TSYP.js";
|
|
16
|
+
import {
|
|
17
|
+
CheckoutClient
|
|
18
|
+
} from "./chunk-S3ZQQOQT.js";
|
|
19
|
+
import {
|
|
20
|
+
StoreClient
|
|
21
|
+
} from "./chunk-673YEXAI.js";
|
|
13
22
|
|
|
14
23
|
// src/islands-registry.tsx
|
|
15
24
|
var ISLANDS = {
|
|
@@ -24,6 +33,12 @@ var ISLANDS = {
|
|
|
24
33
|
"portal-account": PortalAccountClient,
|
|
25
34
|
store: StoreClient,
|
|
26
35
|
cart: CartClient,
|
|
36
|
+
// PAT-944 — the header cart. Admitted because it adds NO new reach: it reads the cart
|
|
37
|
+
// already in this browser (`cart-storage`) and, only once a shopper opens the drawer,
|
|
38
|
+
// posts those same stored lines to the `/api/store/quote` the cart island already
|
|
39
|
+
// calls. It makes no request on mount and renders no count on the server, so a header
|
|
40
|
+
// carrying it stays edge-cacheable.
|
|
41
|
+
"cart-button": CartButtonClient,
|
|
27
42
|
checkout: CheckoutClient
|
|
28
43
|
};
|
|
29
44
|
function mountIslands(root, createRoot) {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatMoney
|
|
3
|
+
} from "./chunk-7BUDUYXN.js";
|
|
4
|
+
import {
|
|
5
|
+
adjustCartQuantity,
|
|
6
|
+
cartToWireLines,
|
|
7
|
+
removeFromCart
|
|
8
|
+
} from "./chunk-4NF7SSIX.js";
|
|
9
|
+
|
|
10
|
+
// src/cart-lines.tsx
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
function quoteSourceQuantities(quoteSourceCart) {
|
|
14
|
+
return new Map(
|
|
15
|
+
quoteSourceCart?.lines.filter((line) => line.kind === "retail").map((line) => [line.variantId, line.quantity]) ?? []
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
function stepperBaseline(sourceQuantities, line) {
|
|
19
|
+
return (sourceQuantities.get(line.variantId) ?? 0) > line.quantity ? line.quantity : void 0;
|
|
20
|
+
}
|
|
21
|
+
function CartProblemList({
|
|
22
|
+
problems
|
|
23
|
+
}) {
|
|
24
|
+
if (problems.length === 0) return null;
|
|
25
|
+
return /* @__PURE__ */ jsx("ul", { className: "sk-cart__problems", role: "status", children: problems.map((p, i) => /* @__PURE__ */ jsx("li", { children: p.message }, `${p.code}-${p.variantId ?? i}`)) });
|
|
26
|
+
}
|
|
27
|
+
function UnsupportedCartLineList({
|
|
28
|
+
lines,
|
|
29
|
+
onMutate
|
|
30
|
+
}) {
|
|
31
|
+
if (lines.length === 0) return null;
|
|
32
|
+
return /* @__PURE__ */ jsx("ul", { className: "sk-cart__lines", children: lines.map((line) => /* @__PURE__ */ jsxs("li", { className: "sk-cart__line", children: [
|
|
33
|
+
/* @__PURE__ */ jsx("span", { className: "sk-cart__line-title", children: line.kind === "fill" ? "Prescription item" : "Unavailable item" }),
|
|
34
|
+
/* @__PURE__ */ jsx("span", { className: "sk-cart__line-unit", children: "Not supported by retail checkout" }),
|
|
35
|
+
/* @__PURE__ */ jsxs("span", { className: "sk-cart__qty", children: [
|
|
36
|
+
"Quantity ",
|
|
37
|
+
line.quantity
|
|
38
|
+
] }),
|
|
39
|
+
/* @__PURE__ */ jsx(
|
|
40
|
+
"button",
|
|
41
|
+
{
|
|
42
|
+
type: "button",
|
|
43
|
+
className: "sk-cart__remove",
|
|
44
|
+
onClick: () => void onMutate(removeFromCart(line.variantId, line.kind)),
|
|
45
|
+
children: "Remove"
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
] }, `${line.kind}-${line.variantId}`)) });
|
|
49
|
+
}
|
|
50
|
+
function CartLineList({
|
|
51
|
+
lines,
|
|
52
|
+
sourceQuantities,
|
|
53
|
+
onMutate
|
|
54
|
+
}) {
|
|
55
|
+
return /* @__PURE__ */ jsx("ul", { className: "sk-cart__lines", children: lines.map((line) => /* @__PURE__ */ jsxs("li", { className: "sk-cart__line", children: [
|
|
56
|
+
/* @__PURE__ */ jsx("a", { className: "sk-cart__line-title", href: `/store/${line.productHandle}`, children: line.title }),
|
|
57
|
+
/* @__PURE__ */ jsxs("span", { className: "sk-cart__line-unit", children: [
|
|
58
|
+
formatMoney(line.unitPrice),
|
|
59
|
+
" each"
|
|
60
|
+
] }),
|
|
61
|
+
/* @__PURE__ */ jsxs("span", { className: "sk-cart__qty", children: [
|
|
62
|
+
/* @__PURE__ */ jsx(
|
|
63
|
+
"button",
|
|
64
|
+
{
|
|
65
|
+
type: "button",
|
|
66
|
+
"aria-label": `Decrease quantity of ${line.title}`,
|
|
67
|
+
onClick: () => void onMutate(
|
|
68
|
+
adjustCartQuantity(
|
|
69
|
+
"retail",
|
|
70
|
+
line.variantId,
|
|
71
|
+
-1,
|
|
72
|
+
stepperBaseline(sourceQuantities, line)
|
|
73
|
+
)
|
|
74
|
+
),
|
|
75
|
+
children: "\u2212"
|
|
76
|
+
}
|
|
77
|
+
),
|
|
78
|
+
/* @__PURE__ */ jsx("span", { "aria-live": "polite", children: line.quantity }),
|
|
79
|
+
/* @__PURE__ */ jsx(
|
|
80
|
+
"button",
|
|
81
|
+
{
|
|
82
|
+
type: "button",
|
|
83
|
+
"aria-label": `Increase quantity of ${line.title}`,
|
|
84
|
+
onClick: () => void onMutate(
|
|
85
|
+
adjustCartQuantity(
|
|
86
|
+
"retail",
|
|
87
|
+
line.variantId,
|
|
88
|
+
1,
|
|
89
|
+
stepperBaseline(sourceQuantities, line)
|
|
90
|
+
)
|
|
91
|
+
),
|
|
92
|
+
children: "+"
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
] }),
|
|
96
|
+
/* @__PURE__ */ jsx("span", { className: "sk-cart__line-total", children: formatMoney(line.lineTotal) }),
|
|
97
|
+
/* @__PURE__ */ jsx(
|
|
98
|
+
"button",
|
|
99
|
+
{
|
|
100
|
+
type: "button",
|
|
101
|
+
className: "sk-cart__remove",
|
|
102
|
+
onClick: () => void onMutate(removeFromCart(line.variantId, "retail")),
|
|
103
|
+
children: "Remove"
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
] }, line.variantId)) });
|
|
107
|
+
}
|
|
108
|
+
function CartTotals({ quote }) {
|
|
109
|
+
return /* @__PURE__ */ jsxs("dl", { className: "sk-cart__totals", children: [
|
|
110
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
111
|
+
/* @__PURE__ */ jsx("dt", { children: "Subtotal" }),
|
|
112
|
+
/* @__PURE__ */ jsx("dd", { children: formatMoney(quote.subtotal) })
|
|
113
|
+
] }),
|
|
114
|
+
/* @__PURE__ */ jsxs("div", { className: "sk-cart__gst-row", children: [
|
|
115
|
+
/* @__PURE__ */ jsx("dt", { children: "GST included" }),
|
|
116
|
+
/* @__PURE__ */ jsx("dd", { children: formatMoney(quote.taxTotal) })
|
|
117
|
+
] }),
|
|
118
|
+
/* @__PURE__ */ jsxs("div", { className: "sk-cart__total-row", children: [
|
|
119
|
+
/* @__PURE__ */ jsx("dt", { children: "Total" }),
|
|
120
|
+
/* @__PURE__ */ jsx("dd", { children: formatMoney(quote.total) })
|
|
121
|
+
] })
|
|
122
|
+
] });
|
|
123
|
+
}
|
|
124
|
+
function useCartQuote(client, cart, enabled = true) {
|
|
125
|
+
const [state, setState] = React.useState({
|
|
126
|
+
quote: null,
|
|
127
|
+
quoteSourceCart: null,
|
|
128
|
+
failed: false
|
|
129
|
+
});
|
|
130
|
+
React.useEffect(() => {
|
|
131
|
+
if (!enabled || cart.lines.length === 0) {
|
|
132
|
+
setState({ quote: null, quoteSourceCart: null, failed: false });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
let alive = true;
|
|
136
|
+
setState({ quote: null, quoteSourceCart: null, failed: false });
|
|
137
|
+
void client.post("/api/store/quote", { lines: cartToWireLines(cart) }).then((result) => {
|
|
138
|
+
if (!alive) return;
|
|
139
|
+
if (!result.ok) {
|
|
140
|
+
setState({ quote: null, quoteSourceCart: null, failed: true });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
setState({ quote: result.data, quoteSourceCart: cart, failed: false });
|
|
144
|
+
});
|
|
145
|
+
return () => {
|
|
146
|
+
alive = false;
|
|
147
|
+
};
|
|
148
|
+
}, [cart, client, enabled]);
|
|
149
|
+
return state;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export {
|
|
153
|
+
quoteSourceQuantities,
|
|
154
|
+
CartProblemList,
|
|
155
|
+
UnsupportedCartLineList,
|
|
156
|
+
CartLineList,
|
|
157
|
+
CartTotals,
|
|
158
|
+
useCartQuote
|
|
159
|
+
};
|