@patientos/website-kit 0.2.5 → 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.
@@ -27,6 +27,7 @@ import {
27
27
  createPortalClient,
28
28
  describedBy,
29
29
  effectiveFee,
30
+ encodePortalReturnTarget,
30
31
  errorCode,
31
32
  errorMessage,
32
33
  feeNote,
@@ -48,6 +49,7 @@ import {
48
49
  isSessionExpired,
49
50
  isSlotTakenError,
50
51
  loadFlowRecord,
52
+ normalizePortalApiOrigin,
51
53
  observeSessionExpiry,
52
54
  once,
53
55
  patientos,
@@ -65,7 +67,7 @@ import {
65
67
  signOutOfPortal,
66
68
  writePortalSignedInHint,
67
69
  zoneAbbr
68
- } from "./chunk-FSZ726DM.js";
70
+ } from "./chunk-ZYX4TXBN.js";
69
71
  import {
70
72
  __export
71
73
  } from "./chunk-MLKGABMK.js";
@@ -12067,6 +12069,25 @@ var EMPTY_CART = Object.freeze({
12067
12069
  v: 1,
12068
12070
  lines: Object.freeze([])
12069
12071
  });
12072
+ function cartLineId(value) {
12073
+ if (typeof value !== "string") return null;
12074
+ const normalized = value.trim();
12075
+ return normalized.length > 0 && normalized.length <= 128 ? normalized : null;
12076
+ }
12077
+ function legacyCartLineId(kind, variantId) {
12078
+ const input = `${kind}\0${variantId}`;
12079
+ let first = 2166136261;
12080
+ let second = 2246822507;
12081
+ for (let index = 0; index < input.length; index += 1) {
12082
+ const code2 = input.charCodeAt(index);
12083
+ first = Math.imul(first ^ code2, 16777619);
12084
+ second = Math.imul(second ^ code2, 3266489909);
12085
+ }
12086
+ return `legacy-${(first >>> 0).toString(16).padStart(8, "0")}${(second >>> 0).toString(16).padStart(8, "0")}`;
12087
+ }
12088
+ function newCartLineId() {
12089
+ return crypto.randomUUID();
12090
+ }
12070
12091
  function storage() {
12071
12092
  try {
12072
12093
  if (typeof window === "undefined" || !window.localStorage) return null;
@@ -12084,7 +12105,39 @@ function parseLine(raw) {
12084
12105
  const n = typeof line.quantity === "number" ? line.quantity : Number(line.quantity);
12085
12106
  if (!Number.isFinite(n)) return null;
12086
12107
  const quantity = Math.min(Math.max(Math.floor(n), 1), MAX_QUANTITY);
12087
- return { kind, variantId, quantity };
12108
+ const lineId = cartLineId(line.lineId) ?? legacyCartLineId(kind, variantId);
12109
+ return { lineId, kind, variantId, quantity };
12110
+ }
12111
+ function normalizeCartLines(entries) {
12112
+ const lines = [];
12113
+ const byIdentity = /* @__PURE__ */ new Map();
12114
+ for (const entry of entries) {
12115
+ const line = parseLine(entry);
12116
+ if (!line) continue;
12117
+ const identity = `${line.kind}\0${line.variantId}`;
12118
+ const existing = byIdentity.get(identity);
12119
+ if (existing) {
12120
+ existing.quantity = Math.min(
12121
+ existing.quantity + line.quantity,
12122
+ MAX_QUANTITY
12123
+ );
12124
+ continue;
12125
+ }
12126
+ if (lines.length >= MAX_LINES) continue;
12127
+ lines.push(line);
12128
+ byIdentity.set(identity, line);
12129
+ }
12130
+ return lines;
12131
+ }
12132
+ function normalizeSettledAttemptTokens(entries) {
12133
+ if (!Array.isArray(entries)) return [];
12134
+ return Array.from(
12135
+ new Set(
12136
+ entries.filter(
12137
+ (entry) => typeof entry === "string" && entry.length > 0 && entry.length <= 128
12138
+ )
12139
+ )
12140
+ ).slice(-32);
12088
12141
  }
12089
12142
  function readCart() {
12090
12143
  const store = storage();
@@ -12098,75 +12151,216 @@ function readCart() {
12098
12151
  if (!raw) return emptyCart();
12099
12152
  try {
12100
12153
  const parsed = JSON.parse(raw);
12101
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return emptyCart();
12154
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
12155
+ return emptyCart();
12102
12156
  const bag = parsed;
12103
12157
  if (bag.v !== 1 || !Array.isArray(bag.lines)) return emptyCart();
12104
- const lines = [];
12105
- for (const entry of bag.lines.slice(0, MAX_LINES)) {
12106
- const line = parseLine(entry);
12107
- if (line) lines.push(line);
12108
- }
12109
- return { v: 1, lines };
12158
+ const settledAttemptTokens = normalizeSettledAttemptTokens(
12159
+ bag.settledAttemptTokens
12160
+ );
12161
+ return {
12162
+ v: 1,
12163
+ lines: normalizeCartLines(bag.lines),
12164
+ ...settledAttemptTokens.length > 0 ? { settledAttemptTokens } : {}
12165
+ };
12110
12166
  } catch {
12111
12167
  return emptyCart();
12112
12168
  }
12113
12169
  }
12114
- function writeCart(cart) {
12115
- const normalised = { v: 1, lines: cart.lines.slice(0, MAX_LINES) };
12170
+ var CART_MUTATION_LOCK = "patientos-cart-mutation-v1";
12171
+ var CART_MUTATION_VALIDATION_TIMEOUT_MS = 5e3;
12172
+ function persistCart(cart) {
12173
+ const settledAttemptTokens = normalizeSettledAttemptTokens(
12174
+ cart.settledAttemptTokens
12175
+ );
12176
+ const normalised = {
12177
+ v: 1,
12178
+ lines: normalizeCartLines(cart.lines),
12179
+ ...settledAttemptTokens.length > 0 ? { settledAttemptTokens } : {}
12180
+ };
12116
12181
  const store = storage();
12117
- if (store) {
12118
- try {
12119
- store.setItem(CART_STORAGE_KEY, JSON.stringify(normalised));
12120
- } catch {
12121
- }
12182
+ if (!store)
12183
+ return {
12184
+ cart: readCart(),
12185
+ persisted: false,
12186
+ status: "cart_storage_unavailable"
12187
+ };
12188
+ try {
12189
+ store.setItem(CART_STORAGE_KEY, JSON.stringify(normalised));
12190
+ } catch {
12191
+ return {
12192
+ cart: readCart(),
12193
+ persisted: false,
12194
+ status: "cart_storage_unavailable"
12195
+ };
12122
12196
  }
12123
12197
  notify(normalised);
12124
- return normalised;
12125
- }
12126
- function addToCart(variantId, quantity = 1) {
12127
- const cart = readCart();
12128
- const add2 = Math.min(Math.max(Math.floor(quantity), 1), MAX_QUANTITY);
12129
- const existing = cart.lines.find((l) => l.kind === "retail" && l.variantId === variantId);
12130
- if (existing) {
12131
- existing.quantity = Math.min(existing.quantity + add2, MAX_QUANTITY);
12132
- } else {
12133
- if (cart.lines.length >= MAX_LINES) return cart;
12134
- cart.lines.push({ kind: "retail", variantId, quantity: add2 });
12198
+ return { cart: normalised, persisted: true, status: "updated" };
12199
+ }
12200
+ function unavailableMutation() {
12201
+ return {
12202
+ cart: readCart(),
12203
+ persisted: false,
12204
+ status: "cart_storage_unavailable"
12205
+ };
12206
+ }
12207
+ async function mutateCart(mutation, beforePersist) {
12208
+ const locks = typeof navigator === "undefined" ? void 0 : navigator.locks;
12209
+ if (!storage() || !locks) return unavailableMutation();
12210
+ try {
12211
+ return await locks.request(CART_MUTATION_LOCK, async () => {
12212
+ const latest = readCart();
12213
+ const mutated = mutation(latest);
12214
+ const proposed = {
12215
+ ...mutated,
12216
+ lines: normalizeCartLines(mutated.lines),
12217
+ ...mutated.settledAttemptTokens === void 0 && latest.settledAttemptTokens ? { settledAttemptTokens: latest.settledAttemptTokens } : {}
12218
+ };
12219
+ if (beforePersist) {
12220
+ const controller = new AbortController();
12221
+ let timer;
12222
+ const accepted = await Promise.race([
12223
+ beforePersist(proposed, controller.signal).catch(() => false),
12224
+ new Promise((resolve) => {
12225
+ timer = setTimeout(() => {
12226
+ controller.abort();
12227
+ resolve(false);
12228
+ }, CART_MUTATION_VALIDATION_TIMEOUT_MS);
12229
+ })
12230
+ ]);
12231
+ if (timer) clearTimeout(timer);
12232
+ if (!accepted) {
12233
+ return {
12234
+ cart: readCart(),
12235
+ persisted: false,
12236
+ status: "not_committed"
12237
+ };
12238
+ }
12239
+ }
12240
+ return persistCart(proposed);
12241
+ });
12242
+ } catch {
12243
+ return unavailableMutation();
12135
12244
  }
12136
- return writeCart(cart);
12137
12245
  }
12138
- function setCartQuantity(variantId, quantity) {
12139
- const cart = readCart();
12140
- const next = Math.floor(quantity);
12141
- if (next < 1) return removeFromCart(variantId);
12142
- return writeCart({
12143
- v: 1,
12144
- lines: cart.lines.map(
12145
- (l) => l.variantId === variantId ? { ...l, quantity: Math.min(next, MAX_QUANTITY) } : l
12146
- )
12246
+ async function withCartLock(operation) {
12247
+ const locks = typeof navigator === "undefined" ? void 0 : navigator.locks;
12248
+ if (!storage() || !locks)
12249
+ return { status: "cart_storage_unavailable", cart: readCart() };
12250
+ try {
12251
+ return await locks.request(CART_MUTATION_LOCK, async () => {
12252
+ const latest = readCart();
12253
+ const controller = new AbortController();
12254
+ let timer;
12255
+ const outcome = await Promise.race([
12256
+ operation(latest, controller.signal).then((result) => ({
12257
+ completed: true,
12258
+ result
12259
+ })),
12260
+ new Promise((resolve) => {
12261
+ timer = setTimeout(() => {
12262
+ controller.abort();
12263
+ resolve({ completed: false });
12264
+ }, CART_MUTATION_VALIDATION_TIMEOUT_MS);
12265
+ })
12266
+ ]);
12267
+ if (timer) clearTimeout(timer);
12268
+ return outcome.completed ? { status: "completed", cart: latest, result: outcome.result } : { status: "not_completed", cart: latest };
12269
+ });
12270
+ } catch {
12271
+ return { status: "not_completed", cart: readCart() };
12272
+ }
12273
+ }
12274
+ async function addToCart(variantId, quantity = 1) {
12275
+ let status = "added";
12276
+ const result = await mutateCart((cart) => {
12277
+ const add2 = Math.min(Math.max(Math.floor(quantity), 1), MAX_QUANTITY);
12278
+ const existing = cart.lines.find(
12279
+ (line) => line.kind === "retail" && line.variantId === variantId
12280
+ );
12281
+ if (existing) {
12282
+ if (existing.quantity >= MAX_QUANTITY) {
12283
+ status = "quantity_limit";
12284
+ return cart;
12285
+ }
12286
+ existing.quantity = Math.min(existing.quantity + add2, MAX_QUANTITY);
12287
+ } else {
12288
+ if (cart.lines.length >= MAX_LINES) {
12289
+ status = "line_limit";
12290
+ return cart;
12291
+ }
12292
+ cart.lines.push({
12293
+ lineId: newCartLineId(),
12294
+ kind: "retail",
12295
+ variantId,
12296
+ quantity: add2
12297
+ });
12298
+ }
12299
+ return cart;
12147
12300
  });
12301
+ return {
12302
+ cart: result.cart,
12303
+ status: result.status === "cart_storage_unavailable" ? result.status : status
12304
+ };
12148
12305
  }
12149
- function removeFromCart(variantId) {
12150
- const cart = readCart();
12151
- return writeCart({ v: 1, lines: cart.lines.filter((l) => l.variantId !== variantId) });
12306
+ async function adjustCartQuantity(kind, variantId, delta, maximumBaseline) {
12307
+ const result = await mutateCart((cart) => ({
12308
+ v: 1,
12309
+ lines: cart.lines.flatMap((line) => {
12310
+ if (line.kind !== kind || line.variantId !== variantId) return [line];
12311
+ const baseline = maximumBaseline === void 0 ? line.quantity : Math.min(line.quantity, Math.max(0, Math.floor(maximumBaseline)));
12312
+ const quantity = Math.min(baseline + Math.floor(delta), MAX_QUANTITY);
12313
+ return quantity > 0 ? [{ ...line, quantity }] : [];
12314
+ })
12315
+ }));
12316
+ return result;
12152
12317
  }
12153
- function clearCart() {
12154
- const store = storage();
12155
- if (store) {
12156
- try {
12157
- store.removeItem(CART_STORAGE_KEY);
12158
- } catch {
12159
- }
12160
- }
12161
- const empty = emptyCart();
12162
- notify(empty);
12163
- return empty;
12318
+ async function removeFromCart(variantId, kind) {
12319
+ const result = await mutateCart((cart) => ({
12320
+ v: 1,
12321
+ lines: cart.lines.filter(
12322
+ (line) => line.variantId !== variantId || kind && line.kind !== kind
12323
+ )
12324
+ }));
12325
+ return result;
12326
+ }
12327
+ async function subtractPurchasedCartLines(purchasedLines, attemptToken) {
12328
+ const purchased = new Map(
12329
+ normalizeCartLines(
12330
+ purchasedLines.filter(
12331
+ (line) => Boolean(line) && typeof line === "object" && !Array.isArray(line) && cartLineId(line.lineId) !== null
12332
+ )
12333
+ ).map((line) => [line.lineId, line])
12334
+ );
12335
+ return mutateCart((cart) => {
12336
+ if (attemptToken && cart.settledAttemptTokens?.includes(attemptToken))
12337
+ return cart;
12338
+ return {
12339
+ v: 1,
12340
+ lines: cart.lines.flatMap((line) => {
12341
+ const purchasedLine = purchased.get(line.lineId);
12342
+ const purchasedQuantity = purchasedLine?.kind === line.kind && purchasedLine.variantId === line.variantId ? purchasedLine.quantity : 0;
12343
+ const quantity = line.quantity - purchasedQuantity;
12344
+ return quantity > 0 ? [{ ...line, quantity }] : [];
12345
+ }),
12346
+ ...attemptToken ? {
12347
+ settledAttemptTokens: [
12348
+ ...cart.settledAttemptTokens ?? [],
12349
+ attemptToken
12350
+ ]
12351
+ } : {}
12352
+ };
12353
+ });
12164
12354
  }
12165
12355
  function cartItemCount(cart = readCart()) {
12166
12356
  return cart.lines.reduce((sum, l) => sum + l.quantity, 0);
12167
12357
  }
12168
12358
  function cartToWireLines(cart) {
12169
- return cart.lines.map((l) => ({ kind: l.kind, variantId: l.variantId, quantity: l.quantity }));
12359
+ return cart.lines.map((l) => ({
12360
+ kind: l.kind,
12361
+ variantId: l.variantId,
12362
+ quantity: l.quantity
12363
+ }));
12170
12364
  }
12171
12365
  var listeners = /* @__PURE__ */ new Set();
12172
12366
  function notify(cart) {
@@ -12183,10 +12377,76 @@ function subscribeToCart(fn) {
12183
12377
  if (e.key !== null && e.key !== CART_STORAGE_KEY) return;
12184
12378
  fn(readCart());
12185
12379
  };
12186
- if (typeof window !== "undefined") window.addEventListener("storage", onStorage);
12380
+ if (typeof window !== "undefined")
12381
+ window.addEventListener("storage", onStorage);
12187
12382
  return () => {
12188
12383
  listeners.delete(fn);
12189
- if (typeof window !== "undefined") window.removeEventListener("storage", onStorage);
12384
+ if (typeof window !== "undefined")
12385
+ window.removeEventListener("storage", onStorage);
12386
+ };
12387
+ }
12388
+
12389
+ // src/store-client.ts
12390
+ var STORE_REQUEST_INIT = {
12391
+ credentials: "include",
12392
+ cache: "no-store"
12393
+ };
12394
+ async function readResult(response) {
12395
+ let body;
12396
+ try {
12397
+ body = await response.json();
12398
+ } catch {
12399
+ return response.ok ? { ok: false, status: null, error: null, message: null } : { ok: false, status: response.status, error: null, message: null };
12400
+ }
12401
+ if (response.ok) return { ok: true, data: body };
12402
+ const failure = body;
12403
+ return {
12404
+ ok: false,
12405
+ status: response.status,
12406
+ error: typeof failure?.error === "string" ? failure.error : null,
12407
+ message: typeof failure?.message === "string" ? failure.message : null
12408
+ };
12409
+ }
12410
+ function createStoreClient(config2 = {}) {
12411
+ const rawOrigin = config2.apiOrigin?.trim() ?? "";
12412
+ const normalizedOrigin = rawOrigin ? normalizePortalApiOrigin(rawOrigin) : "";
12413
+ if (rawOrigin && !normalizedOrigin) {
12414
+ console.error("[store] ignoring a malformed store API origin", rawOrigin);
12415
+ }
12416
+ const apiOrigin = normalizedOrigin ?? "";
12417
+ const fetchImpl = config2.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
12418
+ const url = (path) => apiOrigin && path.startsWith("/") ? apiOrigin + path : path;
12419
+ async function request(path, init) {
12420
+ try {
12421
+ return await readResult(
12422
+ await fetchImpl(url(path), {
12423
+ ...STORE_REQUEST_INIT,
12424
+ ...init
12425
+ })
12426
+ );
12427
+ } catch {
12428
+ return { ok: false, status: null, error: null, message: null };
12429
+ }
12430
+ }
12431
+ return {
12432
+ apiOrigin,
12433
+ url,
12434
+ get: (path) => request(path, { headers: { accept: "application/json" } }),
12435
+ post: (path, body, options) => request(path, {
12436
+ signal: options?.signal,
12437
+ method: "POST",
12438
+ headers: {
12439
+ accept: "application/json",
12440
+ "content-type": "application/json"
12441
+ },
12442
+ body: JSON.stringify(body ?? {})
12443
+ }),
12444
+ signInHref(returnPath = "/checkout") {
12445
+ if (typeof window === "undefined") return `${apiOrigin}/portal/sign-in`;
12446
+ const target = new URL(returnPath, window.location.origin).toString();
12447
+ const redirect = `/portal/return?to=${encodePortalReturnTarget(target)}`;
12448
+ return `${apiOrigin}/portal/sign-in?redirect=${encodeURIComponent(redirect)}`;
12449
+ }
12190
12450
  };
12191
12451
  }
12192
12452
 
@@ -12197,7 +12457,17 @@ function formatMoney(value) {
12197
12457
  if (!Number.isFinite(n)) return value;
12198
12458
  return `$${n.toFixed(2)}`;
12199
12459
  }
12200
- function StoreClient({ categoryHandle, title, columns }) {
12460
+ function StoreClient({
12461
+ categoryHandle,
12462
+ productHandle,
12463
+ storeApiOrigin,
12464
+ title,
12465
+ columns
12466
+ }) {
12467
+ const client = React22.useMemo(
12468
+ () => createStoreClient({ apiOrigin: storeApiOrigin }),
12469
+ [storeApiOrigin]
12470
+ );
12201
12471
  const [catalog, setCatalog] = React22.useState(null);
12202
12472
  const [failed, setFailed] = React22.useState(false);
12203
12473
  const [count, setCount] = React22.useState(0);
@@ -12207,22 +12477,32 @@ function StoreClient({ categoryHandle, title, columns }) {
12207
12477
  }, []);
12208
12478
  React22.useEffect(() => {
12209
12479
  let alive = true;
12210
- fetch("/api/store/catalog", { headers: { accept: "application/json" } }).then(
12211
- (res) => res.ok ? res.json() : Promise.reject(new Error(String(res.status)))
12212
- ).then((data) => {
12213
- if (alive) setCatalog(data);
12214
- }).catch(() => {
12215
- if (alive) setFailed(true);
12480
+ const path = productHandle ? `/api/store/catalog/${encodeURIComponent(productHandle)}` : "/api/store/catalog";
12481
+ void client.get(path).then((result) => {
12482
+ if (!alive) return;
12483
+ if (!result.ok) {
12484
+ setFailed(true);
12485
+ return;
12486
+ }
12487
+ setCatalog(
12488
+ "product" in result.data ? {
12489
+ products: [result.data.product],
12490
+ currency: result.data.currency
12491
+ } : result.data
12492
+ );
12493
+ setFailed(false);
12216
12494
  });
12217
12495
  return () => {
12218
12496
  alive = false;
12219
12497
  };
12220
- }, []);
12498
+ }, [client, productHandle]);
12221
12499
  const products = React22.useMemo(() => {
12222
12500
  if (!catalog) return [];
12223
- if (!categoryHandle) return catalog.products;
12224
- return catalog.products.filter((p) => p.handle.startsWith(categoryHandle));
12225
- }, [catalog, categoryHandle]);
12501
+ if (productHandle || !categoryHandle) return catalog.products;
12502
+ return catalog.products.filter(
12503
+ (product) => product.categories.some((category) => category.handle === categoryHandle)
12504
+ );
12505
+ }, [catalog, categoryHandle, productHandle]);
12226
12506
  if (failed) {
12227
12507
  return /* @__PURE__ */ jsxs25(Fragment12, { children: [
12228
12508
  /* @__PURE__ */ jsx28("h2", { className: "sk-store__heading", children: title ?? "Shop" }),
@@ -12251,17 +12531,30 @@ function StoreCard({ product }) {
12251
12531
  const [variantId, setVariantId] = React22.useState(
12252
12532
  () => (product.variants.find((v) => v.inStock) ?? product.variants[0]).id
12253
12533
  );
12254
- const [added, setAdded] = React22.useState(false);
12534
+ const [addFeedback, setAddFeedback] = React22.useState(null);
12255
12535
  const variant = product.variants.find((v) => v.id === variantId) ?? product.variants[0];
12256
12536
  const image2 = product.thumbnailUrl ?? product.images[0]?.url ?? null;
12257
- function onAdd() {
12258
- addToCart(variant.id, 1);
12259
- setAdded(true);
12260
- window.setTimeout(() => setAdded(false), 1600);
12537
+ async function onAdd() {
12538
+ const result = await addToCart(variant.id, 1);
12539
+ setAddFeedback(result.status);
12540
+ window.setTimeout(() => setAddFeedback(null), 1600);
12261
12541
  }
12262
12542
  return /* @__PURE__ */ jsxs25("li", { className: "sk-store__card", children: [
12263
12543
  /* @__PURE__ */ jsxs25("a", { className: "sk-store__card-link", href: `/store/${product.handle}`, children: [
12264
- image2 ? /* @__PURE__ */ jsx28("img", { className: "sk-store__image", src: image2, alt: product.images[0]?.alt ?? "" }) : /* @__PURE__ */ jsx28("span", { className: "sk-store__image sk-store__image--empty", "aria-hidden": true }),
12544
+ image2 ? /* @__PURE__ */ jsx28(
12545
+ "img",
12546
+ {
12547
+ className: "sk-store__image",
12548
+ src: image2,
12549
+ alt: product.images[0]?.alt ?? ""
12550
+ }
12551
+ ) : /* @__PURE__ */ jsx28(
12552
+ "span",
12553
+ {
12554
+ className: "sk-store__image sk-store__image--empty",
12555
+ "aria-hidden": true
12556
+ }
12557
+ ),
12265
12558
  /* @__PURE__ */ jsx28("span", { className: "sk-store__title", children: product.title })
12266
12559
  ] }),
12267
12560
  /* @__PURE__ */ jsxs25("span", { className: "sk-store__price", children: [
@@ -12270,16 +12563,23 @@ function StoreCard({ product }) {
12270
12563
  ] }),
12271
12564
  product.variants.length > 1 ? /* @__PURE__ */ jsxs25("label", { className: "sk-store__variant", children: [
12272
12565
  /* @__PURE__ */ jsx28("span", { className: "sk-store__variant-label", children: "Option" }),
12273
- /* @__PURE__ */ jsx28("select", { value: variantId, onChange: (e) => setVariantId(e.target.value), children: product.variants.map((v) => /* @__PURE__ */ jsx28("option", { value: v.id, disabled: !v.inStock, children: (v.options.length ? v.options.join(" / ") : v.title) + (v.inStock ? "" : " \u2014 sold out") }, v.id)) })
12566
+ /* @__PURE__ */ jsx28(
12567
+ "select",
12568
+ {
12569
+ value: variantId,
12570
+ onChange: (e) => setVariantId(e.target.value),
12571
+ children: product.variants.map((v) => /* @__PURE__ */ jsx28("option", { value: v.id, disabled: !v.inStock, children: (v.options.length ? v.options.join(" / ") : v.title) + (v.inStock ? "" : " \u2014 sold out") }, v.id))
12572
+ }
12573
+ )
12274
12574
  ] }) : null,
12275
12575
  /* @__PURE__ */ jsx28(
12276
12576
  "button",
12277
12577
  {
12278
12578
  type: "button",
12279
12579
  className: "sk-store__add",
12280
- onClick: onAdd,
12580
+ onClick: () => void onAdd(),
12281
12581
  disabled: !variant.inStock,
12282
- children: !variant.inStock ? "Sold out" : added ? "Added \u2713" : "Add to cart"
12582
+ children: !variant.inStock ? "Sold out" : addFeedback === "added" ? "Added \u2713" : addFeedback === "line_limit" ? "Cart full \u2014 50 products max" : addFeedback === "quantity_limit" ? "Maximum 99 in cart" : addFeedback === "cart_storage_unavailable" ? "This browser can\u2019t safely update the cart" : "Add to cart"
12283
12583
  }
12284
12584
  )
12285
12585
  ] });
@@ -12288,35 +12588,48 @@ function StoreCard({ product }) {
12288
12588
  // src/cart-block.client.tsx
12289
12589
  import * as React23 from "react";
12290
12590
  import { Fragment as Fragment13, jsx as jsx29, jsxs as jsxs26 } from "react/jsx-runtime";
12291
- function CartClient(_props) {
12591
+ function CartClient({ storeApiOrigin }) {
12592
+ const client = React23.useMemo(
12593
+ () => createStoreClient({ apiOrigin: storeApiOrigin }),
12594
+ [storeApiOrigin]
12595
+ );
12292
12596
  const [cart, setCart] = React23.useState(() => readCart());
12293
12597
  const [quote, setQuote] = React23.useState(null);
12598
+ const [quoteSourceCart, setQuoteSourceCart] = React23.useState(
12599
+ null
12600
+ );
12294
12601
  const [failed, setFailed] = React23.useState(false);
12602
+ const [mutationUnavailable, setMutationUnavailable] = React23.useState(false);
12603
+ async function runMutation(mutation) {
12604
+ const result = await mutation;
12605
+ setMutationUnavailable(result.status === "cart_storage_unavailable");
12606
+ }
12295
12607
  React23.useEffect(() => subscribeToCart(setCart), []);
12296
12608
  React23.useEffect(() => {
12297
12609
  if (cart.lines.length === 0) {
12298
12610
  setQuote(null);
12611
+ setQuoteSourceCart(null);
12299
12612
  setFailed(false);
12300
12613
  return;
12301
12614
  }
12302
12615
  let alive = true;
12303
- fetch("/api/store/quote", {
12304
- method: "POST",
12305
- headers: { "content-type": "application/json" },
12306
- body: JSON.stringify({ lines: cartToWireLines(cart) })
12307
- }).then(
12308
- (res) => res.ok ? res.json() : Promise.reject(new Error(String(res.status)))
12309
- ).then((data) => {
12616
+ setQuote(null);
12617
+ setQuoteSourceCart(null);
12618
+ setFailed(false);
12619
+ void client.post("/api/store/quote", { lines: cartToWireLines(cart) }).then((result) => {
12310
12620
  if (!alive) return;
12311
- setQuote(data);
12621
+ if (!result.ok) {
12622
+ setFailed(true);
12623
+ return;
12624
+ }
12625
+ setQuote(result.data);
12626
+ setQuoteSourceCart(cart);
12312
12627
  setFailed(false);
12313
- }).catch(() => {
12314
- if (alive) setFailed(true);
12315
12628
  });
12316
12629
  return () => {
12317
12630
  alive = false;
12318
12631
  };
12319
- }, [cart]);
12632
+ }, [cart, client]);
12320
12633
  if (cart.lines.length === 0) {
12321
12634
  return /* @__PURE__ */ jsxs26(Fragment13, { children: [
12322
12635
  /* @__PURE__ */ jsx29("h2", { className: "sk-cart__heading", children: "Your cart" }),
@@ -12324,13 +12637,54 @@ function CartClient(_props) {
12324
12637
  /* @__PURE__ */ jsx29("a", { className: "sk-cart__continue", href: "/store", children: "Browse the shop" })
12325
12638
  ] });
12326
12639
  }
12640
+ const quotedVariantIds = new Set(
12641
+ quote?.lines.map((line) => line.variantId) ?? []
12642
+ );
12643
+ const quoteSourceQuantities = new Map(
12644
+ quoteSourceCart?.lines.filter((line) => line.kind === "retail").map((line) => [line.variantId, line.quantity]) ?? []
12645
+ );
12646
+ const unsupportedLines = cart.lines.filter(
12647
+ (line) => line.kind !== "retail" || quote !== null && !quotedVariantIds.has(line.variantId)
12648
+ );
12327
12649
  return /* @__PURE__ */ jsxs26(Fragment13, { children: [
12328
12650
  /* @__PURE__ */ jsx29("h2", { className: "sk-cart__heading", children: "Your cart" }),
12329
12651
  quote && quote.problems.length > 0 ? /* @__PURE__ */ jsx29("ul", { className: "sk-cart__problems", role: "status", children: quote.problems.map((p, i) => /* @__PURE__ */ jsx29("li", { children: p.message }, `${p.code}-${p.variantId ?? i}`)) }) : null,
12330
12652
  failed ? /* @__PURE__ */ jsx29("p", { className: "sk-cart__placeholder", role: "alert", children: "We couldn't price your cart just now. Please try again in a moment." }) : null,
12331
- !quote ? /* @__PURE__ */ jsx29("p", { className: "sk-cart__placeholder", role: "status", children: "Pricing your cart\u2026" }) : /* @__PURE__ */ jsxs26(Fragment13, { children: [
12653
+ mutationUnavailable ? /* @__PURE__ */ jsx29("p", { className: "sk-cart__placeholder", role: "alert", children: "This browser can\u2019t safely update the cart. Try a supported browser with site storage enabled." }) : null,
12654
+ unsupportedLines.length > 0 ? /* @__PURE__ */ jsx29("ul", { className: "sk-cart__lines", children: unsupportedLines.map((line) => /* @__PURE__ */ jsxs26(
12655
+ "li",
12656
+ {
12657
+ className: "sk-cart__line",
12658
+ children: [
12659
+ /* @__PURE__ */ jsx29("span", { className: "sk-cart__line-title", children: line.kind === "fill" ? "Prescription item" : "Unavailable item" }),
12660
+ /* @__PURE__ */ jsx29("span", { className: "sk-cart__line-unit", children: "Not supported by retail checkout" }),
12661
+ /* @__PURE__ */ jsxs26("span", { className: "sk-cart__qty", children: [
12662
+ "Quantity ",
12663
+ line.quantity
12664
+ ] }),
12665
+ /* @__PURE__ */ jsx29(
12666
+ "button",
12667
+ {
12668
+ type: "button",
12669
+ className: "sk-cart__remove",
12670
+ onClick: () => void runMutation(removeFromCart(line.variantId, line.kind)),
12671
+ children: "Remove"
12672
+ }
12673
+ )
12674
+ ]
12675
+ },
12676
+ `${line.kind}-${line.variantId}`
12677
+ )) }) : null,
12678
+ quote ? /* @__PURE__ */ jsxs26(Fragment13, { children: [
12332
12679
  /* @__PURE__ */ jsx29("ul", { className: "sk-cart__lines", children: quote.lines.map((line) => /* @__PURE__ */ jsxs26("li", { className: "sk-cart__line", children: [
12333
- /* @__PURE__ */ jsx29("a", { className: "sk-cart__line-title", href: `/store/${line.productHandle}`, children: line.title }),
12680
+ /* @__PURE__ */ jsx29(
12681
+ "a",
12682
+ {
12683
+ className: "sk-cart__line-title",
12684
+ href: `/store/${line.productHandle}`,
12685
+ children: line.title
12686
+ }
12687
+ ),
12334
12688
  /* @__PURE__ */ jsxs26("span", { className: "sk-cart__line-unit", children: [
12335
12689
  formatMoney(line.unitPrice),
12336
12690
  " each"
@@ -12341,7 +12695,14 @@ function CartClient(_props) {
12341
12695
  {
12342
12696
  type: "button",
12343
12697
  "aria-label": `Decrease quantity of ${line.title}`,
12344
- onClick: () => setCartQuantity(line.variantId, line.quantity - 1),
12698
+ onClick: () => void runMutation(
12699
+ adjustCartQuantity(
12700
+ "retail",
12701
+ line.variantId,
12702
+ -1,
12703
+ (quoteSourceQuantities.get(line.variantId) ?? 0) > line.quantity ? line.quantity : void 0
12704
+ )
12705
+ ),
12345
12706
  children: "\u2212"
12346
12707
  }
12347
12708
  ),
@@ -12351,7 +12712,14 @@ function CartClient(_props) {
12351
12712
  {
12352
12713
  type: "button",
12353
12714
  "aria-label": `Increase quantity of ${line.title}`,
12354
- onClick: () => setCartQuantity(line.variantId, line.quantity + 1),
12715
+ onClick: () => void runMutation(
12716
+ adjustCartQuantity(
12717
+ "retail",
12718
+ line.variantId,
12719
+ 1,
12720
+ (quoteSourceQuantities.get(line.variantId) ?? 0) > line.quantity ? line.quantity : void 0
12721
+ )
12722
+ ),
12355
12723
  children: "+"
12356
12724
  }
12357
12725
  )
@@ -12362,7 +12730,7 @@ function CartClient(_props) {
12362
12730
  {
12363
12731
  type: "button",
12364
12732
  className: "sk-cart__remove",
12365
- onClick: () => removeFromCart(line.variantId),
12733
+ onClick: () => void runMutation(removeFromCart(line.variantId, "retail")),
12366
12734
  children: "Remove"
12367
12735
  }
12368
12736
  )
@@ -12384,173 +12752,768 @@ function CartClient(_props) {
12384
12752
  quote.requiresShipping ? /* @__PURE__ */ jsx29("p", { className: "sk-cart__note", children: "Delivery is chosen at checkout." }) : null,
12385
12753
  /* @__PURE__ */ jsxs26("div", { className: "sk-cart__actions", children: [
12386
12754
  /* @__PURE__ */ jsx29("a", { className: "sk-cart__continue", href: "/store", children: "Keep shopping" }),
12387
- /* @__PURE__ */ jsx29(
12388
- "a",
12389
- {
12390
- className: "sk-cart__checkout",
12391
- href: "/checkout",
12392
- "aria-disabled": quote.lines.length === 0,
12393
- children: "Checkout"
12394
- }
12395
- )
12755
+ unsupportedLines.length === 0 && quote.lines.length > 0 ? /* @__PURE__ */ jsx29("a", { className: "sk-cart__checkout", href: "/checkout", children: "Checkout" }) : /* @__PURE__ */ jsx29("span", { className: "sk-cart__checkout", "aria-disabled": "true", children: "Remove unsupported items to checkout" })
12396
12756
  ] })
12397
- ] })
12757
+ ] }) : /* @__PURE__ */ jsx29("p", { className: "sk-cart__placeholder", role: "status", children: "Pricing your cart\u2026" })
12398
12758
  ] });
12399
12759
  }
12400
12760
 
12401
12761
  // src/checkout-block.client.tsx
12402
12762
  import * as React24 from "react";
12763
+
12764
+ // src/checkout-attempt-token.ts
12765
+ var CHECKOUT_ATTEMPT_TOKEN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
12766
+ function isCheckoutAttemptToken(value) {
12767
+ return typeof value === "string" && CHECKOUT_ATTEMPT_TOKEN.test(value);
12768
+ }
12769
+
12770
+ // src/checkout-completion.ts
12771
+ function checkoutCompletionUrl(configuredHref, orderId, siteOrigin) {
12772
+ const fallback = new URL("/checkout/complete", siteOrigin);
12773
+ let destination;
12774
+ try {
12775
+ destination = new URL(configuredHref || fallback.pathname, siteOrigin);
12776
+ } catch {
12777
+ destination = fallback;
12778
+ }
12779
+ if (destination.origin !== fallback.origin || destination.protocol !== "http:" && destination.protocol !== "https:") {
12780
+ destination = fallback;
12781
+ }
12782
+ destination.searchParams.set("order", orderId);
12783
+ return `${destination.pathname}${destination.search}${destination.hash}`;
12784
+ }
12785
+
12786
+ // src/checkout-block.client.tsx
12403
12787
  import { Fragment as Fragment14, jsx as jsx30, jsxs as jsxs27 } from "react/jsx-runtime";
12404
- async function searchStoreAddresses(query) {
12788
+ var CHECKOUT_RECOVERY_PREFIX = "patientos.store.checkout-order.v1";
12789
+ function checkoutRecoveryKey(apiOrigin) {
12790
+ const identity = apiOrigin || (typeof window === "undefined" ? "same-origin" : window.location.origin);
12791
+ let hash = 2166136261;
12792
+ for (let index = 0; index < identity.length; index += 1) {
12793
+ hash ^= identity.charCodeAt(index);
12794
+ hash = Math.imul(hash, 16777619);
12795
+ }
12796
+ return `${CHECKOUT_RECOVERY_PREFIX}:${(hash >>> 0).toString(16)}`;
12797
+ }
12798
+ function discardCheckoutRecovery(key) {
12799
+ try {
12800
+ window.localStorage.removeItem(key);
12801
+ } catch {
12802
+ return;
12803
+ }
12804
+ }
12805
+ function readCheckoutRecovery(key) {
12806
+ try {
12807
+ const value = window.localStorage.getItem(key);
12808
+ if (!value) return null;
12809
+ if (isCheckoutAttemptToken(value))
12810
+ return { v: 2, attemptToken: value, lines: null };
12811
+ const parsed = JSON.parse(value);
12812
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
12813
+ discardCheckoutRecovery(key);
12814
+ return null;
12815
+ }
12816
+ const record = parsed;
12817
+ if (record.v !== 1 && record.v !== 2 || !isCheckoutAttemptToken(record.attemptToken)) {
12818
+ discardCheckoutRecovery(key);
12819
+ return null;
12820
+ }
12821
+ if (record.v !== 2 || !Array.isArray(record.lines)) {
12822
+ return { v: 2, attemptToken: record.attemptToken, lines: null };
12823
+ }
12824
+ const rawLines = record.lines;
12825
+ const lines = normalizeCartLines(rawLines);
12826
+ if (lines.length === 0 || lines.some(
12827
+ (line) => !rawLines.some(
12828
+ (candidate) => candidate !== null && typeof candidate === "object" && !Array.isArray(candidate) && candidate.lineId === line.lineId
12829
+ )
12830
+ )) {
12831
+ return { v: 2, attemptToken: record.attemptToken, lines: null };
12832
+ }
12833
+ return { v: 2, attemptToken: record.attemptToken, lines };
12834
+ } catch {
12835
+ discardCheckoutRecovery(key);
12836
+ return null;
12837
+ }
12838
+ }
12839
+ function sameRecoveryRecord(left, right) {
12840
+ return left?.attemptToken === right.attemptToken && left.lines !== null && right.lines !== null && JSON.stringify(left.lines) === JSON.stringify(right.lines);
12841
+ }
12842
+ function writeCheckoutRecovery(key, record, expectedToken) {
12843
+ try {
12844
+ const existing = readCheckoutRecovery(key);
12845
+ if ((existing?.attemptToken ?? null) !== expectedToken) return false;
12846
+ window.localStorage.setItem(key, JSON.stringify(record));
12847
+ return sameRecoveryRecord(readCheckoutRecovery(key), record);
12848
+ } catch {
12849
+ return false;
12850
+ }
12851
+ }
12852
+ function removeCheckoutRecovery(key, attemptToken) {
12853
+ try {
12854
+ const existing = readCheckoutRecovery(key);
12855
+ if (!existing) return true;
12856
+ if (existing.attemptToken !== attemptToken) return false;
12857
+ window.localStorage.removeItem(key);
12858
+ return readCheckoutRecovery(key)?.attemptToken !== attemptToken;
12859
+ } catch {
12860
+ return false;
12861
+ }
12862
+ }
12863
+ function snapshotPurchasedCartLines(cartLines, quotedLines) {
12864
+ const cartByVariant = new Map(
12865
+ cartLines.filter((line) => line.kind === "retail").map((line) => [line.variantId, line])
12866
+ );
12867
+ const snapshot = [];
12868
+ for (const quoted of quotedLines) {
12869
+ const line = cartByVariant.get(quoted.variantId);
12870
+ if (!line) return null;
12871
+ snapshot.push({ ...line, quantity: quoted.quantity });
12872
+ }
12873
+ return snapshot.length > 0 ? snapshot : null;
12874
+ }
12875
+ function sameCartLineSnapshot(left, right) {
12876
+ if (left.length !== right.length) return false;
12877
+ const rightById = new Map(right.map((line) => [line.lineId, line]));
12878
+ return left.every((line) => {
12879
+ const expected = rightById.get(line.lineId);
12880
+ return expected?.kind === line.kind && expected.variantId === line.variantId && expected.quantity === line.quantity;
12881
+ });
12882
+ }
12883
+ function samePayableQuote(left, right, shippingOptionId) {
12884
+ return JSON.stringify(expectedQuote(left, shippingOptionId)) === JSON.stringify(expectedQuote(right, shippingOptionId));
12885
+ }
12886
+ function expectedQuote(quote, shippingOptionId) {
12887
+ return {
12888
+ shippingOptionId,
12889
+ lines: quote.lines.map(({ variantId, quantity, unitPrice, lineTotal }) => ({
12890
+ variantId,
12891
+ quantity,
12892
+ unitPrice,
12893
+ lineTotal
12894
+ })),
12895
+ subtotal: quote.subtotal,
12896
+ shippingTotal: quote.shippingTotal,
12897
+ taxTotal: quote.taxTotal,
12898
+ total: quote.total,
12899
+ currency: quote.currency
12900
+ };
12901
+ }
12902
+ var StoreRequestError = class extends Error {
12903
+ constructor(message, code2) {
12904
+ super(message);
12905
+ this.code = code2;
12906
+ this.name = "StoreRequestError";
12907
+ }
12908
+ code;
12909
+ };
12910
+ async function searchStoreAddresses(query, client) {
12405
12911
  const q = query.trim();
12406
12912
  if (q.length < 3) return [];
12407
- const res = await fetch(`/api/address/autocomplete?q=${encodeURIComponent(q)}`, {
12408
- credentials: "same-origin",
12409
- headers: { accept: "application/json" }
12410
- });
12411
- if (!res.ok) return [];
12412
- const body = await res.json();
12413
- return body.suggestions ?? [];
12414
- }
12415
- async function validateStoreAddress(address) {
12416
- const res = await fetch("/api/address/validate", {
12417
- method: "POST",
12418
- credentials: "same-origin",
12419
- headers: { "content-type": "application/json" },
12420
- body: JSON.stringify({ address })
12421
- });
12422
- if (!res.ok) return null;
12423
- const v = await res.json();
12424
- if (v.error || !v.formattedAddress) return null;
12425
- return { address: v.formattedAddress, placeId: v.googlePlaceId ?? null };
12913
+ const result = await client.get(`/api/address/autocomplete?q=${encodeURIComponent(q)}`);
12914
+ return result.ok ? result.data.suggestions ?? [] : [];
12915
+ }
12916
+ async function validateStoreAddress(address, client) {
12917
+ const result = await client.post("/api/address/validate", { address });
12918
+ if (!result.ok || result.data.error || !result.data.formattedAddress)
12919
+ return null;
12920
+ return {
12921
+ address: result.data.formattedAddress,
12922
+ placeId: result.data.googlePlaceId ?? null
12923
+ };
12426
12924
  }
12427
- function CheckoutClient(_props) {
12925
+ function CheckoutClient({
12926
+ storeApiOrigin,
12927
+ ordersHref = "/portal/orders",
12928
+ completionHref = "/checkout/complete"
12929
+ }) {
12930
+ const client = React24.useMemo(
12931
+ () => createStoreClient({ apiOrigin: storeApiOrigin }),
12932
+ [storeApiOrigin]
12933
+ );
12428
12934
  const [stage, setStage] = React24.useState("loading");
12429
12935
  const [givenName, setGivenName] = React24.useState(null);
12430
12936
  const [cart] = React24.useState(() => readCart());
12937
+ const recoveryKey = React24.useMemo(
12938
+ () => checkoutRecoveryKey(client.apiOrigin),
12939
+ [client]
12940
+ );
12941
+ const [initialRecovery] = React24.useState(
12942
+ () => readCheckoutRecovery(recoveryKey)
12943
+ );
12944
+ const recoveryRef = React24.useRef(
12945
+ initialRecovery
12946
+ );
12947
+ const attemptTokenRef = React24.useRef(
12948
+ initialRecovery?.attemptToken ?? null
12949
+ );
12950
+ const [checkoutLines, setCheckoutLines] = React24.useState(
12951
+ () => cartToWireLines(cart)
12952
+ );
12953
+ const checkoutCartSnapshotRef = React24.useRef(cart.lines);
12431
12954
  const [quote, setQuote] = React24.useState(null);
12955
+ const [pendingQuote, setPendingQuote] = React24.useState(null);
12956
+ const [pendingQuoteBaseline, setPendingQuoteBaseline] = React24.useState(null);
12432
12957
  const [shippingOptions, setShippingOptions] = React24.useState([]);
12433
12958
  const [error2, setError] = React24.useState(null);
12434
- const [fulfilment, setFulfilment] = React24.useState("pickup");
12435
- const [shippingOptionId, setShippingOptionId] = React24.useState(null);
12959
+ const [resumableCheckout, setResumableCheckout] = React24.useState(null);
12960
+ const [fulfilment, setFulfilment] = React24.useState(
12961
+ "pickup"
12962
+ );
12963
+ const [shippingOptionId, setShippingOptionId] = React24.useState(
12964
+ null
12965
+ );
12436
12966
  const [address, setAddress] = React24.useState(null);
12437
- const [card, setCard] = React24.useState({ number: "", expiry: "", name: "", cvn: "" });
12967
+ const [card, setCard] = React24.useState({
12968
+ number: "",
12969
+ expiry: "",
12970
+ name: "",
12971
+ cvn: ""
12972
+ });
12438
12973
  const orderRef = React24.useRef(null);
12439
12974
  const [iframeUrl, setIframeUrl] = React24.useState(null);
12440
12975
  const iframeOriginRef = React24.useRef(null);
12441
12976
  const iframeElRef = React24.useRef(null);
12442
- React24.useEffect(() => {
12443
- let alive = true;
12444
- if (cart.lines.length === 0) {
12445
- setStage("empty");
12446
- return;
12447
- }
12448
- Promise.all([
12449
- fetch("/api/store/session", { headers: { accept: "application/json" } }).then(
12450
- (r) => r.ok ? r.json() : { signedIn: false }
12451
- ),
12452
- fetch("/api/store/quote", {
12453
- method: "POST",
12454
- headers: { "content-type": "application/json" },
12455
- body: JSON.stringify({ lines: cartToWireLines(cart) })
12456
- }).then((r) => r.ok ? r.json() : null),
12457
- fetch("/api/store/catalog", { headers: { accept: "application/json" } }).then(
12458
- (r) => r.ok ? r.json() : { shippingOptions: [] }
12459
- )
12460
- ]).then(([session, q, catalog]) => {
12461
- if (!alive) return;
12462
- setQuote(q);
12463
- setShippingOptions(catalog.shippingOptions ?? []);
12464
- setGivenName(session.givenName ?? null);
12465
- setStage(session.signedIn ? "details" : "signed_out");
12466
- }).catch(() => {
12467
- if (alive) setError("We couldn\u2019t load your checkout. Please try again.");
12977
+ const ownsCheckoutRef = React24.useRef(false);
12978
+ const ownershipClaimRef = React24.useRef(null);
12979
+ const releaseCheckoutOwnershipRef = React24.useRef(null);
12980
+ async function claimCheckoutOwnership() {
12981
+ if (ownsCheckoutRef.current) return true;
12982
+ if (ownershipClaimRef.current) return ownershipClaimRef.current;
12983
+ const locks = navigator.locks;
12984
+ if (!locks) return false;
12985
+ const claim = new Promise((resolveClaim) => {
12986
+ let resolveHold = null;
12987
+ const hold = new Promise((resolve) => {
12988
+ resolveHold = resolve;
12989
+ });
12990
+ void locks.request(
12991
+ `patientos-store-checkout:${recoveryKey}`,
12992
+ { mode: "exclusive", ifAvailable: true },
12993
+ async (lock) => {
12994
+ if (!lock) {
12995
+ resolveClaim(false);
12996
+ return;
12997
+ }
12998
+ ownsCheckoutRef.current = true;
12999
+ releaseCheckoutOwnershipRef.current = () => {
13000
+ if (!ownsCheckoutRef.current) return;
13001
+ ownsCheckoutRef.current = false;
13002
+ releaseCheckoutOwnershipRef.current = null;
13003
+ resolveHold?.();
13004
+ };
13005
+ resolveClaim(true);
13006
+ await hold;
13007
+ }
13008
+ ).catch(() => resolveClaim(false));
12468
13009
  });
12469
- return () => {
12470
- alive = false;
12471
- };
12472
- }, [cart]);
13010
+ ownershipClaimRef.current = claim;
13011
+ const acquired = await claim;
13012
+ ownershipClaimRef.current = null;
13013
+ return acquired;
13014
+ }
13015
+ function releaseCheckoutOwnership() {
13016
+ releaseCheckoutOwnershipRef.current?.();
13017
+ }
13018
+ function goToCompletion(orderId) {
13019
+ window.location.assign(
13020
+ checkoutCompletionUrl(completionHref, orderId, window.location.origin)
13021
+ );
13022
+ }
13023
+ React24.useEffect(
13024
+ () => () => {
13025
+ releaseCheckoutOwnership();
13026
+ },
13027
+ []
13028
+ );
13029
+ function releaseOwnedAttempt(attemptToken) {
13030
+ const removed = removeCheckoutRecovery(recoveryKey, attemptToken);
13031
+ if (removed && recoveryRef.current?.attemptToken === attemptToken) {
13032
+ recoveryRef.current = null;
13033
+ attemptTokenRef.current = null;
13034
+ }
13035
+ return removed;
13036
+ }
13037
+ async function finishPaidAttemptWhileLocked(attemptToken) {
13038
+ const stored = readCheckoutRecovery(recoveryKey);
13039
+ if (stored?.attemptToken !== attemptToken || stored.lines === null)
13040
+ return "snapshot_unavailable";
13041
+ const settled = await subtractPurchasedCartLines(
13042
+ stored.lines,
13043
+ attemptToken
13044
+ );
13045
+ if (!settled.persisted) return "storage_unavailable";
13046
+ return releaseOwnedAttempt(attemptToken) ? "settled" : "storage_unavailable";
13047
+ }
13048
+ async function finishPaidAttempt(attemptToken) {
13049
+ if (ownsCheckoutRef.current) {
13050
+ const result = await finishPaidAttemptWhileLocked(attemptToken);
13051
+ if (result === "settled") releaseCheckoutOwnership();
13052
+ return result;
13053
+ }
13054
+ const locks = navigator.locks;
13055
+ if (!locks) return "storage_unavailable";
13056
+ return locks.request(
13057
+ `patientos-store-checkout:${recoveryKey}`,
13058
+ () => finishPaidAttemptWhileLocked(attemptToken)
13059
+ );
13060
+ }
13061
+ function paidReconciliationPending(result) {
13062
+ setError(
13063
+ result === "snapshot_unavailable" ? "Payment was received, but this browser no longer has a safe cart snapshot. Review your cart and order before checking out again." : "Payment was received, but your cart update could not be saved. Allow site storage, then try again."
13064
+ );
13065
+ setStage("reconciliation_pending");
13066
+ }
13067
+ async function completePaidAttempt(attemptToken, orderId, alreadyLocked = false) {
13068
+ const result = alreadyLocked ? await finishPaidAttemptWhileLocked(attemptToken) : await finishPaidAttempt(attemptToken);
13069
+ if (result !== "settled") {
13070
+ paidReconciliationPending(result);
13071
+ return false;
13072
+ }
13073
+ if (alreadyLocked) releaseCheckoutOwnership();
13074
+ setStage("paid");
13075
+ goToCompletion(orderId);
13076
+ return true;
13077
+ }
13078
+ async function retryPaidReconciliation() {
13079
+ const attemptToken = attemptTokenRef.current;
13080
+ if (!attemptToken) return;
13081
+ await completePaidAttempt(
13082
+ attemptToken,
13083
+ orderRef.current?.orderId ?? attemptToken
13084
+ );
13085
+ }
13086
+ function recoverySnapshotUnavailable() {
13087
+ releaseCheckoutOwnership();
13088
+ setError(
13089
+ "This checkout can\u2019t be safely continued in this browser. Review My orders and your cart before trying again."
13090
+ );
13091
+ setStage("closed");
13092
+ }
13093
+ function storageUnavailable() {
13094
+ releaseCheckoutOwnership();
13095
+ setError(
13096
+ "Your browser couldn\u2019t safely save and coordinate checkout progress. Allow site storage, then reload and try again."
13097
+ );
13098
+ setStage("storage_error");
13099
+ }
12473
13100
  React24.useEffect(() => {
12474
- if (stage !== "details" || cart.lines.length === 0) return;
12475
13101
  let alive = true;
12476
- fetch("/api/store/quote", {
12477
- method: "POST",
12478
- headers: { "content-type": "application/json" },
12479
- body: JSON.stringify({
12480
- lines: cartToWireLines(cart),
12481
- shippingOptionId: fulfilment === "ship" ? shippingOptionId : null
12482
- })
12483
- }).then((r) => r.ok ? r.json() : null).then((q) => {
12484
- if (alive && q) setQuote(q);
12485
- }).catch(() => void 0);
13102
+ void (async () => {
13103
+ const locks = navigator.locks;
13104
+ if (!locks) {
13105
+ if (!alive) return;
13106
+ storageUnavailable();
13107
+ return;
13108
+ }
13109
+ const storedAttemptToken = attemptTokenRef.current;
13110
+ let bootRecovery = {
13111
+ status: "continue"
13112
+ };
13113
+ if (storedAttemptToken) {
13114
+ const acquired = await claimCheckoutOwnership();
13115
+ if (acquired) {
13116
+ const recovered = await client.post(
13117
+ "/api/store/checkout/recover",
13118
+ { attemptToken: storedAttemptToken }
13119
+ );
13120
+ if (recovered.ok && recovered.data.status === "paid") {
13121
+ await completePaidAttempt(
13122
+ storedAttemptToken,
13123
+ recovered.data.orderId ?? storedAttemptToken,
13124
+ true
13125
+ );
13126
+ return;
13127
+ } else if (recovered.ok && recovered.data.status === "active") {
13128
+ releaseCheckoutOwnership();
13129
+ bootRecovery = { status: "active" };
13130
+ } else if (recovered.ok && recovered.data.status === "resumable" && !Array.isArray(readCheckoutRecovery(recoveryKey)?.lines)) {
13131
+ recoverySnapshotUnavailable();
13132
+ return;
13133
+ } else if (recovered.ok && recovered.data.status === "resumable" && Array.isArray(readCheckoutRecovery(recoveryKey)?.lines) && recovered.data.orderId && recovered.data.sessionId && recovered.data.sessionStatus && recovered.data.providerKey && recovered.data.amount && recovered.data.clientArtifacts) {
13134
+ bootRecovery = {
13135
+ status: "resumable",
13136
+ session: recovered.data
13137
+ };
13138
+ } else if (recovered.ok && recovered.data.status === "processing") {
13139
+ releaseCheckoutOwnership();
13140
+ bootRecovery = { status: "processing" };
13141
+ } else if (recovered.ok) {
13142
+ if (!releaseOwnedAttempt(storedAttemptToken)) {
13143
+ recoverySnapshotUnavailable();
13144
+ return;
13145
+ }
13146
+ releaseCheckoutOwnership();
13147
+ } else {
13148
+ releaseCheckoutOwnership();
13149
+ bootRecovery = {
13150
+ status: recovered.error === "sign_in_required" ? "continue" : "error"
13151
+ };
13152
+ }
13153
+ } else {
13154
+ bootRecovery = { status: "active_elsewhere" };
13155
+ }
13156
+ }
13157
+ if (!alive) return;
13158
+ if (bootRecovery.status === "active") {
13159
+ setStage("active");
13160
+ return;
13161
+ }
13162
+ if (bootRecovery.status === "active_elsewhere") {
13163
+ setStage("active_elsewhere");
13164
+ return;
13165
+ }
13166
+ if (bootRecovery.status === "resumable") {
13167
+ setResumableCheckout(bootRecovery.session);
13168
+ setStage("resume");
13169
+ return;
13170
+ }
13171
+ if (bootRecovery.status === "processing") {
13172
+ setStage("pending");
13173
+ return;
13174
+ }
13175
+ if (bootRecovery.status === "error") {
13176
+ setError(
13177
+ "We couldn\u2019t recover your previous checkout. Please try again."
13178
+ );
13179
+ setStage("quote_error");
13180
+ return;
13181
+ }
13182
+ if (cart.lines.length === 0) {
13183
+ if (!alive) return;
13184
+ setStage("empty");
13185
+ return;
13186
+ }
13187
+ const [sessionResult, quoteResult, catalogResult] = await Promise.all([
13188
+ client.get("/api/store/session"),
13189
+ client.post("/api/store/quote", {
13190
+ lines: checkoutLines
13191
+ }),
13192
+ client.get("/api/store/catalog")
13193
+ ]);
13194
+ if (!alive) return;
13195
+ const session = sessionResult.ok && typeof sessionResult.data.signedIn === "boolean" ? sessionResult.data : null;
13196
+ const unsupportedQuote = quoteResult.ok && quoteResult.data.problems.some(
13197
+ (problem) => problem.code === "not_purchasable"
13198
+ );
13199
+ const payableQuote = quoteResult.ok && quoteResult.data.lines.length > 0 && !unsupportedQuote ? quoteResult.data : null;
13200
+ const catalogLoaded = catalogResult.ok && Array.isArray(catalogResult.data.shippingOptions);
13201
+ setQuote(payableQuote);
13202
+ setShippingOptions(
13203
+ catalogLoaded ? catalogResult.data.shippingOptions : []
13204
+ );
13205
+ setGivenName(session?.givenName ?? null);
13206
+ if (!session) {
13207
+ setError("We couldn\u2019t load your checkout. Please try again.");
13208
+ setStage("quote_error");
13209
+ } else if (unsupportedQuote) {
13210
+ setError(
13211
+ "Remove unsupported items from your cart before starting retail checkout."
13212
+ );
13213
+ setStage("unsupported");
13214
+ } else if (quoteResult.ok && quoteResult.data.lines.length === 0) {
13215
+ setError(null);
13216
+ setStage("empty");
13217
+ } else if (session.signedIn === false) {
13218
+ setStage("signed_out");
13219
+ } else if (!catalogLoaded) {
13220
+ setError("We couldn\u2019t load delivery options. Please try again.");
13221
+ setStage("quote_error");
13222
+ } else if (quoteResult.ok) {
13223
+ setError(null);
13224
+ setStage("details");
13225
+ } else {
13226
+ setError("We couldn\u2019t load your checkout. Please try again.");
13227
+ setStage("quote_error");
13228
+ }
13229
+ })();
13230
+ return () => {
13231
+ alive = false;
13232
+ };
13233
+ }, [cart, client]);
13234
+ React24.useEffect(() => {
13235
+ if (stage !== "details" || cart.lines.length === 0) return;
13236
+ let alive = true;
13237
+ setQuote(null);
13238
+ setError(null);
13239
+ void client.post("/api/store/quote", {
13240
+ lines: checkoutLines,
13241
+ shippingOptionId: fulfilment === "ship" ? shippingOptionId : null
13242
+ }).then((result) => {
13243
+ if (!alive) return;
13244
+ if (result.ok && result.data.lines.length > 0) {
13245
+ setQuote(result.data);
13246
+ } else if (result.ok) {
13247
+ setQuote(null);
13248
+ setStage("empty");
13249
+ } else {
13250
+ setError("We couldn\u2019t update your checkout total. Please try again.");
13251
+ setStage("quote_error");
13252
+ }
13253
+ });
12486
13254
  return () => {
12487
13255
  alive = false;
12488
13256
  };
12489
- }, [fulfilment, shippingOptionId]);
13257
+ }, [client, fulfilment, shippingOptionId]);
12490
13258
  React24.useEffect(() => {
12491
13259
  if (stage !== "three_ds") return;
13260
+ let terminalHandled = false;
12492
13261
  function onMessage(event) {
12493
- if (iframeOriginRef.current && event.origin !== iframeOriginRef.current) return;
12494
- if (iframeElRef.current && event.source !== iframeElRef.current.contentWindow) return;
12495
- const data = typeof event.data === "string" ? event.data : null;
12496
- if (!data) return;
12497
- if (data === "AuthenticationInternalError") {
12498
- setIframeUrl(null);
12499
- setError("The card check could not be completed. Please try another card.");
12500
- setStage("details");
13262
+ if (iframeOriginRef.current && event.origin !== iframeOriginRef.current)
13263
+ return;
13264
+ const iframe = iframeElRef.current;
13265
+ if (!iframe || event.source !== iframe.contentWindow) return;
13266
+ const type = event.data?.type;
13267
+ if (type === "AuthenticationUserInteractionRequired") {
13268
+ if (iframe) {
13269
+ iframe.style.visibility = "visible";
13270
+ iframe.style.height = "500px";
13271
+ }
12501
13272
  return;
12502
13273
  }
12503
- if (data === "AuthenticationComplete" || data === "AuthenticationUserInteractionFinished") {
12504
- setIframeUrl(null);
12505
- void afterChallenge();
13274
+ if (type === "AuthenticationUserInteractionFinished") {
13275
+ if (iframe) {
13276
+ iframe.style.visibility = "hidden";
13277
+ iframe.style.height = "1px";
13278
+ }
13279
+ return;
12506
13280
  }
13281
+ if (type !== "AuthenticationInternalError" && type !== "AuthenticationComplete")
13282
+ return;
13283
+ if (terminalHandled) return;
13284
+ terminalHandled = true;
13285
+ window.removeEventListener("message", onMessage);
13286
+ setIframeUrl(null);
13287
+ if (type === "AuthenticationInternalError") {
13288
+ void cancelFailedChallenge();
13289
+ return;
13290
+ }
13291
+ void afterChallenge();
12507
13292
  }
12508
13293
  window.addEventListener("message", onMessage);
12509
13294
  return () => window.removeEventListener("message", onMessage);
12510
13295
  }, [stage]);
12511
13296
  async function post(path, body) {
12512
- const res = await fetch(path, {
12513
- method: "POST",
12514
- headers: { "content-type": "application/json" },
12515
- body: JSON.stringify(body)
12516
- });
12517
- const data = await res.json().catch(() => ({}));
12518
- if (!res.ok) {
12519
- if (data.error === "sign_in_required") {
13297
+ const result = await client.post(path, body);
13298
+ if (!result.ok) {
13299
+ if (result.error === "sign_in_required") {
12520
13300
  setStage("signed_out");
12521
13301
  throw new Error("sign_in_required");
12522
13302
  }
12523
- throw new Error(typeof data.message === "string" ? data.message : "Something went wrong.");
13303
+ throw new StoreRequestError(
13304
+ result.message ?? "Something went wrong.",
13305
+ result.error
13306
+ );
13307
+ }
13308
+ return result.data;
13309
+ }
13310
+ function showChallenge(artifacts) {
13311
+ if (!artifacts.iframeUrl) return false;
13312
+ const iframeUrl2 = client.url(artifacts.iframeUrl);
13313
+ try {
13314
+ const parsedIframeUrl = new URL(iframeUrl2, window.location.href);
13315
+ const ownedMockUrl = new URL(
13316
+ client.url("/api/store/mock-3ds-challenge"),
13317
+ window.location.href
13318
+ );
13319
+ const isOwnedMock = parsedIframeUrl.origin === ownedMockUrl.origin && parsedIframeUrl.pathname === ownedMockUrl.pathname && parsedIframeUrl.search === "" && parsedIframeUrl.hash === "";
13320
+ if (parsedIframeUrl.username || parsedIframeUrl.password) return false;
13321
+ if (isOwnedMock) {
13322
+ const allowedProtocol = parsedIframeUrl.protocol === "https:" || parsedIframeUrl.protocol === "http:" && parsedIframeUrl.hostname === "localhost";
13323
+ if (!allowedProtocol) return false;
13324
+ iframeOriginRef.current = parsedIframeUrl.origin;
13325
+ } else {
13326
+ if (parsedIframeUrl.protocol !== "https:" || !artifacts.iframeOrigin)
13327
+ return false;
13328
+ const suppliedOrigin = new URL(artifacts.iframeOrigin);
13329
+ if (suppliedOrigin.protocol !== "https:" || suppliedOrigin.username || suppliedOrigin.password || suppliedOrigin.pathname !== "/" || suppliedOrigin.search || suppliedOrigin.hash)
13330
+ return false;
13331
+ if (parsedIframeUrl.origin !== suppliedOrigin.origin) return false;
13332
+ iframeOriginRef.current = suppliedOrigin.origin;
13333
+ }
13334
+ } catch {
13335
+ return false;
13336
+ }
13337
+ setIframeUrl(iframeUrl2);
13338
+ setStage("three_ds");
13339
+ return true;
13340
+ }
13341
+ async function cancelFailedChallenge() {
13342
+ const orderId = orderRef.current?.orderId;
13343
+ if (!orderId) {
13344
+ setStage("pending");
13345
+ return;
13346
+ }
13347
+ setStage("paying");
13348
+ try {
13349
+ await post("/api/store/checkout/cancel-challenge", { orderId });
13350
+ const attemptToken = attemptTokenRef.current;
13351
+ if (attemptToken) releaseOwnedAttempt(attemptToken);
13352
+ setError("The card check could not be completed. Please try again.");
13353
+ setStage(quote ? "details" : "closed");
13354
+ releaseCheckoutOwnership();
13355
+ } catch {
13356
+ setError(
13357
+ "We couldn\u2019t safely close the failed card check. Go to your orders before trying again."
13358
+ );
13359
+ setStage("pending");
13360
+ }
13361
+ }
13362
+ async function recoverAttemptWhileLocked(attemptToken, continueRestart = false) {
13363
+ const recovered = await client.post(
13364
+ "/api/store/checkout/recover",
13365
+ { attemptToken }
13366
+ );
13367
+ if (recovered.ok && recovered.data.status === "active") {
13368
+ setStage("active");
13369
+ releaseCheckoutOwnership();
13370
+ return "active";
13371
+ }
13372
+ if (recovered.ok && recovered.data.status === "resumable" && !Array.isArray(readCheckoutRecovery(recoveryKey)?.lines)) {
13373
+ recoverySnapshotUnavailable();
13374
+ return "blocked";
13375
+ }
13376
+ if (recovered.ok && recovered.data.status === "resumable" && Array.isArray(readCheckoutRecovery(recoveryKey)?.lines) && recovered.data.orderId && recovered.data.sessionId && recovered.data.sessionStatus && recovered.data.providerKey && recovered.data.amount && recovered.data.clientArtifacts) {
13377
+ setResumableCheckout(recovered.data);
13378
+ setStage("resume");
13379
+ return "active";
13380
+ }
13381
+ if (recovered.ok && recovered.data.status === "processing") {
13382
+ setStage("pending");
13383
+ releaseCheckoutOwnership();
13384
+ return "processing";
13385
+ }
13386
+ if (recovered.ok && recovered.data.status === "paid") {
13387
+ return await completePaidAttempt(
13388
+ attemptToken,
13389
+ recovered.data.orderId ?? attemptToken,
13390
+ true
13391
+ ) ? "paid" : "blocked";
13392
+ }
13393
+ if (recovered.ok) {
13394
+ if (!releaseOwnedAttempt(attemptToken)) {
13395
+ recoverySnapshotUnavailable();
13396
+ return "blocked";
13397
+ }
13398
+ if (!continueRestart) {
13399
+ releaseCheckoutOwnership();
13400
+ setError(
13401
+ "Your previous checkout was closed. Review the total and try again."
13402
+ );
13403
+ setStage("details");
13404
+ }
13405
+ return "restart";
12524
13406
  }
12525
- return data;
13407
+ setError(
13408
+ "We couldn\u2019t safely close your previous checkout. Go to your orders before trying again."
13409
+ );
13410
+ setStage("closed");
13411
+ releaseCheckoutOwnership();
13412
+ return "blocked";
13413
+ }
13414
+ async function recoverAttemptForRestart(attemptToken) {
13415
+ if (!await claimCheckoutOwnership()) {
13416
+ setStage("active_elsewhere");
13417
+ return "active";
13418
+ }
13419
+ return recoverAttemptWhileLocked(attemptToken);
13420
+ }
13421
+ async function transitionRetryableDecline(message) {
13422
+ const orderId = orderRef.current?.orderId;
13423
+ if (!orderId) {
13424
+ const attemptToken = attemptTokenRef.current;
13425
+ if (attemptToken) await recoverAttemptWhileLocked(attemptToken);
13426
+ else setStage("pending");
13427
+ return;
13428
+ }
13429
+ setError(message);
13430
+ setStage("loading");
13431
+ const result = await client.post("/api/store/checkout/retry-payment", {
13432
+ orderId
13433
+ }).catch(() => null);
13434
+ const retryData = result?.ok && result.data !== null && typeof result.data === "object" && !Array.isArray(result.data) ? result.data : null;
13435
+ const retryArtifacts = retryData?.clientArtifacts !== null && typeof retryData?.clientArtifacts === "object" && !Array.isArray(retryData.clientArtifacts) ? retryData.clientArtifacts : null;
13436
+ if (retryData?.status !== "ready" || retryData.orderId !== orderId || typeof retryData.sessionId !== "string" || typeof retryData.providerKey !== "string" || typeof retryData.amount !== "string" || !retryArtifacts) {
13437
+ await recoverAttemptWhileLocked(orderId);
13438
+ return;
13439
+ }
13440
+ setResumableCheckout({
13441
+ orderId,
13442
+ sessionId: retryData.sessionId,
13443
+ sessionStatus: "pending",
13444
+ providerKey: retryData.providerKey,
13445
+ amount: retryData.amount,
13446
+ expiresAt: typeof retryData.expiresAt === "string" ? retryData.expiresAt : null,
13447
+ clientArtifacts: retryArtifacts
13448
+ });
13449
+ setStage("resume_payment");
13450
+ }
13451
+ async function handlePaymentRequestError(error3) {
13452
+ if (!(error3 instanceof StoreRequestError)) return false;
13453
+ if (error3.code === "payment_in_progress" || error3.code === "payment_initializing") {
13454
+ setStage("pending");
13455
+ releaseCheckoutOwnership();
13456
+ return true;
13457
+ }
13458
+ if (error3.code === "payment_failed") {
13459
+ await transitionRetryableDecline(
13460
+ "This payment attempt failed. Please check the details or try another card."
13461
+ );
13462
+ return true;
13463
+ }
13464
+ if (error3.code === "payment_cancelled") {
13465
+ setError(
13466
+ "This payment attempt is closed. Go to your orders before trying again."
13467
+ );
13468
+ setStage("closed");
13469
+ releaseCheckoutOwnership();
13470
+ return true;
13471
+ }
13472
+ return false;
12526
13473
  }
12527
13474
  async function applyConfirm(result) {
12528
13475
  const status = result.status;
12529
13476
  if (status === "paid") {
12530
13477
  const orderId = orderRef.current?.orderId ?? "";
12531
- clearCart();
12532
- setStage("paid");
12533
- window.location.assign(`/checkout/complete?order=${encodeURIComponent(orderId)}`);
13478
+ const attemptToken = attemptTokenRef.current;
13479
+ if (!attemptToken) {
13480
+ paidReconciliationPending("snapshot_unavailable");
13481
+ return;
13482
+ }
13483
+ await completePaidAttempt(attemptToken, orderId);
12534
13484
  return;
12535
13485
  }
12536
13486
  if (status === "requires_action") {
12537
13487
  const artifacts = result.clientArtifacts ?? {};
12538
- if (!artifacts.iframeUrl) {
12539
- setError("Your bank asked for a check we couldn\u2019t show. Please try another card.");
12540
- setStage("details");
12541
- return;
12542
- }
12543
- iframeOriginRef.current = artifacts.iframeOrigin ?? null;
12544
- setIframeUrl(artifacts.iframeUrl);
12545
- setStage("three_ds");
13488
+ if (!showChallenge(artifacts)) await cancelFailedChallenge();
12546
13489
  return;
12547
13490
  }
12548
13491
  if (status === "declined") {
12549
- setError("Your card was declined. Please check the details or try another card.");
12550
- setStage("details");
13492
+ await transitionRetryableDecline(
13493
+ "Your card was declined. Please check the details or try another card."
13494
+ );
12551
13495
  return;
12552
13496
  }
12553
13497
  setStage("pending");
13498
+ releaseCheckoutOwnership();
13499
+ }
13500
+ async function dispatchConfirm(orderId) {
13501
+ try {
13502
+ await applyConfirm(
13503
+ await post("/api/store/checkout/confirm", { orderId })
13504
+ );
13505
+ } catch (err) {
13506
+ if (err instanceof Error && err.message === "sign_in_required") return;
13507
+ const attemptToken = attemptTokenRef.current;
13508
+ if (attemptToken && err instanceof StoreRequestError && err.code === "checkout_paid") {
13509
+ await completePaidAttempt(attemptToken, orderId);
13510
+ return;
13511
+ }
13512
+ if (await handlePaymentRequestError(err)) return;
13513
+ setError(null);
13514
+ setStage("pending");
13515
+ releaseCheckoutOwnership();
13516
+ }
12554
13517
  }
12555
13518
  async function afterChallenge() {
12556
13519
  const orderId = orderRef.current?.orderId;
@@ -12559,45 +13522,245 @@ function CheckoutClient(_props) {
12559
13522
  try {
12560
13523
  const auth = await post("/api/store/checkout/authenticate", { orderId });
12561
13524
  if (auth.status === "payment_failed") {
12562
- setError("The card check failed. Please try another card.");
12563
- setStage("details");
13525
+ await transitionRetryableDecline(
13526
+ "The card check failed. Please try another card."
13527
+ );
12564
13528
  return;
12565
13529
  }
12566
13530
  if (auth.status === "requires_action") {
12567
13531
  const artifacts = auth.clientArtifacts ?? {};
12568
- if (artifacts.iframeUrl) {
12569
- iframeOriginRef.current = artifacts.iframeOrigin ?? null;
12570
- setIframeUrl(artifacts.iframeUrl);
12571
- setStage("three_ds");
13532
+ if (showChallenge(artifacts)) return;
13533
+ await cancelFailedChallenge();
13534
+ return;
13535
+ }
13536
+ await dispatchConfirm(orderId);
13537
+ } catch (err) {
13538
+ if (err instanceof Error && err.message === "sign_in_required") return;
13539
+ if (await handlePaymentRequestError(err)) return;
13540
+ setError(err instanceof Error ? err.message : "Something went wrong.");
13541
+ setStage(resumableCheckout ? "pending" : "details");
13542
+ if (resumableCheckout) releaseCheckoutOwnership();
13543
+ }
13544
+ }
13545
+ async function initializeCheckoutAttempt(currentQuote) {
13546
+ if (!ownsCheckoutRef.current) return null;
13547
+ while (true) {
13548
+ const ownedToken = attemptTokenRef.current;
13549
+ const stored = readCheckoutRecovery(recoveryKey);
13550
+ if (ownedToken) {
13551
+ if (stored?.attemptToken !== ownedToken || stored.lines === null) {
13552
+ recoverySnapshotUnavailable();
13553
+ return null;
13554
+ }
13555
+ recoveryRef.current = stored;
13556
+ break;
13557
+ }
13558
+ if (stored) {
13559
+ recoveryRef.current = stored;
13560
+ attemptTokenRef.current = stored.attemptToken;
13561
+ const disposition = await recoverAttemptWhileLocked(
13562
+ stored.attemptToken,
13563
+ true
13564
+ );
13565
+ if (disposition !== "restart") return null;
13566
+ continue;
13567
+ }
13568
+ break;
13569
+ }
13570
+ const existingToken = attemptTokenRef.current;
13571
+ const expectedLines = recoveryRef.current?.lines ?? checkoutCartSnapshotRef.current;
13572
+ if (expectedLines === null) {
13573
+ recoverySnapshotUnavailable();
13574
+ return null;
13575
+ }
13576
+ const selectedShippingOptionId = fulfilment === "ship" ? shippingOptionId : null;
13577
+ const coordinated = await withCartLock(async (latest, signal) => {
13578
+ const wireLines = cartToWireLines(latest);
13579
+ const quoteResult = await client.post(
13580
+ "/api/store/quote",
13581
+ {
13582
+ lines: wireLines,
13583
+ shippingOptionId: selectedShippingOptionId
13584
+ },
13585
+ { signal }
13586
+ );
13587
+ if (!quoteResult.ok || signal.aborted)
13588
+ return { status: "quote_error" };
13589
+ const freshQuote = quoteResult.data;
13590
+ const purchasedLines = snapshotPurchasedCartLines(
13591
+ latest.lines,
13592
+ freshQuote.lines
13593
+ );
13594
+ if (!purchasedLines || freshQuote.lines.length === 0 || freshQuote.problems.length > 0 || !sameCartLineSnapshot(latest.lines, expectedLines) || !samePayableQuote(currentQuote, freshQuote, selectedShippingOptionId)) {
13595
+ return {
13596
+ status: "cart_changed",
13597
+ quote: freshQuote,
13598
+ baseline: latest
13599
+ };
13600
+ }
13601
+ const attemptToken = existingToken ?? crypto.randomUUID();
13602
+ if (existingToken) {
13603
+ if (!sameCartLineSnapshot(purchasedLines, expectedLines)) {
13604
+ return {
13605
+ status: "cart_changed",
13606
+ quote: freshQuote,
13607
+ baseline: latest
13608
+ };
13609
+ }
13610
+ } else {
13611
+ const recovery = {
13612
+ v: 2,
13613
+ attemptToken,
13614
+ lines: purchasedLines
13615
+ };
13616
+ if (!writeCheckoutRecovery(recoveryKey, recovery, null))
13617
+ return { status: "storage_error" };
13618
+ recoveryRef.current = recovery;
13619
+ attemptTokenRef.current = attemptToken;
13620
+ }
13621
+ const initPromise = post("/api/store/checkout/init", {
13622
+ lines: wireLines,
13623
+ attemptToken,
13624
+ expectedQuote: expectedQuote(freshQuote, selectedShippingOptionId),
13625
+ fulfilmentMethod: fulfilment,
13626
+ shippingOptionId: selectedShippingOptionId,
13627
+ shippingPlace: fulfilment === "ship" && address ? { address: address.address, placeId: address.placeId ?? null } : null
13628
+ });
13629
+ return {
13630
+ status: "ready",
13631
+ attemptToken,
13632
+ initPromise
13633
+ };
13634
+ });
13635
+ if (coordinated.status === "cart_storage_unavailable") {
13636
+ storageUnavailable();
13637
+ return null;
13638
+ }
13639
+ if (coordinated.status === "not_completed") {
13640
+ setError("We couldn\u2019t confirm your checkout total. Please try again.");
13641
+ setStage("quote_error");
13642
+ releaseCheckoutOwnership();
13643
+ return null;
13644
+ }
13645
+ const result = coordinated.result;
13646
+ if (result.status === "storage_error") {
13647
+ storageUnavailable();
13648
+ return null;
13649
+ }
13650
+ if (result.status === "quote_error") {
13651
+ setError("We couldn\u2019t confirm your checkout total. Please try again.");
13652
+ setStage("quote_error");
13653
+ releaseCheckoutOwnership();
13654
+ return null;
13655
+ }
13656
+ if (result.status === "cart_changed") {
13657
+ if (existingToken) {
13658
+ const disposition = await recoverAttemptWhileLocked(
13659
+ existingToken,
13660
+ true
13661
+ );
13662
+ if (disposition !== "restart") return null;
13663
+ }
13664
+ setPendingQuote(result.quote);
13665
+ setPendingQuoteBaseline(result.baseline);
13666
+ setQuote(null);
13667
+ setError(null);
13668
+ setStage("cart_changed");
13669
+ return null;
13670
+ }
13671
+ return {
13672
+ attemptToken: result.attemptToken,
13673
+ init: await result.initPromise
13674
+ };
13675
+ }
13676
+ async function beginResume() {
13677
+ if (!resumableCheckout) return;
13678
+ if (!await claimCheckoutOwnership()) {
13679
+ setStage("active_elsewhere");
13680
+ return;
13681
+ }
13682
+ orderRef.current = {
13683
+ orderId: resumableCheckout.orderId,
13684
+ providerKey: resumableCheckout.providerKey
13685
+ };
13686
+ if (resumableCheckout.sessionStatus === "requires_action") {
13687
+ if (!showChallenge(resumableCheckout.clientArtifacts))
13688
+ await cancelFailedChallenge();
13689
+ return;
13690
+ }
13691
+ setError(null);
13692
+ setStage("resume_payment");
13693
+ }
13694
+ async function onResumePayment(e) {
13695
+ e.preventDefault();
13696
+ if (!resumableCheckout) return;
13697
+ if (!await claimCheckoutOwnership()) {
13698
+ setStage("active_elsewhere");
13699
+ return;
13700
+ }
13701
+ setError(null);
13702
+ setStage("paying");
13703
+ orderRef.current = {
13704
+ orderId: resumableCheckout.orderId,
13705
+ providerKey: resumableCheckout.providerKey
13706
+ };
13707
+ try {
13708
+ if (resumableCheckout.providerKey !== "mock") {
13709
+ const { authKey } = resumableCheckout.clientArtifacts;
13710
+ if (!authKey) {
13711
+ setStage("pending");
13712
+ releaseCheckoutOwnership();
12572
13713
  return;
12573
13714
  }
13715
+ await loadBpointScript();
13716
+ await attachCardToAuthKey(authKey, card);
12574
13717
  }
12575
- await applyConfirm(await post("/api/store/checkout/confirm", { orderId }));
13718
+ await dispatchConfirm(resumableCheckout.orderId);
12576
13719
  } catch (err) {
12577
- if (err instanceof Error && err.message === "sign_in_required") return;
13720
+ if (err instanceof Error && err.message === "sign_in_required") {
13721
+ releaseCheckoutOwnership();
13722
+ return;
13723
+ }
13724
+ if (await handlePaymentRequestError(err)) return;
12578
13725
  setError(err instanceof Error ? err.message : "Something went wrong.");
12579
- setStage("details");
13726
+ setStage("resume_payment");
12580
13727
  }
12581
13728
  }
12582
13729
  async function onPay(e) {
12583
13730
  e.preventDefault();
13731
+ if (!quote || quote.lines.length === 0) {
13732
+ setQuote(null);
13733
+ setError(
13734
+ quote ? null : "We couldn\u2019t confirm your checkout total. Please try again."
13735
+ );
13736
+ setStage(quote ? "empty" : "quote_error");
13737
+ return;
13738
+ }
13739
+ if (!await claimCheckoutOwnership()) {
13740
+ setStage("active_elsewhere");
13741
+ return;
13742
+ }
12584
13743
  setError(null);
12585
13744
  setStage("paying");
13745
+ let attemptToken = attemptTokenRef.current;
12586
13746
  try {
12587
- const init = await post("/api/store/checkout/init", {
12588
- lines: cartToWireLines(cart),
12589
- fulfilmentMethod: fulfilment,
12590
- shippingOptionId: fulfilment === "ship" ? shippingOptionId : null,
12591
- // The REFERENCE, never components — /api/store resolves it against Google.
12592
- shippingPlace: fulfilment === "ship" && address ? { address: address.address, placeId: address.placeId ?? null } : null,
12593
- // A previous, abandoned attempt by this shopper — the server cancels it and
12594
- // gives its stock back in the SAME tx that mints the replacement.
12595
- priorOrderId: orderRef.current?.orderId ?? null
12596
- });
13747
+ const initialized = await initializeCheckoutAttempt(quote);
13748
+ if (!initialized) return;
13749
+ attemptToken = initialized.attemptToken;
13750
+ const { init } = initialized;
13751
+ setError(null);
12597
13752
  if (init.status === "cart_changed") {
12598
- setQuote(init.quote);
12599
- setError("Your cart changed \u2014 please review it before paying.");
12600
- setStage("details");
13753
+ const changedQuote = init.quote;
13754
+ setPendingQuote(changedQuote);
13755
+ const baselineLines = recoveryRef.current?.lines;
13756
+ if (!baselineLines) {
13757
+ recoverySnapshotUnavailable();
13758
+ return;
13759
+ }
13760
+ setPendingQuoteBaseline({ v: 1, lines: baselineLines });
13761
+ setQuote(null);
13762
+ setError(null);
13763
+ setStage("cart_changed");
12601
13764
  return;
12602
13765
  }
12603
13766
  const orderId = String(init.orderId);
@@ -12605,17 +13768,220 @@ function CheckoutClient(_props) {
12605
13768
  orderRef.current = { orderId, providerKey };
12606
13769
  if (providerKey !== "mock") {
12607
13770
  const artifacts = init.clientArtifacts ?? {};
12608
- if (!artifacts.authKey) throw new Error("We couldn\u2019t start a secure payment. Please try again.");
13771
+ if (!artifacts.authKey)
13772
+ throw new Error(
13773
+ "We couldn\u2019t start a secure payment. Please try again."
13774
+ );
12609
13775
  await loadBpointScript();
12610
13776
  await attachCardToAuthKey(artifacts.authKey, card);
12611
13777
  }
12612
- await applyConfirm(await post("/api/store/checkout/confirm", { orderId }));
13778
+ await dispatchConfirm(orderId);
12613
13779
  } catch (err) {
12614
- if (err instanceof Error && err.message === "sign_in_required") return;
13780
+ if (err instanceof Error && err.message === "sign_in_required") {
13781
+ releaseCheckoutOwnership();
13782
+ return;
13783
+ }
13784
+ const failedAttemptToken = attemptToken ?? attemptTokenRef.current;
13785
+ if (failedAttemptToken && err instanceof StoreRequestError && err.code === "checkout_paid") {
13786
+ await completePaidAttempt(failedAttemptToken, failedAttemptToken);
13787
+ return;
13788
+ }
13789
+ if (await handlePaymentRequestError(err)) return;
13790
+ if (failedAttemptToken && err instanceof StoreRequestError && (err.code === "checkout_attempt_changed" || err.code === "checkout_attempt_closed" || err.code === "checkout_init_failed" || err.code === "invalid_checkout_attempt")) {
13791
+ await recoverAttemptForRestart(failedAttemptToken);
13792
+ return;
13793
+ }
12615
13794
  setError(err instanceof Error ? err.message : "Something went wrong.");
12616
13795
  setStage("details");
12617
13796
  }
12618
13797
  }
13798
+ async function retryQuote() {
13799
+ setError(null);
13800
+ setStage("loading");
13801
+ const sessionResult = await client.get("/api/store/session");
13802
+ if (!sessionResult.ok || typeof sessionResult.data.signedIn !== "boolean") {
13803
+ setQuote(null);
13804
+ setPendingQuote(null);
13805
+ setPendingQuoteBaseline(null);
13806
+ setError("We couldn\u2019t load your checkout. Please try again.");
13807
+ setStage("quote_error");
13808
+ return;
13809
+ }
13810
+ const session = sessionResult.data;
13811
+ setGivenName(session.givenName ?? null);
13812
+ if (session.signedIn === false) {
13813
+ setQuote(null);
13814
+ setPendingQuote(null);
13815
+ setPendingQuoteBaseline(null);
13816
+ setStage("signed_out");
13817
+ return;
13818
+ }
13819
+ const [result, catalogResult] = await Promise.all([
13820
+ client.post("/api/store/quote", {
13821
+ lines: checkoutLines,
13822
+ shippingOptionId: fulfilment === "ship" ? shippingOptionId : null
13823
+ }),
13824
+ client.get("/api/store/catalog")
13825
+ ]);
13826
+ const catalogLoaded = catalogResult.ok && Array.isArray(catalogResult.data.shippingOptions);
13827
+ if (!catalogLoaded) {
13828
+ setQuote(null);
13829
+ setShippingOptions([]);
13830
+ setError("We couldn\u2019t load delivery options. Please try again.");
13831
+ setStage("quote_error");
13832
+ } else if (result.ok && result.data.problems.some((problem) => problem.code === "not_purchasable")) {
13833
+ setQuote(null);
13834
+ setError(
13835
+ "Remove unsupported items from your cart before starting retail checkout."
13836
+ );
13837
+ setStage("unsupported");
13838
+ } else if (result.ok && result.data.lines.length > 0) {
13839
+ setShippingOptions(catalogResult.data.shippingOptions);
13840
+ setQuote(result.data);
13841
+ setStage("details");
13842
+ } else if (result.ok) {
13843
+ setQuote(null);
13844
+ setStage("empty");
13845
+ } else {
13846
+ setQuote(null);
13847
+ setError("We couldn\u2019t load your checkout. Please try again.");
13848
+ setStage("quote_error");
13849
+ }
13850
+ }
13851
+ async function acceptHealedQuote() {
13852
+ if (!pendingQuote) return;
13853
+ const locks = navigator.locks;
13854
+ if (!locks) {
13855
+ storageUnavailable();
13856
+ return;
13857
+ }
13858
+ const attemptedRetail = new Map(
13859
+ (pendingQuoteBaseline?.lines ?? checkoutLines).filter((line) => line.kind === "retail").map((line) => [line.variantId, line.quantity])
13860
+ );
13861
+ const healedRetail = new Map(
13862
+ pendingQuote.lines.map((line) => [line.variantId, line.quantity])
13863
+ );
13864
+ const optionUnavailable = pendingQuote.problems.some(
13865
+ (problem) => problem.code === "shipping_option_unavailable"
13866
+ );
13867
+ const nextShippingOptionId = optionUnavailable ? null : shippingOptionId;
13868
+ setPendingQuote(null);
13869
+ setPendingQuoteBaseline(null);
13870
+ setQuote(null);
13871
+ setError(null);
13872
+ setStage("loading");
13873
+ const outcome = {
13874
+ confirmedQuote: null,
13875
+ nextProblemQuote: null,
13876
+ nextProblemBaseline: null,
13877
+ quoteFailed: false
13878
+ };
13879
+ const continueWhileCoordinated = async () => {
13880
+ const attemptToken = attemptTokenRef.current;
13881
+ const stored = readCheckoutRecovery(recoveryKey);
13882
+ if (attemptToken && (stored?.attemptToken !== attemptToken || stored.lines === null) || !attemptToken && stored)
13883
+ return null;
13884
+ return mutateCart(
13885
+ (latest) => ({
13886
+ v: 1,
13887
+ lines: latest.lines.flatMap((line) => {
13888
+ if (line.kind !== "retail") return [line];
13889
+ const attemptedQuantity = attemptedRetail.get(line.variantId);
13890
+ if (attemptedQuantity === void 0) return [line];
13891
+ const healedQuantity = healedRetail.get(line.variantId);
13892
+ if (healedQuantity === void 0) return [];
13893
+ return [
13894
+ {
13895
+ ...line,
13896
+ quantity: line.quantity <= attemptedQuantity ? Math.min(line.quantity, healedQuantity) : Math.min(
13897
+ 99,
13898
+ healedQuantity + (line.quantity - attemptedQuantity)
13899
+ )
13900
+ }
13901
+ ];
13902
+ })
13903
+ }),
13904
+ async (proposed, signal) => {
13905
+ const payableLines = cartToWireLines({
13906
+ v: 1,
13907
+ lines: proposed.lines.filter((line) => line.kind === "retail")
13908
+ });
13909
+ const result = await client.post(
13910
+ "/api/store/quote",
13911
+ {
13912
+ lines: payableLines,
13913
+ shippingOptionId: fulfilment === "ship" ? nextShippingOptionId : null
13914
+ },
13915
+ { signal }
13916
+ );
13917
+ if (!result.ok) {
13918
+ outcome.quoteFailed = true;
13919
+ return false;
13920
+ }
13921
+ if (result.data.lines.length === 0 || result.data.problems.length > 0) {
13922
+ outcome.nextProblemQuote = result.data;
13923
+ outcome.nextProblemBaseline = proposed;
13924
+ return false;
13925
+ }
13926
+ const purchasedLines = snapshotPurchasedCartLines(
13927
+ proposed.lines,
13928
+ result.data.lines
13929
+ );
13930
+ if (!purchasedLines) return false;
13931
+ if (attemptToken) {
13932
+ const recovery = {
13933
+ v: 2,
13934
+ attemptToken,
13935
+ lines: purchasedLines
13936
+ };
13937
+ if (!writeCheckoutRecovery(recoveryKey, recovery, attemptToken))
13938
+ return false;
13939
+ recoveryRef.current = recovery;
13940
+ }
13941
+ outcome.confirmedQuote = result.data;
13942
+ return true;
13943
+ }
13944
+ );
13945
+ };
13946
+ const persisted = ownsCheckoutRef.current ? await continueWhileCoordinated() : await locks.request(
13947
+ `patientos-store-checkout:${recoveryKey}`,
13948
+ continueWhileCoordinated
13949
+ );
13950
+ if (!persisted || persisted.status === "cart_storage_unavailable") {
13951
+ storageUnavailable();
13952
+ return;
13953
+ }
13954
+ if (persisted.status === "not_committed") {
13955
+ if (outcome.quoteFailed) {
13956
+ setError("We couldn\u2019t load your checkout. Please try again.");
13957
+ setStage("quote_error");
13958
+ } else if (outcome.nextProblemQuote?.lines.length === 0) {
13959
+ setStage("empty");
13960
+ } else if (outcome.nextProblemQuote && outcome.nextProblemBaseline) {
13961
+ setPendingQuote(outcome.nextProblemQuote);
13962
+ setPendingQuoteBaseline(outcome.nextProblemBaseline);
13963
+ setStage("cart_changed");
13964
+ } else {
13965
+ storageUnavailable();
13966
+ }
13967
+ return;
13968
+ }
13969
+ if (optionUnavailable) setShippingOptionId(null);
13970
+ if (!outcome.confirmedQuote) {
13971
+ storageUnavailable();
13972
+ return;
13973
+ }
13974
+ checkoutCartSnapshotRef.current = persisted.cart.lines;
13975
+ setCheckoutLines(
13976
+ outcome.confirmedQuote.lines.map((line) => ({
13977
+ kind: "retail",
13978
+ variantId: line.variantId,
13979
+ quantity: line.quantity
13980
+ }))
13981
+ );
13982
+ setQuote(outcome.confirmedQuote);
13983
+ setStage("details");
13984
+ }
12619
13985
  if (stage === "loading") {
12620
13986
  return /* @__PURE__ */ jsxs27(Fragment14, { children: [
12621
13987
  /* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Checkout" }),
@@ -12633,7 +13999,69 @@ function CheckoutClient(_props) {
12633
13999
  return /* @__PURE__ */ jsxs27(Fragment14, { children: [
12634
14000
  /* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Checkout" }),
12635
14001
  /* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", children: "Please sign in to finish your order. Your order is kept with your clinic record so you can see it later." }),
12636
- /* @__PURE__ */ jsx30("a", { className: "sk-checkout__signin", href: "/portal/sign-in?redirect=/checkout", children: "Sign in to continue" })
14002
+ /* @__PURE__ */ jsx30(
14003
+ "a",
14004
+ {
14005
+ className: "sk-checkout__signin",
14006
+ href: client.signInHref("/checkout"),
14007
+ children: "Sign in to continue"
14008
+ }
14009
+ )
14010
+ ] });
14011
+ }
14012
+ if (stage === "unsupported") {
14013
+ return /* @__PURE__ */ jsxs27(Fragment14, { children: [
14014
+ /* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Update your cart" }),
14015
+ /* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "alert", children: error2 }),
14016
+ /* @__PURE__ */ jsx30("a", { className: "sk-checkout__link", href: "/cart", children: "Return to cart" })
14017
+ ] });
14018
+ }
14019
+ if (stage === "quote_error") {
14020
+ return /* @__PURE__ */ jsxs27(Fragment14, { children: [
14021
+ /* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: givenName ? `Checkout \u2014 hello, ${givenName}` : "Checkout" }),
14022
+ /* @__PURE__ */ jsx30("p", { className: "sk-checkout__error", role: "alert", children: error2 ?? "We couldn\u2019t load your checkout. Please try again." }),
14023
+ /* @__PURE__ */ jsx30(
14024
+ "button",
14025
+ {
14026
+ type: "button",
14027
+ className: "sk-checkout__pay",
14028
+ onClick: () => void retryQuote(),
14029
+ children: "Try again"
14030
+ }
14031
+ )
14032
+ ] });
14033
+ }
14034
+ if (stage === "cart_changed" && pendingQuote) {
14035
+ return /* @__PURE__ */ jsxs27(Fragment14, { children: [
14036
+ /* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Your cart was updated" }),
14037
+ /* @__PURE__ */ jsx30("div", { className: "sk-checkout__error", role: "alert", children: pendingQuote.problems.length > 0 ? /* @__PURE__ */ jsx30("ul", { children: pendingQuote.problems.map((problem, index) => /* @__PURE__ */ jsx30("li", { children: problem.message }, `${problem.code}-${index}`)) }) : /* @__PURE__ */ jsx30("p", { children: "Prices or delivery details changed while you were checking out." }) }),
14038
+ /* @__PURE__ */ jsx30(
14039
+ "button",
14040
+ {
14041
+ type: "button",
14042
+ className: "sk-checkout__pay",
14043
+ onClick: () => void acceptHealedQuote(),
14044
+ children: "Continue with updated cart"
14045
+ }
14046
+ )
14047
+ ] });
14048
+ }
14049
+ if (stage === "reconciliation_pending") {
14050
+ const persistedRecovery = readCheckoutRecovery(recoveryKey);
14051
+ const canRetry = persistedRecovery?.attemptToken === attemptTokenRef.current && persistedRecovery.lines !== null;
14052
+ return /* @__PURE__ */ jsxs27(Fragment14, { children: [
14053
+ /* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Payment received, cart update pending" }),
14054
+ /* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "alert", children: error2 }),
14055
+ canRetry ? /* @__PURE__ */ jsx30(
14056
+ "button",
14057
+ {
14058
+ type: "button",
14059
+ className: "sk-checkout__pay",
14060
+ onClick: () => void retryPaidReconciliation(),
14061
+ children: "Try updating cart again"
14062
+ }
14063
+ ) : /* @__PURE__ */ jsx30("a", { className: "sk-checkout__link", href: "/cart", children: "Review cart" }),
14064
+ /* @__PURE__ */ jsx30("a", { className: "sk-checkout__link", href: ordersHref, children: "View My orders" })
12637
14065
  ] });
12638
14066
  }
12639
14067
  if (stage === "paid") {
@@ -12642,11 +14070,130 @@ function CheckoutClient(_props) {
12642
14070
  /* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "status", children: "Taking you to your order\u2026" })
12643
14071
  ] });
12644
14072
  }
14073
+ if (stage === "storage_error") {
14074
+ return /* @__PURE__ */ jsxs27(Fragment14, { children: [
14075
+ /* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Checkout unavailable" }),
14076
+ /* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "alert", children: error2 })
14077
+ ] });
14078
+ }
14079
+ if (stage === "closed") {
14080
+ return /* @__PURE__ */ jsxs27(Fragment14, { children: [
14081
+ /* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Payment not completed" }),
14082
+ /* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "status", children: error2 }),
14083
+ /* @__PURE__ */ jsx30("a", { className: "sk-checkout__link", href: ordersHref, children: "Go to my orders" })
14084
+ ] });
14085
+ }
14086
+ if (stage === "resume" && resumableCheckout) {
14087
+ return /* @__PURE__ */ jsxs27(Fragment14, { children: [
14088
+ /* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Resume checkout" }),
14089
+ /* @__PURE__ */ jsxs27("p", { className: "sk-checkout__note", role: "status", children: [
14090
+ "Your checkout for ",
14091
+ formatMoney(resumableCheckout.amount),
14092
+ " is ready to continue."
14093
+ ] }),
14094
+ /* @__PURE__ */ jsx30(
14095
+ "button",
14096
+ {
14097
+ type: "button",
14098
+ className: "sk-checkout__pay",
14099
+ onClick: beginResume,
14100
+ children: "Resume checkout"
14101
+ }
14102
+ ),
14103
+ /* @__PURE__ */ jsx30("a", { className: "sk-checkout__link", href: ordersHref, children: "Go to my orders" })
14104
+ ] });
14105
+ }
14106
+ if (stage === "resume_payment" && resumableCheckout) {
14107
+ return /* @__PURE__ */ jsxs27(Fragment14, { children: [
14108
+ /* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Resume checkout" }),
14109
+ error2 ? /* @__PURE__ */ jsx30("p", { className: "sk-checkout__error", role: "alert", children: error2 }) : null,
14110
+ /* @__PURE__ */ jsxs27("p", { className: "sk-checkout__note", role: "status", children: [
14111
+ "Order total: ",
14112
+ formatMoney(resumableCheckout.amount)
14113
+ ] }),
14114
+ /* @__PURE__ */ jsxs27("form", { className: "sk-checkout__form", onSubmit: onResumePayment, children: [
14115
+ /* @__PURE__ */ jsxs27("fieldset", { className: "sk-checkout__fieldset", children: [
14116
+ /* @__PURE__ */ jsx30("legend", { children: "Card details" }),
14117
+ /* @__PURE__ */ jsxs27("label", { children: [
14118
+ /* @__PURE__ */ jsx30("span", { children: "Name on card" }),
14119
+ /* @__PURE__ */ jsx30(
14120
+ "input",
14121
+ {
14122
+ required: true,
14123
+ autoComplete: "cc-name",
14124
+ value: card.name,
14125
+ onChange: (e) => setCard({ ...card, name: e.target.value })
14126
+ }
14127
+ )
14128
+ ] }),
14129
+ /* @__PURE__ */ jsxs27("label", { children: [
14130
+ /* @__PURE__ */ jsx30("span", { children: "Card number" }),
14131
+ /* @__PURE__ */ jsx30(
14132
+ "input",
14133
+ {
14134
+ required: true,
14135
+ inputMode: "numeric",
14136
+ autoComplete: "cc-number",
14137
+ value: card.number,
14138
+ onChange: (e) => setCard({ ...card, number: e.target.value })
14139
+ }
14140
+ )
14141
+ ] }),
14142
+ /* @__PURE__ */ jsxs27("label", { children: [
14143
+ /* @__PURE__ */ jsx30("span", { children: "Expiry (MM/YY)" }),
14144
+ /* @__PURE__ */ jsx30(
14145
+ "input",
14146
+ {
14147
+ required: true,
14148
+ inputMode: "numeric",
14149
+ autoComplete: "cc-exp",
14150
+ placeholder: "MM/YY",
14151
+ value: card.expiry,
14152
+ onChange: (e) => setCard({ ...card, expiry: e.target.value })
14153
+ }
14154
+ )
14155
+ ] }),
14156
+ /* @__PURE__ */ jsxs27("label", { children: [
14157
+ /* @__PURE__ */ jsx30("span", { children: "Security code" }),
14158
+ /* @__PURE__ */ jsx30(
14159
+ "input",
14160
+ {
14161
+ required: true,
14162
+ inputMode: "numeric",
14163
+ autoComplete: "cc-csc",
14164
+ value: card.cvn,
14165
+ onChange: (e) => setCard({ ...card, cvn: e.target.value })
14166
+ }
14167
+ )
14168
+ ] }),
14169
+ /* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", children: "Your card details go straight to our payment provider \u2014 they never reach this clinic's systems." })
14170
+ ] }),
14171
+ /* @__PURE__ */ jsxs27("button", { type: "submit", className: "sk-checkout__pay", children: [
14172
+ "Pay ",
14173
+ formatMoney(resumableCheckout.amount)
14174
+ ] })
14175
+ ] })
14176
+ ] });
14177
+ }
14178
+ if (stage === "active_elsewhere") {
14179
+ return /* @__PURE__ */ jsxs27(Fragment14, { children: [
14180
+ /* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Checkout active elsewhere" }),
14181
+ /* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "status", children: "Finish checkout in your other tab, or close it and reload this page." }),
14182
+ /* @__PURE__ */ jsx30("a", { className: "sk-checkout__link", href: ordersHref, children: "Go to my orders" })
14183
+ ] });
14184
+ }
14185
+ if (stage === "active") {
14186
+ return /* @__PURE__ */ jsxs27(Fragment14, { children: [
14187
+ /* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Checkout is still opening" }),
14188
+ /* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "status", children: "Wait a moment, then reload this page before trying again." }),
14189
+ /* @__PURE__ */ jsx30("a", { className: "sk-checkout__link", href: ordersHref, children: "Go to my orders" })
14190
+ ] });
14191
+ }
12645
14192
  if (stage === "pending") {
12646
14193
  return /* @__PURE__ */ jsxs27(Fragment14, { children: [
12647
14194
  /* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Confirming your payment" }),
12648
- /* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "status", children: "Your bank hasn't confirmed the result yet. We're checking with them \u2014 please don't pay again. You'll see the order in your account once it's confirmed." }),
12649
- /* @__PURE__ */ jsx30("a", { className: "sk-checkout__link", href: "/portal/orders", children: "Go to my orders" })
14195
+ /* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "status", children: error2 ?? "Your bank hasn't confirmed the result yet. We're checking with them \u2014 please don't pay again. You'll see the order in your account once it's confirmed." }),
14196
+ /* @__PURE__ */ jsx30("a", { className: "sk-checkout__link", href: ordersHref, children: "Go to my orders" })
12650
14197
  ] });
12651
14198
  }
12652
14199
  if (stage === "three_ds" && iframeUrl) {
@@ -12658,16 +14205,23 @@ function CheckoutClient(_props) {
12658
14205
  ref: iframeElRef,
12659
14206
  className: "sk-checkout__3ds",
12660
14207
  src: iframeUrl,
14208
+ style: { visibility: "hidden", height: "1px" },
12661
14209
  title: "Card verification"
12662
14210
  }
12663
14211
  )
12664
14212
  ] });
12665
14213
  }
12666
14214
  const busy = stage === "paying";
14215
+ if (!quote) {
14216
+ return /* @__PURE__ */ jsxs27(Fragment14, { children: [
14217
+ /* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: givenName ? `Checkout \u2014 hello, ${givenName}` : "Checkout" }),
14218
+ /* @__PURE__ */ jsx30("p", { className: "sk-checkout__placeholder", role: "status", children: "Updating your order total\u2026" })
14219
+ ] });
14220
+ }
12667
14221
  return /* @__PURE__ */ jsxs27(Fragment14, { children: [
12668
14222
  /* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: givenName ? `Checkout \u2014 hello, ${givenName}` : "Checkout" }),
12669
14223
  error2 ? /* @__PURE__ */ jsx30("p", { className: "sk-checkout__error", role: "alert", children: error2 }) : null,
12670
- quote ? /* @__PURE__ */ jsxs27("div", { className: "sk-checkout__summary", children: [
14224
+ /* @__PURE__ */ jsxs27("div", { className: "sk-checkout__summary", children: [
12671
14225
  /* @__PURE__ */ jsx30("ul", { className: "sk-checkout__lines", children: quote.lines.map((l) => /* @__PURE__ */ jsxs27("li", { children: [
12672
14226
  /* @__PURE__ */ jsxs27("span", { children: [
12673
14227
  l.title,
@@ -12694,9 +14248,9 @@ function CheckoutClient(_props) {
12694
14248
  /* @__PURE__ */ jsx30("dd", { children: formatMoney(quote.total) })
12695
14249
  ] })
12696
14250
  ] })
12697
- ] }) : null,
14251
+ ] }),
12698
14252
  /* @__PURE__ */ jsxs27("form", { className: "sk-checkout__form", onSubmit: onPay, children: [
12699
- quote?.requiresShipping ? /* @__PURE__ */ jsxs27("fieldset", { className: "sk-checkout__fieldset", children: [
14253
+ quote.requiresShipping ? /* @__PURE__ */ jsxs27("fieldset", { className: "sk-checkout__fieldset", children: [
12700
14254
  /* @__PURE__ */ jsx30("legend", { children: "How would you like to get this?" }),
12701
14255
  /* @__PURE__ */ jsxs27("label", { children: [
12702
14256
  /* @__PURE__ */ jsx30(
@@ -12749,8 +14303,8 @@ function CheckoutClient(_props) {
12749
14303
  label: "Delivery address",
12750
14304
  value: address,
12751
14305
  onChange: setAddress,
12752
- search: searchStoreAddresses,
12753
- validate: validateStoreAddress
14306
+ search: (query) => searchStoreAddresses(query, client),
14307
+ validate: (value) => validateStoreAddress(value, client)
12754
14308
  }
12755
14309
  )
12756
14310
  ] }) : null
@@ -12811,7 +14365,7 @@ function CheckoutClient(_props) {
12811
14365
  ] }),
12812
14366
  /* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", children: "Your card details go straight to our payment provider \u2014 they never reach this clinic's systems." })
12813
14367
  ] }),
12814
- /* @__PURE__ */ jsx30("button", { type: "submit", className: "sk-checkout__pay", disabled: busy, children: busy ? "Processing\u2026" : quote ? `Pay ${formatMoney(quote.total)}` : "Pay" })
14368
+ /* @__PURE__ */ jsx30("button", { type: "submit", className: "sk-checkout__pay", disabled: busy, children: busy ? "Processing\u2026" : `Pay ${formatMoney(quote.total)}` })
12815
14369
  ] })
12816
14370
  ] });
12817
14371
  }
@@ -12831,6 +14385,7 @@ export {
12831
14385
  getThemeTokens,
12832
14386
  PortalAccount,
12833
14387
  PortalAccountClient,
14388
+ createStoreClient,
12834
14389
  StoreClient,
12835
14390
  CartClient,
12836
14391
  CheckoutClient