@patientos/website-kit 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -2
- package/dist/booking-block-D-UOAPx3.d.ts +74 -0
- package/dist/cart-block-B-ytc9us.d.ts +20 -0
- package/dist/cart-button-BCLFittB.d.ts +24 -0
- package/dist/cart-storage.d.ts +110 -0
- package/dist/cart-storage.js +39 -0
- package/dist/checkout-block-BeXwpqYe.d.ts +24 -0
- package/dist/chunk-2CZFJOKK.js +79 -0
- package/dist/{chunk-ZYX4TXBN.js → chunk-3T7UWKAH.js} +4 -310
- package/dist/{chunk-7ZOQ6UKG.js → chunk-4GKSSM5N.js} +578 -3163
- package/dist/chunk-4NF7SSIX.js +367 -0
- package/dist/chunk-7BUDUYXN.js +80 -0
- package/dist/chunk-FZ4WKWIE.js +310 -0
- package/dist/{chunk-ZZFBTLR4.js → chunk-HTVKKLWI.js} +20 -5
- package/dist/chunk-IOC6QLB7.js +159 -0
- package/dist/chunk-S3ZQQOQT.js +2057 -0
- package/dist/chunk-UTAMB2SK.js +214 -0
- package/dist/chunk-WQSJ46TP.js +19 -0
- package/dist/chunk-ZNH6TSYP.js +167 -0
- package/dist/index.d.ts +12 -32
- package/dist/index.js +88 -26
- package/dist/islands-impl/cart-button.d.ts +19 -0
- package/dist/islands-impl/cart-button.js +13 -0
- package/dist/islands-impl/cart.d.ts +31 -0
- package/dist/islands-impl/cart.js +11 -0
- package/dist/islands-impl/checkout.d.ts +6 -0
- package/dist/islands-impl/checkout.js +13 -0
- package/dist/islands-impl/store.d.ts +30 -0
- package/dist/islands-impl/store.js +16 -0
- package/dist/islands-impl.d.ts +14 -10
- package/dist/islands-impl.js +22 -7
- package/dist/islands-registry.js +13 -4
- package/dist/{portal-account-8uQc_Ncx.d.ts → portal-account-DRJyr3nz.d.ts} +1 -82
- package/dist/{portal-account.client-BBe_q9yC.d.ts → portal-account.client-Bke4NsgW.d.ts} +2 -1
- package/dist/portal-booking-client.d.ts +3 -2
- package/dist/portal-booking-client.js +2 -1
- package/dist/portal-client-my3q5GME.d.ts +83 -0
- package/dist/store-block-CFxnsfKU.d.ts +37 -0
- package/dist/store-catalog-p7TFOpfk.d.ts +78 -0
- package/dist/store-catalog.d.ts +3 -0
- package/dist/store-catalog.js +11 -0
- package/dist/website-kit.css +193 -0
- package/package.json +31 -1
- package/dist/checkout-block-BdpPIuQ5.d.ts +0 -136
|
@@ -0,0 +1,2057 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BPOINT_SCRIPT_URL
|
|
3
|
+
} from "./chunk-ZOF22TJA.js";
|
|
4
|
+
import {
|
|
5
|
+
createStoreClient,
|
|
6
|
+
formatMoney
|
|
7
|
+
} from "./chunk-7BUDUYXN.js";
|
|
8
|
+
import {
|
|
9
|
+
cartToWireLines,
|
|
10
|
+
mutateCart,
|
|
11
|
+
normalizeCartLines,
|
|
12
|
+
readCart,
|
|
13
|
+
subtractPurchasedCartLines,
|
|
14
|
+
withCartLock
|
|
15
|
+
} from "./chunk-4NF7SSIX.js";
|
|
16
|
+
|
|
17
|
+
// src/checkout-block.client.tsx
|
|
18
|
+
import * as React from "react";
|
|
19
|
+
|
|
20
|
+
// src/address-picker.tsx
|
|
21
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
22
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
23
|
+
var DEBOUNCE_MS = 300;
|
|
24
|
+
var MIN_QUERY = 3;
|
|
25
|
+
function nextActiveIndex(current, count, delta) {
|
|
26
|
+
if (count <= 0) return -1;
|
|
27
|
+
const next = current + delta;
|
|
28
|
+
if (next < 0) return count - 1;
|
|
29
|
+
if (next >= count) return 0;
|
|
30
|
+
return next;
|
|
31
|
+
}
|
|
32
|
+
function AddressPicker({
|
|
33
|
+
value,
|
|
34
|
+
onChange,
|
|
35
|
+
search,
|
|
36
|
+
validate,
|
|
37
|
+
idPrefix,
|
|
38
|
+
disabled,
|
|
39
|
+
error,
|
|
40
|
+
label = "Address"
|
|
41
|
+
}) {
|
|
42
|
+
const [query, setQuery] = useState("");
|
|
43
|
+
const [suggestions, setSuggestions] = useState([]);
|
|
44
|
+
const [active, setActive] = useState(-1);
|
|
45
|
+
const [open, setOpen] = useState(false);
|
|
46
|
+
const [busy, setBusy] = useState(false);
|
|
47
|
+
const [checking, setChecking] = useState(false);
|
|
48
|
+
const [refused, setRefused] = useState(null);
|
|
49
|
+
const live = useRef({ search, validate });
|
|
50
|
+
live.current = { search, validate };
|
|
51
|
+
const seq = useRef(0);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const q = query.trim();
|
|
54
|
+
setRefused(null);
|
|
55
|
+
if (q.length < MIN_QUERY) {
|
|
56
|
+
setSuggestions([]);
|
|
57
|
+
setOpen(false);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
setBusy(true);
|
|
61
|
+
const mine = ++seq.current;
|
|
62
|
+
const timer = setTimeout(() => {
|
|
63
|
+
live.current.search(q).then((next) => {
|
|
64
|
+
if (mine !== seq.current) return;
|
|
65
|
+
setSuggestions(next);
|
|
66
|
+
setActive(-1);
|
|
67
|
+
setOpen(next.length > 0);
|
|
68
|
+
}).catch(() => {
|
|
69
|
+
if (mine !== seq.current) return;
|
|
70
|
+
setSuggestions([]);
|
|
71
|
+
setOpen(false);
|
|
72
|
+
}).finally(() => {
|
|
73
|
+
if (mine === seq.current) setBusy(false);
|
|
74
|
+
});
|
|
75
|
+
}, DEBOUNCE_MS);
|
|
76
|
+
return () => clearTimeout(timer);
|
|
77
|
+
}, [query]);
|
|
78
|
+
const choose = useCallback(
|
|
79
|
+
(s) => {
|
|
80
|
+
onChange({ address: s.label, placeId: s.placeId });
|
|
81
|
+
setQuery("");
|
|
82
|
+
setSuggestions([]);
|
|
83
|
+
setOpen(false);
|
|
84
|
+
},
|
|
85
|
+
[onChange]
|
|
86
|
+
);
|
|
87
|
+
const checkTyped = useCallback(async () => {
|
|
88
|
+
const address = query.trim();
|
|
89
|
+
if (!address || checking) return;
|
|
90
|
+
setChecking(true);
|
|
91
|
+
setRefused(null);
|
|
92
|
+
try {
|
|
93
|
+
const confirmed = await live.current.validate(address);
|
|
94
|
+
if (!confirmed) {
|
|
95
|
+
setRefused("We couldn\u2019t verify that address. Check it and try again.");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
onChange(confirmed);
|
|
99
|
+
setQuery("");
|
|
100
|
+
setSuggestions([]);
|
|
101
|
+
setOpen(false);
|
|
102
|
+
} catch {
|
|
103
|
+
setRefused("We couldn\u2019t verify that address. Check it and try again.");
|
|
104
|
+
} finally {
|
|
105
|
+
setChecking(false);
|
|
106
|
+
}
|
|
107
|
+
}, [query, checking, onChange]);
|
|
108
|
+
const inputId = `${idPrefix}-address`;
|
|
109
|
+
const errorId = `${idPrefix}-address-error`;
|
|
110
|
+
const listId = `${idPrefix}-address-list`;
|
|
111
|
+
if (value) {
|
|
112
|
+
return /* @__PURE__ */ jsxs("div", { className: "sk-address sk-address--picked", children: [
|
|
113
|
+
/* @__PURE__ */ jsx("span", { className: "sk-address__label", children: label }),
|
|
114
|
+
/* @__PURE__ */ jsx("p", { className: "sk-address__chosen", children: value.address }),
|
|
115
|
+
/* @__PURE__ */ jsx(
|
|
116
|
+
"button",
|
|
117
|
+
{
|
|
118
|
+
type: "button",
|
|
119
|
+
className: "sk-address__change",
|
|
120
|
+
disabled,
|
|
121
|
+
onClick: () => onChange(null),
|
|
122
|
+
children: "Change"
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
] });
|
|
126
|
+
}
|
|
127
|
+
function onKeyDown(e) {
|
|
128
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
129
|
+
if (!open || suggestions.length === 0) return;
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
setActive((i) => nextActiveIndex(i, suggestions.length, e.key === "ArrowDown" ? 1 : -1));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (e.key === "Escape" && open) {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
setOpen(false);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (e.key === "Enter") {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
const chosen = open && active >= 0 ? suggestions[active] : void 0;
|
|
142
|
+
if (chosen) choose(chosen);
|
|
143
|
+
else if (suggestions.length === 0) void checkTyped();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const showNoMatches = query.trim().length >= MIN_QUERY && !busy && suggestions.length === 0;
|
|
147
|
+
const status = checking ? "Checking that address\u2026" : busy ? "Searching\u2026" : open && suggestions.length > 0 ? `${suggestions.length} ${suggestions.length === 1 ? "address" : "addresses"} found. Use the arrow keys to review them.` : showNoMatches ? "No matches for that search." : "";
|
|
148
|
+
return /* @__PURE__ */ jsxs("div", { className: "sk-address", children: [
|
|
149
|
+
/* @__PURE__ */ jsx("label", { className: "sk-address__label", htmlFor: inputId, children: label }),
|
|
150
|
+
/* @__PURE__ */ jsxs("div", { className: "sk-address__box", children: [
|
|
151
|
+
/* @__PURE__ */ jsx(
|
|
152
|
+
"input",
|
|
153
|
+
{
|
|
154
|
+
id: inputId,
|
|
155
|
+
type: "text",
|
|
156
|
+
role: "combobox",
|
|
157
|
+
className: "sk-address__input",
|
|
158
|
+
autoComplete: "off",
|
|
159
|
+
"aria-expanded": open,
|
|
160
|
+
"aria-controls": listId,
|
|
161
|
+
"aria-autocomplete": "list",
|
|
162
|
+
"aria-activedescendant": active >= 0 ? `${listId}-${active}` : void 0,
|
|
163
|
+
"aria-busy": busy || void 0,
|
|
164
|
+
"aria-invalid": error || refused ? true : void 0,
|
|
165
|
+
"aria-describedby": error || refused ? errorId : void 0,
|
|
166
|
+
placeholder: "Start typing your address\u2026",
|
|
167
|
+
disabled,
|
|
168
|
+
value: query,
|
|
169
|
+
onChange: (e) => setQuery(e.target.value),
|
|
170
|
+
onKeyDown
|
|
171
|
+
}
|
|
172
|
+
),
|
|
173
|
+
open && /* @__PURE__ */ jsx(
|
|
174
|
+
"ul",
|
|
175
|
+
{
|
|
176
|
+
className: "sk-address__list",
|
|
177
|
+
id: listId,
|
|
178
|
+
role: "listbox",
|
|
179
|
+
"aria-label": "Address suggestions",
|
|
180
|
+
children: suggestions.map((s, i) => /* @__PURE__ */ jsx(
|
|
181
|
+
"li",
|
|
182
|
+
{
|
|
183
|
+
id: `${listId}-${i}`,
|
|
184
|
+
role: "option",
|
|
185
|
+
"aria-selected": i === active,
|
|
186
|
+
className: ["sk-address__option", i === active ? "sk-address__option--active" : null].filter(Boolean).join(" "),
|
|
187
|
+
onMouseDown: (e) => {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
choose(s);
|
|
190
|
+
},
|
|
191
|
+
children: s.label
|
|
192
|
+
},
|
|
193
|
+
s.placeId
|
|
194
|
+
))
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
] }),
|
|
198
|
+
showNoMatches && /* @__PURE__ */ jsxs("div", { className: "sk-address__empty", children: [
|
|
199
|
+
/* @__PURE__ */ jsx("p", { children: "No matches for that search." }),
|
|
200
|
+
/* @__PURE__ */ jsx(
|
|
201
|
+
"button",
|
|
202
|
+
{
|
|
203
|
+
type: "button",
|
|
204
|
+
className: "sk-address__check",
|
|
205
|
+
disabled: checking,
|
|
206
|
+
onClick: () => void checkTyped(),
|
|
207
|
+
children: checking ? "Checking\u2026" : "Check this address"
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
] }),
|
|
211
|
+
(error || refused) && /* @__PURE__ */ jsx("p", { className: "sk-address__error", id: errorId, children: refused ?? error }),
|
|
212
|
+
/* @__PURE__ */ jsx("p", { className: "sk-address__status", role: "status", "aria-live": "polite", children: status })
|
|
213
|
+
] });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/checkout-attempt-token.ts
|
|
217
|
+
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;
|
|
218
|
+
function isCheckoutAttemptToken(value) {
|
|
219
|
+
return typeof value === "string" && CHECKOUT_ATTEMPT_TOKEN.test(value);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/checkout-completion.ts
|
|
223
|
+
function checkoutCompletionUrl(configuredHref, orderId, siteOrigin) {
|
|
224
|
+
const fallback = new URL("/checkout/complete", siteOrigin);
|
|
225
|
+
let destination;
|
|
226
|
+
try {
|
|
227
|
+
destination = new URL(configuredHref || fallback.pathname, siteOrigin);
|
|
228
|
+
} catch {
|
|
229
|
+
destination = fallback;
|
|
230
|
+
}
|
|
231
|
+
if (destination.origin !== fallback.origin || destination.protocol !== "http:" && destination.protocol !== "https:") {
|
|
232
|
+
destination = fallback;
|
|
233
|
+
}
|
|
234
|
+
destination.searchParams.set("order", orderId);
|
|
235
|
+
return `${destination.pathname}${destination.search}${destination.hash}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/bpoint-client.ts
|
|
239
|
+
function bpoint() {
|
|
240
|
+
return globalThis.BPOINT;
|
|
241
|
+
}
|
|
242
|
+
var loadPromise = null;
|
|
243
|
+
var SCRIPT_TIMEOUT_MS = 15e3;
|
|
244
|
+
var CALLBACK_TIMEOUT_MS = 3e4;
|
|
245
|
+
function loadBpointScript() {
|
|
246
|
+
if (loadPromise) return loadPromise;
|
|
247
|
+
loadPromise = new Promise((resolve, reject) => {
|
|
248
|
+
if (typeof window !== "undefined" && bpoint()) {
|
|
249
|
+
resolve();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const script = document.createElement("script");
|
|
253
|
+
script.src = BPOINT_SCRIPT_URL;
|
|
254
|
+
script.type = "text/javascript";
|
|
255
|
+
const timer = setTimeout(
|
|
256
|
+
() => reject(new Error("The payment gateway did not load. Please try again.")),
|
|
257
|
+
SCRIPT_TIMEOUT_MS
|
|
258
|
+
);
|
|
259
|
+
script.onload = () => {
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
resolve();
|
|
262
|
+
};
|
|
263
|
+
script.onerror = () => {
|
|
264
|
+
clearTimeout(timer);
|
|
265
|
+
reject(new Error("Failed to load BPOINT script"));
|
|
266
|
+
};
|
|
267
|
+
document.head.appendChild(script);
|
|
268
|
+
});
|
|
269
|
+
loadPromise = loadPromise.catch((err) => {
|
|
270
|
+
loadPromise = null;
|
|
271
|
+
throw err;
|
|
272
|
+
});
|
|
273
|
+
return loadPromise;
|
|
274
|
+
}
|
|
275
|
+
async function attachCardToAuthKey(authKey, card) {
|
|
276
|
+
await loadBpointScript();
|
|
277
|
+
const digits = card.number.replace(/\D/g, "");
|
|
278
|
+
const exp = card.expiry.replace(/\D/g, "");
|
|
279
|
+
const api = bpoint();
|
|
280
|
+
if (!api) throw new Error("The payment gateway did not load. Please try again.");
|
|
281
|
+
return new Promise((resolve, reject) => {
|
|
282
|
+
const timer = setTimeout(
|
|
283
|
+
() => reject(new Error("The payment gateway did not respond. Please try again.")),
|
|
284
|
+
CALLBACK_TIMEOUT_MS
|
|
285
|
+
);
|
|
286
|
+
api.txn.authkey.attachPaymentMethod(
|
|
287
|
+
authKey,
|
|
288
|
+
{
|
|
289
|
+
card: {
|
|
290
|
+
number: digits,
|
|
291
|
+
expiry: { month: exp.slice(0, 2), year: exp.slice(2, 4) },
|
|
292
|
+
name: card.name.trim(),
|
|
293
|
+
cvn: card.cvn
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
(code) => {
|
|
297
|
+
clearTimeout(timer);
|
|
298
|
+
if (code === "success") resolve();
|
|
299
|
+
else
|
|
300
|
+
reject(
|
|
301
|
+
new Error(
|
|
302
|
+
"We couldn't validate your card. Check the number, expiry and security code, then try again."
|
|
303
|
+
)
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/checkout-block.client.tsx
|
|
311
|
+
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
312
|
+
var BOTH_FULFILMENT_METHODS = { pickup: true, delivery: true };
|
|
313
|
+
function readFulfilmentPolicy(value) {
|
|
314
|
+
if (!value || typeof value !== "object") return BOTH_FULFILMENT_METHODS;
|
|
315
|
+
const raw = value;
|
|
316
|
+
return {
|
|
317
|
+
pickup: raw.pickup !== false,
|
|
318
|
+
delivery: raw.delivery !== false
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function formatSavedAddress(a) {
|
|
322
|
+
const locality = [a.suburb, a.state, a.postcode].map((part) => (part ?? "").trim()).filter(Boolean).join(" ");
|
|
323
|
+
return [(a.line1 ?? "").trim(), (a.line2 ?? "").trim(), locality].filter(Boolean).join(", ");
|
|
324
|
+
}
|
|
325
|
+
var CHECKOUT_RECOVERY_PREFIX = "patientos.store.checkout-order.v1";
|
|
326
|
+
function checkoutRecoveryKey(apiOrigin) {
|
|
327
|
+
const identity = apiOrigin || (typeof window === "undefined" ? "same-origin" : window.location.origin);
|
|
328
|
+
let hash = 2166136261;
|
|
329
|
+
for (let index = 0; index < identity.length; index += 1) {
|
|
330
|
+
hash ^= identity.charCodeAt(index);
|
|
331
|
+
hash = Math.imul(hash, 16777619);
|
|
332
|
+
}
|
|
333
|
+
return `${CHECKOUT_RECOVERY_PREFIX}:${(hash >>> 0).toString(16)}`;
|
|
334
|
+
}
|
|
335
|
+
function discardCheckoutRecovery(key) {
|
|
336
|
+
try {
|
|
337
|
+
window.localStorage.removeItem(key);
|
|
338
|
+
} catch {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function readCheckoutRecovery(key) {
|
|
343
|
+
try {
|
|
344
|
+
const value = window.localStorage.getItem(key);
|
|
345
|
+
if (!value) return null;
|
|
346
|
+
if (isCheckoutAttemptToken(value))
|
|
347
|
+
return { v: 2, attemptToken: value, lines: null };
|
|
348
|
+
const parsed = JSON.parse(value);
|
|
349
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
350
|
+
discardCheckoutRecovery(key);
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
const record = parsed;
|
|
354
|
+
if (record.v !== 1 && record.v !== 2 || !isCheckoutAttemptToken(record.attemptToken)) {
|
|
355
|
+
discardCheckoutRecovery(key);
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
if (record.v !== 2 || !Array.isArray(record.lines)) {
|
|
359
|
+
return { v: 2, attemptToken: record.attemptToken, lines: null };
|
|
360
|
+
}
|
|
361
|
+
const rawLines = record.lines;
|
|
362
|
+
const lines = normalizeCartLines(rawLines);
|
|
363
|
+
if (lines.length === 0 || lines.some(
|
|
364
|
+
(line) => !rawLines.some(
|
|
365
|
+
(candidate) => candidate !== null && typeof candidate === "object" && !Array.isArray(candidate) && candidate.lineId === line.lineId
|
|
366
|
+
)
|
|
367
|
+
)) {
|
|
368
|
+
return { v: 2, attemptToken: record.attemptToken, lines: null };
|
|
369
|
+
}
|
|
370
|
+
return { v: 2, attemptToken: record.attemptToken, lines };
|
|
371
|
+
} catch {
|
|
372
|
+
discardCheckoutRecovery(key);
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function sameRecoveryRecord(left, right) {
|
|
377
|
+
return left?.attemptToken === right.attemptToken && left.lines !== null && right.lines !== null && JSON.stringify(left.lines) === JSON.stringify(right.lines);
|
|
378
|
+
}
|
|
379
|
+
function writeCheckoutRecovery(key, record, expectedToken) {
|
|
380
|
+
try {
|
|
381
|
+
const existing = readCheckoutRecovery(key);
|
|
382
|
+
if ((existing?.attemptToken ?? null) !== expectedToken) return false;
|
|
383
|
+
window.localStorage.setItem(key, JSON.stringify(record));
|
|
384
|
+
return sameRecoveryRecord(readCheckoutRecovery(key), record);
|
|
385
|
+
} catch {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function removeCheckoutRecovery(key, attemptToken) {
|
|
390
|
+
try {
|
|
391
|
+
const existing = readCheckoutRecovery(key);
|
|
392
|
+
if (!existing) return true;
|
|
393
|
+
if (existing.attemptToken !== attemptToken) return false;
|
|
394
|
+
window.localStorage.removeItem(key);
|
|
395
|
+
return readCheckoutRecovery(key)?.attemptToken !== attemptToken;
|
|
396
|
+
} catch {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function snapshotPurchasedCartLines(cartLines, quotedLines) {
|
|
401
|
+
const cartByVariant = new Map(
|
|
402
|
+
cartLines.filter((line) => line.kind === "retail").map((line) => [line.variantId, line])
|
|
403
|
+
);
|
|
404
|
+
const snapshot = [];
|
|
405
|
+
for (const quoted of quotedLines) {
|
|
406
|
+
const line = cartByVariant.get(quoted.variantId);
|
|
407
|
+
if (!line) return null;
|
|
408
|
+
snapshot.push({ ...line, quantity: quoted.quantity });
|
|
409
|
+
}
|
|
410
|
+
return snapshot.length > 0 ? snapshot : null;
|
|
411
|
+
}
|
|
412
|
+
function sameCartLineSnapshot(left, right) {
|
|
413
|
+
if (left.length !== right.length) return false;
|
|
414
|
+
const rightById = new Map(right.map((line) => [line.lineId, line]));
|
|
415
|
+
return left.every((line) => {
|
|
416
|
+
const expected = rightById.get(line.lineId);
|
|
417
|
+
return expected?.kind === line.kind && expected.variantId === line.variantId && expected.quantity === line.quantity;
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
function samePayableQuote(left, right, shippingOptionId) {
|
|
421
|
+
return JSON.stringify(expectedQuote(left, shippingOptionId)) === JSON.stringify(expectedQuote(right, shippingOptionId));
|
|
422
|
+
}
|
|
423
|
+
function expectedQuote(quote, shippingOptionId) {
|
|
424
|
+
return {
|
|
425
|
+
shippingOptionId,
|
|
426
|
+
lines: quote.lines.map(({ variantId, quantity, unitPrice, lineTotal }) => ({
|
|
427
|
+
variantId,
|
|
428
|
+
quantity,
|
|
429
|
+
unitPrice,
|
|
430
|
+
lineTotal
|
|
431
|
+
})),
|
|
432
|
+
subtotal: quote.subtotal,
|
|
433
|
+
shippingTotal: quote.shippingTotal,
|
|
434
|
+
taxTotal: quote.taxTotal,
|
|
435
|
+
total: quote.total,
|
|
436
|
+
currency: quote.currency
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
var StoreRequestError = class extends Error {
|
|
440
|
+
constructor(message, code) {
|
|
441
|
+
super(message);
|
|
442
|
+
this.code = code;
|
|
443
|
+
this.name = "StoreRequestError";
|
|
444
|
+
}
|
|
445
|
+
code;
|
|
446
|
+
};
|
|
447
|
+
async function searchStoreAddresses(query, client) {
|
|
448
|
+
const q = query.trim();
|
|
449
|
+
if (q.length < 3) return [];
|
|
450
|
+
const result = await client.get(`/api/address/autocomplete?q=${encodeURIComponent(q)}`);
|
|
451
|
+
return result.ok ? result.data.suggestions ?? [] : [];
|
|
452
|
+
}
|
|
453
|
+
async function validateStoreAddress(address, client) {
|
|
454
|
+
const result = await client.post("/api/address/validate", { address });
|
|
455
|
+
if (!result.ok || result.data.error || !result.data.formattedAddress)
|
|
456
|
+
return null;
|
|
457
|
+
return {
|
|
458
|
+
address: result.data.formattedAddress,
|
|
459
|
+
placeId: result.data.googlePlaceId ?? null
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function CheckoutClient({
|
|
463
|
+
storeApiOrigin,
|
|
464
|
+
ordersHref = "/portal/orders",
|
|
465
|
+
completionHref = "/checkout/complete"
|
|
466
|
+
}) {
|
|
467
|
+
const client = React.useMemo(
|
|
468
|
+
() => createStoreClient({ apiOrigin: storeApiOrigin }),
|
|
469
|
+
[storeApiOrigin]
|
|
470
|
+
);
|
|
471
|
+
const [stage, setStage] = React.useState("loading");
|
|
472
|
+
const [givenName, setGivenName] = React.useState(null);
|
|
473
|
+
const [cart] = React.useState(() => readCart());
|
|
474
|
+
const recoveryKey = React.useMemo(
|
|
475
|
+
() => checkoutRecoveryKey(client.apiOrigin),
|
|
476
|
+
[client]
|
|
477
|
+
);
|
|
478
|
+
const [initialRecovery] = React.useState(
|
|
479
|
+
() => readCheckoutRecovery(recoveryKey)
|
|
480
|
+
);
|
|
481
|
+
const recoveryRef = React.useRef(
|
|
482
|
+
initialRecovery
|
|
483
|
+
);
|
|
484
|
+
const attemptTokenRef = React.useRef(
|
|
485
|
+
initialRecovery?.attemptToken ?? null
|
|
486
|
+
);
|
|
487
|
+
const [checkoutLines, setCheckoutLines] = React.useState(
|
|
488
|
+
() => cartToWireLines(cart)
|
|
489
|
+
);
|
|
490
|
+
const checkoutCartSnapshotRef = React.useRef(cart.lines);
|
|
491
|
+
const [quote, setQuote] = React.useState(null);
|
|
492
|
+
const [pendingQuote, setPendingQuote] = React.useState(null);
|
|
493
|
+
const [pendingQuoteBaseline, setPendingQuoteBaseline] = React.useState(null);
|
|
494
|
+
const [shippingOptions, setShippingOptions] = React.useState([]);
|
|
495
|
+
const [error, setError] = React.useState(null);
|
|
496
|
+
const [resumableCheckout, setResumableCheckout] = React.useState(null);
|
|
497
|
+
const [fulfilmentPolicy, setFulfilmentPolicy] = React.useState(
|
|
498
|
+
BOTH_FULFILMENT_METHODS
|
|
499
|
+
);
|
|
500
|
+
const [fulfilment, setFulfilment] = React.useState(
|
|
501
|
+
"pickup"
|
|
502
|
+
);
|
|
503
|
+
const [savedAddresses, setSavedAddresses] = React.useState(null);
|
|
504
|
+
const [savedAddressId, setSavedAddressId] = React.useState(
|
|
505
|
+
null
|
|
506
|
+
);
|
|
507
|
+
const [addingAddress, setAddingAddress] = React.useState(false);
|
|
508
|
+
const [savingAddress, setSavingAddress] = React.useState(false);
|
|
509
|
+
const [addressBookError, setAddressBookError] = React.useState(
|
|
510
|
+
null
|
|
511
|
+
);
|
|
512
|
+
const addressBookRequested = React.useRef(false);
|
|
513
|
+
const usingSavedAddress = !addingAddress && savedAddresses !== null && savedAddressId !== null;
|
|
514
|
+
const offersBothMethods = fulfilmentPolicy.pickup && fulfilmentPolicy.delivery;
|
|
515
|
+
const offersEitherMethod = fulfilmentPolicy.pickup || fulfilmentPolicy.delivery;
|
|
516
|
+
const [shippingOptionId, setShippingOptionId] = React.useState(
|
|
517
|
+
null
|
|
518
|
+
);
|
|
519
|
+
const [address, setAddress] = React.useState(null);
|
|
520
|
+
const [card, setCard] = React.useState({
|
|
521
|
+
number: "",
|
|
522
|
+
expiry: "",
|
|
523
|
+
name: "",
|
|
524
|
+
cvn: ""
|
|
525
|
+
});
|
|
526
|
+
const orderRef = React.useRef(null);
|
|
527
|
+
const [iframeUrl, setIframeUrl] = React.useState(null);
|
|
528
|
+
const iframeOriginRef = React.useRef(null);
|
|
529
|
+
const iframeElRef = React.useRef(null);
|
|
530
|
+
const ownsCheckoutRef = React.useRef(false);
|
|
531
|
+
const ownershipClaimRef = React.useRef(null);
|
|
532
|
+
const releaseCheckoutOwnershipRef = React.useRef(null);
|
|
533
|
+
async function claimCheckoutOwnership() {
|
|
534
|
+
if (ownsCheckoutRef.current) return true;
|
|
535
|
+
if (ownershipClaimRef.current) return ownershipClaimRef.current;
|
|
536
|
+
const locks = navigator.locks;
|
|
537
|
+
if (!locks) return false;
|
|
538
|
+
const claim = new Promise((resolveClaim) => {
|
|
539
|
+
let resolveHold = null;
|
|
540
|
+
const hold = new Promise((resolve) => {
|
|
541
|
+
resolveHold = resolve;
|
|
542
|
+
});
|
|
543
|
+
void locks.request(
|
|
544
|
+
`patientos-store-checkout:${recoveryKey}`,
|
|
545
|
+
{ mode: "exclusive", ifAvailable: true },
|
|
546
|
+
async (lock) => {
|
|
547
|
+
if (!lock) {
|
|
548
|
+
resolveClaim(false);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
ownsCheckoutRef.current = true;
|
|
552
|
+
releaseCheckoutOwnershipRef.current = () => {
|
|
553
|
+
if (!ownsCheckoutRef.current) return;
|
|
554
|
+
ownsCheckoutRef.current = false;
|
|
555
|
+
releaseCheckoutOwnershipRef.current = null;
|
|
556
|
+
resolveHold?.();
|
|
557
|
+
};
|
|
558
|
+
resolveClaim(true);
|
|
559
|
+
await hold;
|
|
560
|
+
}
|
|
561
|
+
).catch(() => resolveClaim(false));
|
|
562
|
+
});
|
|
563
|
+
ownershipClaimRef.current = claim;
|
|
564
|
+
const acquired = await claim;
|
|
565
|
+
ownershipClaimRef.current = null;
|
|
566
|
+
return acquired;
|
|
567
|
+
}
|
|
568
|
+
function releaseCheckoutOwnership() {
|
|
569
|
+
releaseCheckoutOwnershipRef.current?.();
|
|
570
|
+
}
|
|
571
|
+
function goToCompletion(orderId) {
|
|
572
|
+
window.location.assign(
|
|
573
|
+
checkoutCompletionUrl(completionHref, orderId, window.location.origin)
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
React.useEffect(
|
|
577
|
+
() => () => {
|
|
578
|
+
releaseCheckoutOwnership();
|
|
579
|
+
},
|
|
580
|
+
[]
|
|
581
|
+
);
|
|
582
|
+
function releaseOwnedAttempt(attemptToken) {
|
|
583
|
+
const removed = removeCheckoutRecovery(recoveryKey, attemptToken);
|
|
584
|
+
if (removed && recoveryRef.current?.attemptToken === attemptToken) {
|
|
585
|
+
recoveryRef.current = null;
|
|
586
|
+
attemptTokenRef.current = null;
|
|
587
|
+
}
|
|
588
|
+
return removed;
|
|
589
|
+
}
|
|
590
|
+
async function finishPaidAttemptWhileLocked(attemptToken) {
|
|
591
|
+
const stored = readCheckoutRecovery(recoveryKey);
|
|
592
|
+
if (stored?.attemptToken !== attemptToken || stored.lines === null)
|
|
593
|
+
return "snapshot_unavailable";
|
|
594
|
+
const settled = await subtractPurchasedCartLines(
|
|
595
|
+
stored.lines,
|
|
596
|
+
attemptToken
|
|
597
|
+
);
|
|
598
|
+
if (!settled.persisted) return "storage_unavailable";
|
|
599
|
+
return releaseOwnedAttempt(attemptToken) ? "settled" : "storage_unavailable";
|
|
600
|
+
}
|
|
601
|
+
async function finishPaidAttempt(attemptToken) {
|
|
602
|
+
if (ownsCheckoutRef.current) {
|
|
603
|
+
const result = await finishPaidAttemptWhileLocked(attemptToken);
|
|
604
|
+
if (result === "settled") releaseCheckoutOwnership();
|
|
605
|
+
return result;
|
|
606
|
+
}
|
|
607
|
+
const locks = navigator.locks;
|
|
608
|
+
if (!locks) return "storage_unavailable";
|
|
609
|
+
return locks.request(
|
|
610
|
+
`patientos-store-checkout:${recoveryKey}`,
|
|
611
|
+
() => finishPaidAttemptWhileLocked(attemptToken)
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
function paidReconciliationPending(result) {
|
|
615
|
+
setError(
|
|
616
|
+
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."
|
|
617
|
+
);
|
|
618
|
+
setStage("reconciliation_pending");
|
|
619
|
+
}
|
|
620
|
+
async function completePaidAttempt(attemptToken, orderId, alreadyLocked = false) {
|
|
621
|
+
const result = alreadyLocked ? await finishPaidAttemptWhileLocked(attemptToken) : await finishPaidAttempt(attemptToken);
|
|
622
|
+
if (result !== "settled") {
|
|
623
|
+
paidReconciliationPending(result);
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
if (alreadyLocked) releaseCheckoutOwnership();
|
|
627
|
+
setStage("paid");
|
|
628
|
+
goToCompletion(orderId);
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
async function retryPaidReconciliation() {
|
|
632
|
+
const attemptToken = attemptTokenRef.current;
|
|
633
|
+
if (!attemptToken) return;
|
|
634
|
+
await completePaidAttempt(
|
|
635
|
+
attemptToken,
|
|
636
|
+
orderRef.current?.orderId ?? attemptToken
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
function recoverySnapshotUnavailable() {
|
|
640
|
+
releaseCheckoutOwnership();
|
|
641
|
+
setError(
|
|
642
|
+
"This checkout can\u2019t be safely continued in this browser. Review My orders and your cart before trying again."
|
|
643
|
+
);
|
|
644
|
+
setStage("closed");
|
|
645
|
+
}
|
|
646
|
+
function storageUnavailable() {
|
|
647
|
+
releaseCheckoutOwnership();
|
|
648
|
+
setError(
|
|
649
|
+
"Your browser couldn\u2019t safely save and coordinate checkout progress. Allow site storage, then reload and try again."
|
|
650
|
+
);
|
|
651
|
+
setStage("storage_error");
|
|
652
|
+
}
|
|
653
|
+
React.useEffect(() => {
|
|
654
|
+
let alive = true;
|
|
655
|
+
void (async () => {
|
|
656
|
+
const locks = navigator.locks;
|
|
657
|
+
if (!locks) {
|
|
658
|
+
if (!alive) return;
|
|
659
|
+
storageUnavailable();
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const storedAttemptToken = attemptTokenRef.current;
|
|
663
|
+
let bootRecovery = {
|
|
664
|
+
status: "continue"
|
|
665
|
+
};
|
|
666
|
+
if (storedAttemptToken) {
|
|
667
|
+
const acquired = await claimCheckoutOwnership();
|
|
668
|
+
if (acquired) {
|
|
669
|
+
const recovered = await client.post(
|
|
670
|
+
"/api/store/checkout/recover",
|
|
671
|
+
{ attemptToken: storedAttemptToken }
|
|
672
|
+
);
|
|
673
|
+
if (recovered.ok && recovered.data.status === "paid") {
|
|
674
|
+
await completePaidAttempt(
|
|
675
|
+
storedAttemptToken,
|
|
676
|
+
recovered.data.orderId ?? storedAttemptToken,
|
|
677
|
+
true
|
|
678
|
+
);
|
|
679
|
+
return;
|
|
680
|
+
} else if (recovered.ok && recovered.data.status === "active") {
|
|
681
|
+
releaseCheckoutOwnership();
|
|
682
|
+
bootRecovery = { status: "active" };
|
|
683
|
+
} else if (recovered.ok && recovered.data.status === "resumable" && !Array.isArray(readCheckoutRecovery(recoveryKey)?.lines)) {
|
|
684
|
+
recoverySnapshotUnavailable();
|
|
685
|
+
return;
|
|
686
|
+
} 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) {
|
|
687
|
+
bootRecovery = {
|
|
688
|
+
status: "resumable",
|
|
689
|
+
session: recovered.data
|
|
690
|
+
};
|
|
691
|
+
} else if (recovered.ok && recovered.data.status === "processing") {
|
|
692
|
+
releaseCheckoutOwnership();
|
|
693
|
+
bootRecovery = { status: "processing" };
|
|
694
|
+
} else if (recovered.ok) {
|
|
695
|
+
if (!releaseOwnedAttempt(storedAttemptToken)) {
|
|
696
|
+
recoverySnapshotUnavailable();
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
releaseCheckoutOwnership();
|
|
700
|
+
} else {
|
|
701
|
+
releaseCheckoutOwnership();
|
|
702
|
+
bootRecovery = {
|
|
703
|
+
status: recovered.error === "sign_in_required" ? "continue" : "error"
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
} else {
|
|
707
|
+
bootRecovery = { status: "active_elsewhere" };
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (!alive) return;
|
|
711
|
+
if (bootRecovery.status === "active") {
|
|
712
|
+
setStage("active");
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (bootRecovery.status === "active_elsewhere") {
|
|
716
|
+
setStage("active_elsewhere");
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (bootRecovery.status === "resumable") {
|
|
720
|
+
setResumableCheckout(bootRecovery.session);
|
|
721
|
+
setStage("resume");
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (bootRecovery.status === "processing") {
|
|
725
|
+
setStage("pending");
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (bootRecovery.status === "error") {
|
|
729
|
+
setError(
|
|
730
|
+
"We couldn\u2019t recover your previous checkout. Please try again."
|
|
731
|
+
);
|
|
732
|
+
setStage("quote_error");
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (cart.lines.length === 0) {
|
|
736
|
+
if (!alive) return;
|
|
737
|
+
setStage("empty");
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const [sessionResult, quoteResult, catalogResult] = await Promise.all([
|
|
741
|
+
client.get("/api/store/session"),
|
|
742
|
+
client.post("/api/store/quote", {
|
|
743
|
+
lines: checkoutLines
|
|
744
|
+
}),
|
|
745
|
+
client.get("/api/store/catalog")
|
|
746
|
+
]);
|
|
747
|
+
if (!alive) return;
|
|
748
|
+
const session = sessionResult.ok && typeof sessionResult.data.signedIn === "boolean" ? sessionResult.data : null;
|
|
749
|
+
const unsupportedQuote = quoteResult.ok && quoteResult.data.problems.some(
|
|
750
|
+
(problem) => problem.code === "not_purchasable"
|
|
751
|
+
);
|
|
752
|
+
const payableQuote = quoteResult.ok && quoteResult.data.lines.length > 0 && !unsupportedQuote ? quoteResult.data : null;
|
|
753
|
+
const catalogLoaded = catalogResult.ok && Array.isArray(catalogResult.data.shippingOptions);
|
|
754
|
+
setQuote(payableQuote);
|
|
755
|
+
setShippingOptions(
|
|
756
|
+
catalogLoaded ? catalogResult.data.shippingOptions : []
|
|
757
|
+
);
|
|
758
|
+
if (catalogLoaded) {
|
|
759
|
+
const policy = readFulfilmentPolicy(catalogResult.data.fulfilment);
|
|
760
|
+
setFulfilmentPolicy(policy);
|
|
761
|
+
if (!policy.pickup && policy.delivery) setFulfilment("ship");
|
|
762
|
+
if (!policy.delivery) setFulfilment("pickup");
|
|
763
|
+
}
|
|
764
|
+
setGivenName(session?.givenName ?? null);
|
|
765
|
+
if (!session) {
|
|
766
|
+
setError("We couldn\u2019t load your checkout. Please try again.");
|
|
767
|
+
setStage("quote_error");
|
|
768
|
+
} else if (unsupportedQuote) {
|
|
769
|
+
setError(
|
|
770
|
+
"Remove unsupported items from your cart before starting retail checkout."
|
|
771
|
+
);
|
|
772
|
+
setStage("unsupported");
|
|
773
|
+
} else if (quoteResult.ok && quoteResult.data.lines.length === 0) {
|
|
774
|
+
setError(null);
|
|
775
|
+
setStage("empty");
|
|
776
|
+
} else if (session.signedIn === false) {
|
|
777
|
+
setStage("signed_out");
|
|
778
|
+
} else if (!catalogLoaded) {
|
|
779
|
+
setError("We couldn\u2019t load delivery options. Please try again.");
|
|
780
|
+
setStage("quote_error");
|
|
781
|
+
} else if (quoteResult.ok) {
|
|
782
|
+
setError(null);
|
|
783
|
+
setStage("details");
|
|
784
|
+
} else {
|
|
785
|
+
setError("We couldn\u2019t load your checkout. Please try again.");
|
|
786
|
+
setStage("quote_error");
|
|
787
|
+
}
|
|
788
|
+
})();
|
|
789
|
+
return () => {
|
|
790
|
+
alive = false;
|
|
791
|
+
};
|
|
792
|
+
}, [cart, client]);
|
|
793
|
+
const loadSavedAddresses = React.useCallback(
|
|
794
|
+
async (selectId) => {
|
|
795
|
+
const result = await client.get("/api/store/addresses").catch(() => null);
|
|
796
|
+
if (!result || !result.ok || !Array.isArray(result.data.items)) {
|
|
797
|
+
setSavedAddresses(null);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const items = result.data.items.filter(
|
|
801
|
+
(item) => item && typeof item.id === "string"
|
|
802
|
+
);
|
|
803
|
+
if (items.length === 0) {
|
|
804
|
+
setSavedAddresses(null);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
setSavedAddresses(items);
|
|
808
|
+
setSavedAddressId((current) => {
|
|
809
|
+
const wanted = selectId ?? current;
|
|
810
|
+
if (wanted && items.some((item) => item.id === wanted)) return wanted;
|
|
811
|
+
return (items.find((item) => item.isPrimary) ?? items[0]).id;
|
|
812
|
+
});
|
|
813
|
+
},
|
|
814
|
+
[client]
|
|
815
|
+
);
|
|
816
|
+
React.useEffect(() => {
|
|
817
|
+
if (stage !== "details" || fulfilment !== "ship") return;
|
|
818
|
+
if (addressBookRequested.current) return;
|
|
819
|
+
addressBookRequested.current = true;
|
|
820
|
+
void loadSavedAddresses();
|
|
821
|
+
}, [stage, fulfilment, loadSavedAddresses]);
|
|
822
|
+
async function saveNewAddress() {
|
|
823
|
+
if (!address || savingAddress) return;
|
|
824
|
+
setSavingAddress(true);
|
|
825
|
+
setAddressBookError(null);
|
|
826
|
+
const result = await client.post("/api/store/addresses", {
|
|
827
|
+
address: address.address,
|
|
828
|
+
placeId: address.placeId ?? null
|
|
829
|
+
}).catch(() => null);
|
|
830
|
+
setSavingAddress(false);
|
|
831
|
+
if (!result || !result.ok || typeof result.data.id !== "string") {
|
|
832
|
+
setAddressBookError(
|
|
833
|
+
(result && !result.ok ? result.message : null) ?? "We couldn\u2019t save that address. You can still use it for this order."
|
|
834
|
+
);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
await loadSavedAddresses(result.data.id);
|
|
838
|
+
setAddingAddress(false);
|
|
839
|
+
setAddress(null);
|
|
840
|
+
}
|
|
841
|
+
React.useEffect(() => {
|
|
842
|
+
if (stage !== "details" || cart.lines.length === 0) return;
|
|
843
|
+
let alive = true;
|
|
844
|
+
setQuote(null);
|
|
845
|
+
setError(null);
|
|
846
|
+
void client.post("/api/store/quote", {
|
|
847
|
+
lines: checkoutLines,
|
|
848
|
+
shippingOptionId: fulfilment === "ship" ? shippingOptionId : null
|
|
849
|
+
}).then((result) => {
|
|
850
|
+
if (!alive) return;
|
|
851
|
+
if (result.ok && result.data.lines.length > 0) {
|
|
852
|
+
setQuote(result.data);
|
|
853
|
+
} else if (result.ok) {
|
|
854
|
+
setQuote(null);
|
|
855
|
+
setStage("empty");
|
|
856
|
+
} else {
|
|
857
|
+
setError("We couldn\u2019t update your checkout total. Please try again.");
|
|
858
|
+
setStage("quote_error");
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
return () => {
|
|
862
|
+
alive = false;
|
|
863
|
+
};
|
|
864
|
+
}, [client, fulfilment, shippingOptionId]);
|
|
865
|
+
React.useEffect(() => {
|
|
866
|
+
if (stage !== "three_ds") return;
|
|
867
|
+
let terminalHandled = false;
|
|
868
|
+
function onMessage(event) {
|
|
869
|
+
if (iframeOriginRef.current && event.origin !== iframeOriginRef.current)
|
|
870
|
+
return;
|
|
871
|
+
const iframe = iframeElRef.current;
|
|
872
|
+
if (!iframe || event.source !== iframe.contentWindow) return;
|
|
873
|
+
const type = event.data?.type;
|
|
874
|
+
if (type === "AuthenticationUserInteractionRequired") {
|
|
875
|
+
if (iframe) {
|
|
876
|
+
iframe.style.visibility = "visible";
|
|
877
|
+
iframe.style.height = "500px";
|
|
878
|
+
}
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (type === "AuthenticationUserInteractionFinished") {
|
|
882
|
+
if (iframe) {
|
|
883
|
+
iframe.style.visibility = "hidden";
|
|
884
|
+
iframe.style.height = "1px";
|
|
885
|
+
}
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (type !== "AuthenticationInternalError" && type !== "AuthenticationComplete")
|
|
889
|
+
return;
|
|
890
|
+
if (terminalHandled) return;
|
|
891
|
+
terminalHandled = true;
|
|
892
|
+
window.removeEventListener("message", onMessage);
|
|
893
|
+
setIframeUrl(null);
|
|
894
|
+
if (type === "AuthenticationInternalError") {
|
|
895
|
+
void cancelFailedChallenge();
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
void afterChallenge();
|
|
899
|
+
}
|
|
900
|
+
window.addEventListener("message", onMessage);
|
|
901
|
+
return () => window.removeEventListener("message", onMessage);
|
|
902
|
+
}, [stage]);
|
|
903
|
+
async function post(path, body) {
|
|
904
|
+
const result = await client.post(path, body);
|
|
905
|
+
if (!result.ok) {
|
|
906
|
+
if (result.error === "sign_in_required") {
|
|
907
|
+
setStage("signed_out");
|
|
908
|
+
throw new Error("sign_in_required");
|
|
909
|
+
}
|
|
910
|
+
throw new StoreRequestError(
|
|
911
|
+
result.message ?? "Something went wrong.",
|
|
912
|
+
result.error
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
return result.data;
|
|
916
|
+
}
|
|
917
|
+
function showChallenge(artifacts) {
|
|
918
|
+
if (!artifacts.iframeUrl) return false;
|
|
919
|
+
const iframeUrl2 = client.url(artifacts.iframeUrl);
|
|
920
|
+
try {
|
|
921
|
+
const parsedIframeUrl = new URL(iframeUrl2, window.location.href);
|
|
922
|
+
const ownedMockUrl = new URL(
|
|
923
|
+
client.url("/api/store/mock-3ds-challenge"),
|
|
924
|
+
window.location.href
|
|
925
|
+
);
|
|
926
|
+
const isOwnedMock = parsedIframeUrl.origin === ownedMockUrl.origin && parsedIframeUrl.pathname === ownedMockUrl.pathname && parsedIframeUrl.search === "" && parsedIframeUrl.hash === "";
|
|
927
|
+
if (parsedIframeUrl.username || parsedIframeUrl.password) return false;
|
|
928
|
+
if (isOwnedMock) {
|
|
929
|
+
const allowedProtocol = parsedIframeUrl.protocol === "https:" || parsedIframeUrl.protocol === "http:" && parsedIframeUrl.hostname === "localhost";
|
|
930
|
+
if (!allowedProtocol) return false;
|
|
931
|
+
iframeOriginRef.current = parsedIframeUrl.origin;
|
|
932
|
+
} else {
|
|
933
|
+
if (parsedIframeUrl.protocol !== "https:" || !artifacts.iframeOrigin)
|
|
934
|
+
return false;
|
|
935
|
+
const suppliedOrigin = new URL(artifacts.iframeOrigin);
|
|
936
|
+
if (suppliedOrigin.protocol !== "https:" || suppliedOrigin.username || suppliedOrigin.password || suppliedOrigin.pathname !== "/" || suppliedOrigin.search || suppliedOrigin.hash)
|
|
937
|
+
return false;
|
|
938
|
+
if (parsedIframeUrl.origin !== suppliedOrigin.origin) return false;
|
|
939
|
+
iframeOriginRef.current = suppliedOrigin.origin;
|
|
940
|
+
}
|
|
941
|
+
} catch {
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
setIframeUrl(iframeUrl2);
|
|
945
|
+
setStage("three_ds");
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
async function cancelFailedChallenge() {
|
|
949
|
+
const orderId = orderRef.current?.orderId;
|
|
950
|
+
if (!orderId) {
|
|
951
|
+
setStage("pending");
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
setStage("paying");
|
|
955
|
+
try {
|
|
956
|
+
await post("/api/store/checkout/cancel-challenge", { orderId });
|
|
957
|
+
const attemptToken = attemptTokenRef.current;
|
|
958
|
+
if (attemptToken) releaseOwnedAttempt(attemptToken);
|
|
959
|
+
setError("The card check could not be completed. Please try again.");
|
|
960
|
+
setStage(quote ? "details" : "closed");
|
|
961
|
+
releaseCheckoutOwnership();
|
|
962
|
+
} catch {
|
|
963
|
+
setError(
|
|
964
|
+
"We couldn\u2019t safely close the failed card check. Go to your orders before trying again."
|
|
965
|
+
);
|
|
966
|
+
setStage("pending");
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
async function recoverAttemptWhileLocked(attemptToken, continueRestart = false) {
|
|
970
|
+
const recovered = await client.post(
|
|
971
|
+
"/api/store/checkout/recover",
|
|
972
|
+
{ attemptToken }
|
|
973
|
+
);
|
|
974
|
+
if (recovered.ok && recovered.data.status === "active") {
|
|
975
|
+
setStage("active");
|
|
976
|
+
releaseCheckoutOwnership();
|
|
977
|
+
return "active";
|
|
978
|
+
}
|
|
979
|
+
if (recovered.ok && recovered.data.status === "resumable" && !Array.isArray(readCheckoutRecovery(recoveryKey)?.lines)) {
|
|
980
|
+
recoverySnapshotUnavailable();
|
|
981
|
+
return "blocked";
|
|
982
|
+
}
|
|
983
|
+
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) {
|
|
984
|
+
setResumableCheckout(recovered.data);
|
|
985
|
+
setStage("resume");
|
|
986
|
+
return "active";
|
|
987
|
+
}
|
|
988
|
+
if (recovered.ok && recovered.data.status === "processing") {
|
|
989
|
+
setStage("pending");
|
|
990
|
+
releaseCheckoutOwnership();
|
|
991
|
+
return "processing";
|
|
992
|
+
}
|
|
993
|
+
if (recovered.ok && recovered.data.status === "paid") {
|
|
994
|
+
return await completePaidAttempt(
|
|
995
|
+
attemptToken,
|
|
996
|
+
recovered.data.orderId ?? attemptToken,
|
|
997
|
+
true
|
|
998
|
+
) ? "paid" : "blocked";
|
|
999
|
+
}
|
|
1000
|
+
if (recovered.ok) {
|
|
1001
|
+
if (!releaseOwnedAttempt(attemptToken)) {
|
|
1002
|
+
recoverySnapshotUnavailable();
|
|
1003
|
+
return "blocked";
|
|
1004
|
+
}
|
|
1005
|
+
if (!continueRestart) {
|
|
1006
|
+
releaseCheckoutOwnership();
|
|
1007
|
+
setError(
|
|
1008
|
+
"Your previous checkout was closed. Review the total and try again."
|
|
1009
|
+
);
|
|
1010
|
+
setStage("details");
|
|
1011
|
+
}
|
|
1012
|
+
return "restart";
|
|
1013
|
+
}
|
|
1014
|
+
setError(
|
|
1015
|
+
"We couldn\u2019t safely close your previous checkout. Go to your orders before trying again."
|
|
1016
|
+
);
|
|
1017
|
+
setStage("closed");
|
|
1018
|
+
releaseCheckoutOwnership();
|
|
1019
|
+
return "blocked";
|
|
1020
|
+
}
|
|
1021
|
+
async function recoverAttemptForRestart(attemptToken) {
|
|
1022
|
+
if (!await claimCheckoutOwnership()) {
|
|
1023
|
+
setStage("active_elsewhere");
|
|
1024
|
+
return "active";
|
|
1025
|
+
}
|
|
1026
|
+
return recoverAttemptWhileLocked(attemptToken);
|
|
1027
|
+
}
|
|
1028
|
+
async function transitionRetryableDecline(message) {
|
|
1029
|
+
const orderId = orderRef.current?.orderId;
|
|
1030
|
+
if (!orderId) {
|
|
1031
|
+
const attemptToken = attemptTokenRef.current;
|
|
1032
|
+
if (attemptToken) await recoverAttemptWhileLocked(attemptToken);
|
|
1033
|
+
else setStage("pending");
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
setError(message);
|
|
1037
|
+
setStage("loading");
|
|
1038
|
+
const result = await client.post("/api/store/checkout/retry-payment", {
|
|
1039
|
+
orderId
|
|
1040
|
+
}).catch(() => null);
|
|
1041
|
+
const retryData = result?.ok && result.data !== null && typeof result.data === "object" && !Array.isArray(result.data) ? result.data : null;
|
|
1042
|
+
const retryArtifacts = retryData?.clientArtifacts !== null && typeof retryData?.clientArtifacts === "object" && !Array.isArray(retryData.clientArtifacts) ? retryData.clientArtifacts : null;
|
|
1043
|
+
if (retryData?.status !== "ready" || retryData.orderId !== orderId || typeof retryData.sessionId !== "string" || typeof retryData.providerKey !== "string" || typeof retryData.amount !== "string" || !retryArtifacts) {
|
|
1044
|
+
await recoverAttemptWhileLocked(orderId);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
setResumableCheckout({
|
|
1048
|
+
orderId,
|
|
1049
|
+
sessionId: retryData.sessionId,
|
|
1050
|
+
sessionStatus: "pending",
|
|
1051
|
+
providerKey: retryData.providerKey,
|
|
1052
|
+
amount: retryData.amount,
|
|
1053
|
+
expiresAt: typeof retryData.expiresAt === "string" ? retryData.expiresAt : null,
|
|
1054
|
+
clientArtifacts: retryArtifacts
|
|
1055
|
+
});
|
|
1056
|
+
setStage("resume_payment");
|
|
1057
|
+
}
|
|
1058
|
+
async function handlePaymentRequestError(error2) {
|
|
1059
|
+
if (!(error2 instanceof StoreRequestError)) return false;
|
|
1060
|
+
if (error2.code === "payment_in_progress" || error2.code === "payment_initializing") {
|
|
1061
|
+
setStage("pending");
|
|
1062
|
+
releaseCheckoutOwnership();
|
|
1063
|
+
return true;
|
|
1064
|
+
}
|
|
1065
|
+
if (error2.code === "payment_failed") {
|
|
1066
|
+
await transitionRetryableDecline(
|
|
1067
|
+
"This payment attempt failed. Please check the details or try another card."
|
|
1068
|
+
);
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1071
|
+
if (error2.code === "payment_cancelled") {
|
|
1072
|
+
setError(
|
|
1073
|
+
"This payment attempt is closed. Go to your orders before trying again."
|
|
1074
|
+
);
|
|
1075
|
+
setStage("closed");
|
|
1076
|
+
releaseCheckoutOwnership();
|
|
1077
|
+
return true;
|
|
1078
|
+
}
|
|
1079
|
+
return false;
|
|
1080
|
+
}
|
|
1081
|
+
async function applyConfirm(result) {
|
|
1082
|
+
const status = result.status;
|
|
1083
|
+
if (status === "paid") {
|
|
1084
|
+
const orderId = orderRef.current?.orderId ?? "";
|
|
1085
|
+
const attemptToken = attemptTokenRef.current;
|
|
1086
|
+
if (!attemptToken) {
|
|
1087
|
+
paidReconciliationPending("snapshot_unavailable");
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
await completePaidAttempt(attemptToken, orderId);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
if (status === "requires_action") {
|
|
1094
|
+
const artifacts = result.clientArtifacts ?? {};
|
|
1095
|
+
if (!showChallenge(artifacts)) await cancelFailedChallenge();
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (status === "declined") {
|
|
1099
|
+
await transitionRetryableDecline(
|
|
1100
|
+
"Your card was declined. Please check the details or try another card."
|
|
1101
|
+
);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
setStage("pending");
|
|
1105
|
+
releaseCheckoutOwnership();
|
|
1106
|
+
}
|
|
1107
|
+
async function dispatchConfirm(orderId) {
|
|
1108
|
+
try {
|
|
1109
|
+
await applyConfirm(
|
|
1110
|
+
await post("/api/store/checkout/confirm", { orderId })
|
|
1111
|
+
);
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
if (err instanceof Error && err.message === "sign_in_required") return;
|
|
1114
|
+
const attemptToken = attemptTokenRef.current;
|
|
1115
|
+
if (attemptToken && err instanceof StoreRequestError && err.code === "checkout_paid") {
|
|
1116
|
+
await completePaidAttempt(attemptToken, orderId);
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
if (await handlePaymentRequestError(err)) return;
|
|
1120
|
+
setError(null);
|
|
1121
|
+
setStage("pending");
|
|
1122
|
+
releaseCheckoutOwnership();
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
async function afterChallenge() {
|
|
1126
|
+
const orderId = orderRef.current?.orderId;
|
|
1127
|
+
if (!orderId) return;
|
|
1128
|
+
setStage("paying");
|
|
1129
|
+
try {
|
|
1130
|
+
const auth = await post("/api/store/checkout/authenticate", { orderId });
|
|
1131
|
+
if (auth.status === "payment_failed") {
|
|
1132
|
+
await transitionRetryableDecline(
|
|
1133
|
+
"The card check failed. Please try another card."
|
|
1134
|
+
);
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
if (auth.status === "requires_action") {
|
|
1138
|
+
const artifacts = auth.clientArtifacts ?? {};
|
|
1139
|
+
if (showChallenge(artifacts)) return;
|
|
1140
|
+
await cancelFailedChallenge();
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
await dispatchConfirm(orderId);
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
if (err instanceof Error && err.message === "sign_in_required") return;
|
|
1146
|
+
if (await handlePaymentRequestError(err)) return;
|
|
1147
|
+
setError(err instanceof Error ? err.message : "Something went wrong.");
|
|
1148
|
+
setStage(resumableCheckout ? "pending" : "details");
|
|
1149
|
+
if (resumableCheckout) releaseCheckoutOwnership();
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
async function initializeCheckoutAttempt(currentQuote) {
|
|
1153
|
+
if (!ownsCheckoutRef.current) return null;
|
|
1154
|
+
while (true) {
|
|
1155
|
+
const ownedToken = attemptTokenRef.current;
|
|
1156
|
+
const stored = readCheckoutRecovery(recoveryKey);
|
|
1157
|
+
if (ownedToken) {
|
|
1158
|
+
if (stored?.attemptToken !== ownedToken || stored.lines === null) {
|
|
1159
|
+
recoverySnapshotUnavailable();
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
recoveryRef.current = stored;
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1165
|
+
if (stored) {
|
|
1166
|
+
recoveryRef.current = stored;
|
|
1167
|
+
attemptTokenRef.current = stored.attemptToken;
|
|
1168
|
+
const disposition = await recoverAttemptWhileLocked(
|
|
1169
|
+
stored.attemptToken,
|
|
1170
|
+
true
|
|
1171
|
+
);
|
|
1172
|
+
if (disposition !== "restart") return null;
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1177
|
+
const existingToken = attemptTokenRef.current;
|
|
1178
|
+
const expectedLines = recoveryRef.current?.lines ?? checkoutCartSnapshotRef.current;
|
|
1179
|
+
if (expectedLines === null) {
|
|
1180
|
+
recoverySnapshotUnavailable();
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
const selectedShippingOptionId = fulfilment === "ship" ? shippingOptionId : null;
|
|
1184
|
+
const coordinated = await withCartLock(async (latest, signal) => {
|
|
1185
|
+
const wireLines = cartToWireLines(latest);
|
|
1186
|
+
const quoteResult = await client.post(
|
|
1187
|
+
"/api/store/quote",
|
|
1188
|
+
{
|
|
1189
|
+
lines: wireLines,
|
|
1190
|
+
shippingOptionId: selectedShippingOptionId
|
|
1191
|
+
},
|
|
1192
|
+
{ signal }
|
|
1193
|
+
);
|
|
1194
|
+
if (!quoteResult.ok || signal.aborted)
|
|
1195
|
+
return { status: "quote_error" };
|
|
1196
|
+
const freshQuote = quoteResult.data;
|
|
1197
|
+
const purchasedLines = snapshotPurchasedCartLines(
|
|
1198
|
+
latest.lines,
|
|
1199
|
+
freshQuote.lines
|
|
1200
|
+
);
|
|
1201
|
+
if (!purchasedLines || freshQuote.lines.length === 0 || freshQuote.problems.length > 0 || !sameCartLineSnapshot(latest.lines, expectedLines) || !samePayableQuote(currentQuote, freshQuote, selectedShippingOptionId)) {
|
|
1202
|
+
return {
|
|
1203
|
+
status: "cart_changed",
|
|
1204
|
+
quote: freshQuote,
|
|
1205
|
+
baseline: latest
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
const attemptToken = existingToken ?? crypto.randomUUID();
|
|
1209
|
+
if (existingToken) {
|
|
1210
|
+
if (!sameCartLineSnapshot(purchasedLines, expectedLines)) {
|
|
1211
|
+
return {
|
|
1212
|
+
status: "cart_changed",
|
|
1213
|
+
quote: freshQuote,
|
|
1214
|
+
baseline: latest
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
} else {
|
|
1218
|
+
const recovery = {
|
|
1219
|
+
v: 2,
|
|
1220
|
+
attemptToken,
|
|
1221
|
+
lines: purchasedLines
|
|
1222
|
+
};
|
|
1223
|
+
if (!writeCheckoutRecovery(recoveryKey, recovery, null))
|
|
1224
|
+
return { status: "storage_error" };
|
|
1225
|
+
recoveryRef.current = recovery;
|
|
1226
|
+
attemptTokenRef.current = attemptToken;
|
|
1227
|
+
}
|
|
1228
|
+
const initPromise = post("/api/store/checkout/init", {
|
|
1229
|
+
lines: wireLines,
|
|
1230
|
+
attemptToken,
|
|
1231
|
+
expectedQuote: expectedQuote(freshQuote, selectedShippingOptionId),
|
|
1232
|
+
fulfilmentMethod: fulfilment,
|
|
1233
|
+
shippingOptionId: selectedShippingOptionId,
|
|
1234
|
+
// Exactly one of these carries the destination. A SAVED address is named by
|
|
1235
|
+
// id and resolved server-side under this shopper's own scope; a fresh pick
|
|
1236
|
+
// is a Google reference. Neither is address components — the client has
|
|
1237
|
+
// never been allowed to supply those (PAT-850).
|
|
1238
|
+
shippingAddressId: fulfilment === "ship" && usingSavedAddress ? savedAddressId : null,
|
|
1239
|
+
shippingPlace: fulfilment === "ship" && !usingSavedAddress && address ? { address: address.address, placeId: address.placeId ?? null } : null
|
|
1240
|
+
});
|
|
1241
|
+
return {
|
|
1242
|
+
status: "ready",
|
|
1243
|
+
attemptToken,
|
|
1244
|
+
initPromise
|
|
1245
|
+
};
|
|
1246
|
+
});
|
|
1247
|
+
if (coordinated.status === "cart_storage_unavailable") {
|
|
1248
|
+
storageUnavailable();
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1251
|
+
if (coordinated.status === "not_completed") {
|
|
1252
|
+
setError("We couldn\u2019t confirm your checkout total. Please try again.");
|
|
1253
|
+
setStage("quote_error");
|
|
1254
|
+
releaseCheckoutOwnership();
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
const result = coordinated.result;
|
|
1258
|
+
if (result.status === "storage_error") {
|
|
1259
|
+
storageUnavailable();
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
if (result.status === "quote_error") {
|
|
1263
|
+
setError("We couldn\u2019t confirm your checkout total. Please try again.");
|
|
1264
|
+
setStage("quote_error");
|
|
1265
|
+
releaseCheckoutOwnership();
|
|
1266
|
+
return null;
|
|
1267
|
+
}
|
|
1268
|
+
if (result.status === "cart_changed") {
|
|
1269
|
+
if (existingToken) {
|
|
1270
|
+
const disposition = await recoverAttemptWhileLocked(
|
|
1271
|
+
existingToken,
|
|
1272
|
+
true
|
|
1273
|
+
);
|
|
1274
|
+
if (disposition !== "restart") return null;
|
|
1275
|
+
}
|
|
1276
|
+
setPendingQuote(result.quote);
|
|
1277
|
+
setPendingQuoteBaseline(result.baseline);
|
|
1278
|
+
setQuote(null);
|
|
1279
|
+
setError(null);
|
|
1280
|
+
setStage("cart_changed");
|
|
1281
|
+
return null;
|
|
1282
|
+
}
|
|
1283
|
+
return {
|
|
1284
|
+
attemptToken: result.attemptToken,
|
|
1285
|
+
init: await result.initPromise
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
async function beginResume() {
|
|
1289
|
+
if (!resumableCheckout) return;
|
|
1290
|
+
if (!await claimCheckoutOwnership()) {
|
|
1291
|
+
setStage("active_elsewhere");
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
orderRef.current = {
|
|
1295
|
+
orderId: resumableCheckout.orderId,
|
|
1296
|
+
providerKey: resumableCheckout.providerKey
|
|
1297
|
+
};
|
|
1298
|
+
if (resumableCheckout.sessionStatus === "requires_action") {
|
|
1299
|
+
if (!showChallenge(resumableCheckout.clientArtifacts))
|
|
1300
|
+
await cancelFailedChallenge();
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
setError(null);
|
|
1304
|
+
setStage("resume_payment");
|
|
1305
|
+
}
|
|
1306
|
+
async function onResumePayment(e) {
|
|
1307
|
+
e.preventDefault();
|
|
1308
|
+
if (!resumableCheckout) return;
|
|
1309
|
+
if (!await claimCheckoutOwnership()) {
|
|
1310
|
+
setStage("active_elsewhere");
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
setError(null);
|
|
1314
|
+
setStage("paying");
|
|
1315
|
+
orderRef.current = {
|
|
1316
|
+
orderId: resumableCheckout.orderId,
|
|
1317
|
+
providerKey: resumableCheckout.providerKey
|
|
1318
|
+
};
|
|
1319
|
+
try {
|
|
1320
|
+
if (resumableCheckout.providerKey !== "mock") {
|
|
1321
|
+
const { authKey } = resumableCheckout.clientArtifacts;
|
|
1322
|
+
if (!authKey) {
|
|
1323
|
+
setStage("pending");
|
|
1324
|
+
releaseCheckoutOwnership();
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
await loadBpointScript();
|
|
1328
|
+
await attachCardToAuthKey(authKey, card);
|
|
1329
|
+
}
|
|
1330
|
+
await dispatchConfirm(resumableCheckout.orderId);
|
|
1331
|
+
} catch (err) {
|
|
1332
|
+
if (err instanceof Error && err.message === "sign_in_required") {
|
|
1333
|
+
releaseCheckoutOwnership();
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
if (await handlePaymentRequestError(err)) return;
|
|
1337
|
+
setError(err instanceof Error ? err.message : "Something went wrong.");
|
|
1338
|
+
setStage("resume_payment");
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
async function onPay(e) {
|
|
1342
|
+
e.preventDefault();
|
|
1343
|
+
if (!quote || quote.lines.length === 0) {
|
|
1344
|
+
setQuote(null);
|
|
1345
|
+
setError(
|
|
1346
|
+
quote ? null : "We couldn\u2019t confirm your checkout total. Please try again."
|
|
1347
|
+
);
|
|
1348
|
+
setStage(quote ? "empty" : "quote_error");
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
if (!await claimCheckoutOwnership()) {
|
|
1352
|
+
setStage("active_elsewhere");
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
setError(null);
|
|
1356
|
+
setStage("paying");
|
|
1357
|
+
let attemptToken = attemptTokenRef.current;
|
|
1358
|
+
try {
|
|
1359
|
+
const initialized = await initializeCheckoutAttempt(quote);
|
|
1360
|
+
if (!initialized) return;
|
|
1361
|
+
attemptToken = initialized.attemptToken;
|
|
1362
|
+
const { init } = initialized;
|
|
1363
|
+
setError(null);
|
|
1364
|
+
if (init.status === "cart_changed") {
|
|
1365
|
+
const changedQuote = init.quote;
|
|
1366
|
+
setPendingQuote(changedQuote);
|
|
1367
|
+
const baselineLines = recoveryRef.current?.lines;
|
|
1368
|
+
if (!baselineLines) {
|
|
1369
|
+
recoverySnapshotUnavailable();
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
setPendingQuoteBaseline({ v: 1, lines: baselineLines });
|
|
1373
|
+
setQuote(null);
|
|
1374
|
+
setError(null);
|
|
1375
|
+
setStage("cart_changed");
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
const orderId = String(init.orderId);
|
|
1379
|
+
const providerKey = String(init.providerKey);
|
|
1380
|
+
orderRef.current = { orderId, providerKey };
|
|
1381
|
+
if (providerKey !== "mock") {
|
|
1382
|
+
const artifacts = init.clientArtifacts ?? {};
|
|
1383
|
+
if (!artifacts.authKey)
|
|
1384
|
+
throw new Error(
|
|
1385
|
+
"We couldn\u2019t start a secure payment. Please try again."
|
|
1386
|
+
);
|
|
1387
|
+
await loadBpointScript();
|
|
1388
|
+
await attachCardToAuthKey(artifacts.authKey, card);
|
|
1389
|
+
}
|
|
1390
|
+
await dispatchConfirm(orderId);
|
|
1391
|
+
} catch (err) {
|
|
1392
|
+
if (err instanceof Error && err.message === "sign_in_required") {
|
|
1393
|
+
releaseCheckoutOwnership();
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
const failedAttemptToken = attemptToken ?? attemptTokenRef.current;
|
|
1397
|
+
if (failedAttemptToken && err instanceof StoreRequestError && err.code === "checkout_paid") {
|
|
1398
|
+
await completePaidAttempt(failedAttemptToken, failedAttemptToken);
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
if (await handlePaymentRequestError(err)) return;
|
|
1402
|
+
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")) {
|
|
1403
|
+
await recoverAttemptForRestart(failedAttemptToken);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
setError(err instanceof Error ? err.message : "Something went wrong.");
|
|
1407
|
+
setStage("details");
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
async function retryQuote() {
|
|
1411
|
+
setError(null);
|
|
1412
|
+
setStage("loading");
|
|
1413
|
+
const sessionResult = await client.get("/api/store/session");
|
|
1414
|
+
if (!sessionResult.ok || typeof sessionResult.data.signedIn !== "boolean") {
|
|
1415
|
+
setQuote(null);
|
|
1416
|
+
setPendingQuote(null);
|
|
1417
|
+
setPendingQuoteBaseline(null);
|
|
1418
|
+
setError("We couldn\u2019t load your checkout. Please try again.");
|
|
1419
|
+
setStage("quote_error");
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
const session = sessionResult.data;
|
|
1423
|
+
setGivenName(session.givenName ?? null);
|
|
1424
|
+
if (session.signedIn === false) {
|
|
1425
|
+
setQuote(null);
|
|
1426
|
+
setPendingQuote(null);
|
|
1427
|
+
setPendingQuoteBaseline(null);
|
|
1428
|
+
setStage("signed_out");
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
const [result, catalogResult] = await Promise.all([
|
|
1432
|
+
client.post("/api/store/quote", {
|
|
1433
|
+
lines: checkoutLines,
|
|
1434
|
+
shippingOptionId: fulfilment === "ship" ? shippingOptionId : null
|
|
1435
|
+
}),
|
|
1436
|
+
client.get("/api/store/catalog")
|
|
1437
|
+
]);
|
|
1438
|
+
const catalogLoaded = catalogResult.ok && Array.isArray(catalogResult.data.shippingOptions);
|
|
1439
|
+
if (catalogLoaded) {
|
|
1440
|
+
const policy = readFulfilmentPolicy(catalogResult.data.fulfilment);
|
|
1441
|
+
setFulfilmentPolicy(policy);
|
|
1442
|
+
if (!policy.pickup && policy.delivery) setFulfilment("ship");
|
|
1443
|
+
if (!policy.delivery) setFulfilment("pickup");
|
|
1444
|
+
}
|
|
1445
|
+
if (!catalogLoaded) {
|
|
1446
|
+
setQuote(null);
|
|
1447
|
+
setShippingOptions([]);
|
|
1448
|
+
setError("We couldn\u2019t load delivery options. Please try again.");
|
|
1449
|
+
setStage("quote_error");
|
|
1450
|
+
} else if (result.ok && result.data.problems.some((problem) => problem.code === "not_purchasable")) {
|
|
1451
|
+
setQuote(null);
|
|
1452
|
+
setError(
|
|
1453
|
+
"Remove unsupported items from your cart before starting retail checkout."
|
|
1454
|
+
);
|
|
1455
|
+
setStage("unsupported");
|
|
1456
|
+
} else if (result.ok && result.data.lines.length > 0) {
|
|
1457
|
+
setShippingOptions(catalogResult.data.shippingOptions);
|
|
1458
|
+
setQuote(result.data);
|
|
1459
|
+
setStage("details");
|
|
1460
|
+
} else if (result.ok) {
|
|
1461
|
+
setQuote(null);
|
|
1462
|
+
setStage("empty");
|
|
1463
|
+
} else {
|
|
1464
|
+
setQuote(null);
|
|
1465
|
+
setError("We couldn\u2019t load your checkout. Please try again.");
|
|
1466
|
+
setStage("quote_error");
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
async function acceptHealedQuote() {
|
|
1470
|
+
if (!pendingQuote) return;
|
|
1471
|
+
const locks = navigator.locks;
|
|
1472
|
+
if (!locks) {
|
|
1473
|
+
storageUnavailable();
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
const attemptedRetail = new Map(
|
|
1477
|
+
(pendingQuoteBaseline?.lines ?? checkoutLines).filter((line) => line.kind === "retail").map((line) => [line.variantId, line.quantity])
|
|
1478
|
+
);
|
|
1479
|
+
const healedRetail = new Map(
|
|
1480
|
+
pendingQuote.lines.map((line) => [line.variantId, line.quantity])
|
|
1481
|
+
);
|
|
1482
|
+
const optionUnavailable = pendingQuote.problems.some(
|
|
1483
|
+
(problem) => problem.code === "shipping_option_unavailable"
|
|
1484
|
+
);
|
|
1485
|
+
const nextShippingOptionId = optionUnavailable ? null : shippingOptionId;
|
|
1486
|
+
setPendingQuote(null);
|
|
1487
|
+
setPendingQuoteBaseline(null);
|
|
1488
|
+
setQuote(null);
|
|
1489
|
+
setError(null);
|
|
1490
|
+
setStage("loading");
|
|
1491
|
+
const outcome = {
|
|
1492
|
+
confirmedQuote: null,
|
|
1493
|
+
nextProblemQuote: null,
|
|
1494
|
+
nextProblemBaseline: null,
|
|
1495
|
+
quoteFailed: false
|
|
1496
|
+
};
|
|
1497
|
+
const continueWhileCoordinated = async () => {
|
|
1498
|
+
const attemptToken = attemptTokenRef.current;
|
|
1499
|
+
const stored = readCheckoutRecovery(recoveryKey);
|
|
1500
|
+
if (attemptToken && (stored?.attemptToken !== attemptToken || stored.lines === null) || !attemptToken && stored)
|
|
1501
|
+
return null;
|
|
1502
|
+
return mutateCart(
|
|
1503
|
+
(latest) => ({
|
|
1504
|
+
v: 1,
|
|
1505
|
+
lines: latest.lines.flatMap((line) => {
|
|
1506
|
+
if (line.kind !== "retail") return [line];
|
|
1507
|
+
const attemptedQuantity = attemptedRetail.get(line.variantId);
|
|
1508
|
+
if (attemptedQuantity === void 0) return [line];
|
|
1509
|
+
const healedQuantity = healedRetail.get(line.variantId);
|
|
1510
|
+
if (healedQuantity === void 0) return [];
|
|
1511
|
+
return [
|
|
1512
|
+
{
|
|
1513
|
+
...line,
|
|
1514
|
+
quantity: line.quantity <= attemptedQuantity ? Math.min(line.quantity, healedQuantity) : Math.min(
|
|
1515
|
+
99,
|
|
1516
|
+
healedQuantity + (line.quantity - attemptedQuantity)
|
|
1517
|
+
)
|
|
1518
|
+
}
|
|
1519
|
+
];
|
|
1520
|
+
})
|
|
1521
|
+
}),
|
|
1522
|
+
async (proposed, signal) => {
|
|
1523
|
+
const payableLines = cartToWireLines({
|
|
1524
|
+
v: 1,
|
|
1525
|
+
lines: proposed.lines.filter((line) => line.kind === "retail")
|
|
1526
|
+
});
|
|
1527
|
+
const result = await client.post(
|
|
1528
|
+
"/api/store/quote",
|
|
1529
|
+
{
|
|
1530
|
+
lines: payableLines,
|
|
1531
|
+
shippingOptionId: fulfilment === "ship" ? nextShippingOptionId : null
|
|
1532
|
+
},
|
|
1533
|
+
{ signal }
|
|
1534
|
+
);
|
|
1535
|
+
if (!result.ok) {
|
|
1536
|
+
outcome.quoteFailed = true;
|
|
1537
|
+
return false;
|
|
1538
|
+
}
|
|
1539
|
+
if (result.data.lines.length === 0 || result.data.problems.length > 0) {
|
|
1540
|
+
outcome.nextProblemQuote = result.data;
|
|
1541
|
+
outcome.nextProblemBaseline = proposed;
|
|
1542
|
+
return false;
|
|
1543
|
+
}
|
|
1544
|
+
const purchasedLines = snapshotPurchasedCartLines(
|
|
1545
|
+
proposed.lines,
|
|
1546
|
+
result.data.lines
|
|
1547
|
+
);
|
|
1548
|
+
if (!purchasedLines) return false;
|
|
1549
|
+
if (attemptToken) {
|
|
1550
|
+
const recovery = {
|
|
1551
|
+
v: 2,
|
|
1552
|
+
attemptToken,
|
|
1553
|
+
lines: purchasedLines
|
|
1554
|
+
};
|
|
1555
|
+
if (!writeCheckoutRecovery(recoveryKey, recovery, attemptToken))
|
|
1556
|
+
return false;
|
|
1557
|
+
recoveryRef.current = recovery;
|
|
1558
|
+
}
|
|
1559
|
+
outcome.confirmedQuote = result.data;
|
|
1560
|
+
return true;
|
|
1561
|
+
}
|
|
1562
|
+
);
|
|
1563
|
+
};
|
|
1564
|
+
const persisted = ownsCheckoutRef.current ? await continueWhileCoordinated() : await locks.request(
|
|
1565
|
+
`patientos-store-checkout:${recoveryKey}`,
|
|
1566
|
+
continueWhileCoordinated
|
|
1567
|
+
);
|
|
1568
|
+
if (!persisted || persisted.status === "cart_storage_unavailable") {
|
|
1569
|
+
storageUnavailable();
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
if (persisted.status === "not_committed") {
|
|
1573
|
+
if (outcome.quoteFailed) {
|
|
1574
|
+
setError("We couldn\u2019t load your checkout. Please try again.");
|
|
1575
|
+
setStage("quote_error");
|
|
1576
|
+
} else if (outcome.nextProblemQuote?.lines.length === 0) {
|
|
1577
|
+
setStage("empty");
|
|
1578
|
+
} else if (outcome.nextProblemQuote && outcome.nextProblemBaseline) {
|
|
1579
|
+
setPendingQuote(outcome.nextProblemQuote);
|
|
1580
|
+
setPendingQuoteBaseline(outcome.nextProblemBaseline);
|
|
1581
|
+
setStage("cart_changed");
|
|
1582
|
+
} else {
|
|
1583
|
+
storageUnavailable();
|
|
1584
|
+
}
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
if (optionUnavailable) setShippingOptionId(null);
|
|
1588
|
+
if (!outcome.confirmedQuote) {
|
|
1589
|
+
storageUnavailable();
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
checkoutCartSnapshotRef.current = persisted.cart.lines;
|
|
1593
|
+
setCheckoutLines(
|
|
1594
|
+
outcome.confirmedQuote.lines.map((line) => ({
|
|
1595
|
+
kind: "retail",
|
|
1596
|
+
variantId: line.variantId,
|
|
1597
|
+
quantity: line.quantity
|
|
1598
|
+
}))
|
|
1599
|
+
);
|
|
1600
|
+
setQuote(outcome.confirmedQuote);
|
|
1601
|
+
setStage("details");
|
|
1602
|
+
}
|
|
1603
|
+
if (stage === "loading") {
|
|
1604
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1605
|
+
/* @__PURE__ */ jsx2("h2", { className: "sk-checkout__heading", children: "Checkout" }),
|
|
1606
|
+
/* @__PURE__ */ jsx2("p", { className: "sk-checkout__placeholder", role: "status", children: "Loading your order\u2026" })
|
|
1607
|
+
] });
|
|
1608
|
+
}
|
|
1609
|
+
if (stage === "empty") {
|
|
1610
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1611
|
+
/* @__PURE__ */ jsx2("h2", { className: "sk-checkout__heading", children: "Checkout" }),
|
|
1612
|
+
/* @__PURE__ */ jsx2("p", { className: "sk-checkout__placeholder", role: "status", children: "Your cart is empty." }),
|
|
1613
|
+
/* @__PURE__ */ jsx2("a", { className: "sk-checkout__link", href: "/store", children: "Browse the shop" })
|
|
1614
|
+
] });
|
|
1615
|
+
}
|
|
1616
|
+
if (stage === "signed_out") {
|
|
1617
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1618
|
+
/* @__PURE__ */ jsx2("h2", { className: "sk-checkout__heading", children: "Checkout" }),
|
|
1619
|
+
/* @__PURE__ */ jsx2("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." }),
|
|
1620
|
+
/* @__PURE__ */ jsx2(
|
|
1621
|
+
"a",
|
|
1622
|
+
{
|
|
1623
|
+
className: "sk-checkout__signin",
|
|
1624
|
+
href: client.signInHref("/checkout"),
|
|
1625
|
+
children: "Sign in to continue"
|
|
1626
|
+
}
|
|
1627
|
+
)
|
|
1628
|
+
] });
|
|
1629
|
+
}
|
|
1630
|
+
if (stage === "unsupported") {
|
|
1631
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1632
|
+
/* @__PURE__ */ jsx2("h2", { className: "sk-checkout__heading", children: "Update your cart" }),
|
|
1633
|
+
/* @__PURE__ */ jsx2("p", { className: "sk-checkout__note", role: "alert", children: error }),
|
|
1634
|
+
/* @__PURE__ */ jsx2("a", { className: "sk-checkout__link", href: "/cart", children: "Return to cart" })
|
|
1635
|
+
] });
|
|
1636
|
+
}
|
|
1637
|
+
if (stage === "quote_error") {
|
|
1638
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1639
|
+
/* @__PURE__ */ jsx2("h2", { className: "sk-checkout__heading", children: givenName ? `Checkout \u2014 hello, ${givenName}` : "Checkout" }),
|
|
1640
|
+
/* @__PURE__ */ jsx2("p", { className: "sk-checkout__error", role: "alert", children: error ?? "We couldn\u2019t load your checkout. Please try again." }),
|
|
1641
|
+
/* @__PURE__ */ jsx2(
|
|
1642
|
+
"button",
|
|
1643
|
+
{
|
|
1644
|
+
type: "button",
|
|
1645
|
+
className: "sk-checkout__pay",
|
|
1646
|
+
onClick: () => void retryQuote(),
|
|
1647
|
+
children: "Try again"
|
|
1648
|
+
}
|
|
1649
|
+
)
|
|
1650
|
+
] });
|
|
1651
|
+
}
|
|
1652
|
+
if (stage === "cart_changed" && pendingQuote) {
|
|
1653
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1654
|
+
/* @__PURE__ */ jsx2("h2", { className: "sk-checkout__heading", children: "Your cart was updated" }),
|
|
1655
|
+
/* @__PURE__ */ jsx2("div", { className: "sk-checkout__error", role: "alert", children: pendingQuote.problems.length > 0 ? /* @__PURE__ */ jsx2("ul", { children: pendingQuote.problems.map((problem, index) => /* @__PURE__ */ jsx2("li", { children: problem.message }, `${problem.code}-${index}`)) }) : /* @__PURE__ */ jsx2("p", { children: "Prices or delivery details changed while you were checking out." }) }),
|
|
1656
|
+
/* @__PURE__ */ jsx2(
|
|
1657
|
+
"button",
|
|
1658
|
+
{
|
|
1659
|
+
type: "button",
|
|
1660
|
+
className: "sk-checkout__pay",
|
|
1661
|
+
onClick: () => void acceptHealedQuote(),
|
|
1662
|
+
children: "Continue with updated cart"
|
|
1663
|
+
}
|
|
1664
|
+
)
|
|
1665
|
+
] });
|
|
1666
|
+
}
|
|
1667
|
+
if (stage === "reconciliation_pending") {
|
|
1668
|
+
const persistedRecovery = readCheckoutRecovery(recoveryKey);
|
|
1669
|
+
const canRetry = persistedRecovery?.attemptToken === attemptTokenRef.current && persistedRecovery.lines !== null;
|
|
1670
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1671
|
+
/* @__PURE__ */ jsx2("h2", { className: "sk-checkout__heading", children: "Payment received, cart update pending" }),
|
|
1672
|
+
/* @__PURE__ */ jsx2("p", { className: "sk-checkout__note", role: "alert", children: error }),
|
|
1673
|
+
canRetry ? /* @__PURE__ */ jsx2(
|
|
1674
|
+
"button",
|
|
1675
|
+
{
|
|
1676
|
+
type: "button",
|
|
1677
|
+
className: "sk-checkout__pay",
|
|
1678
|
+
onClick: () => void retryPaidReconciliation(),
|
|
1679
|
+
children: "Try updating cart again"
|
|
1680
|
+
}
|
|
1681
|
+
) : /* @__PURE__ */ jsx2("a", { className: "sk-checkout__link", href: "/cart", children: "Review cart" }),
|
|
1682
|
+
/* @__PURE__ */ jsx2("a", { className: "sk-checkout__link", href: ordersHref, children: "View My orders" })
|
|
1683
|
+
] });
|
|
1684
|
+
}
|
|
1685
|
+
if (stage === "paid") {
|
|
1686
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1687
|
+
/* @__PURE__ */ jsx2("h2", { className: "sk-checkout__heading", children: "Payment received" }),
|
|
1688
|
+
/* @__PURE__ */ jsx2("p", { className: "sk-checkout__note", role: "status", children: "Taking you to your order\u2026" })
|
|
1689
|
+
] });
|
|
1690
|
+
}
|
|
1691
|
+
if (stage === "storage_error") {
|
|
1692
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1693
|
+
/* @__PURE__ */ jsx2("h2", { className: "sk-checkout__heading", children: "Checkout unavailable" }),
|
|
1694
|
+
/* @__PURE__ */ jsx2("p", { className: "sk-checkout__note", role: "alert", children: error })
|
|
1695
|
+
] });
|
|
1696
|
+
}
|
|
1697
|
+
if (stage === "closed") {
|
|
1698
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1699
|
+
/* @__PURE__ */ jsx2("h2", { className: "sk-checkout__heading", children: "Payment not completed" }),
|
|
1700
|
+
/* @__PURE__ */ jsx2("p", { className: "sk-checkout__note", role: "status", children: error }),
|
|
1701
|
+
/* @__PURE__ */ jsx2("a", { className: "sk-checkout__link", href: ordersHref, children: "Go to my orders" })
|
|
1702
|
+
] });
|
|
1703
|
+
}
|
|
1704
|
+
if (stage === "resume" && resumableCheckout) {
|
|
1705
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1706
|
+
/* @__PURE__ */ jsx2("h2", { className: "sk-checkout__heading", children: "Resume checkout" }),
|
|
1707
|
+
/* @__PURE__ */ jsxs2("p", { className: "sk-checkout__note", role: "status", children: [
|
|
1708
|
+
"Your checkout for ",
|
|
1709
|
+
formatMoney(resumableCheckout.amount),
|
|
1710
|
+
" is ready to continue."
|
|
1711
|
+
] }),
|
|
1712
|
+
/* @__PURE__ */ jsx2(
|
|
1713
|
+
"button",
|
|
1714
|
+
{
|
|
1715
|
+
type: "button",
|
|
1716
|
+
className: "sk-checkout__pay",
|
|
1717
|
+
onClick: beginResume,
|
|
1718
|
+
children: "Resume checkout"
|
|
1719
|
+
}
|
|
1720
|
+
),
|
|
1721
|
+
/* @__PURE__ */ jsx2("a", { className: "sk-checkout__link", href: ordersHref, children: "Go to my orders" })
|
|
1722
|
+
] });
|
|
1723
|
+
}
|
|
1724
|
+
if (stage === "resume_payment" && resumableCheckout) {
|
|
1725
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1726
|
+
/* @__PURE__ */ jsx2("h2", { className: "sk-checkout__heading", children: "Resume checkout" }),
|
|
1727
|
+
error ? /* @__PURE__ */ jsx2("p", { className: "sk-checkout__error", role: "alert", children: error }) : null,
|
|
1728
|
+
/* @__PURE__ */ jsxs2("p", { className: "sk-checkout__note", role: "status", children: [
|
|
1729
|
+
"Order total: ",
|
|
1730
|
+
formatMoney(resumableCheckout.amount)
|
|
1731
|
+
] }),
|
|
1732
|
+
/* @__PURE__ */ jsxs2("form", { className: "sk-checkout__form", onSubmit: onResumePayment, children: [
|
|
1733
|
+
/* @__PURE__ */ jsxs2("fieldset", { className: "sk-checkout__fieldset", children: [
|
|
1734
|
+
/* @__PURE__ */ jsx2("legend", { children: "Card details" }),
|
|
1735
|
+
/* @__PURE__ */ jsxs2("label", { children: [
|
|
1736
|
+
/* @__PURE__ */ jsx2("span", { children: "Name on card" }),
|
|
1737
|
+
/* @__PURE__ */ jsx2(
|
|
1738
|
+
"input",
|
|
1739
|
+
{
|
|
1740
|
+
required: true,
|
|
1741
|
+
autoComplete: "cc-name",
|
|
1742
|
+
value: card.name,
|
|
1743
|
+
onChange: (e) => setCard({ ...card, name: e.target.value })
|
|
1744
|
+
}
|
|
1745
|
+
)
|
|
1746
|
+
] }),
|
|
1747
|
+
/* @__PURE__ */ jsxs2("label", { children: [
|
|
1748
|
+
/* @__PURE__ */ jsx2("span", { children: "Card number" }),
|
|
1749
|
+
/* @__PURE__ */ jsx2(
|
|
1750
|
+
"input",
|
|
1751
|
+
{
|
|
1752
|
+
required: true,
|
|
1753
|
+
inputMode: "numeric",
|
|
1754
|
+
autoComplete: "cc-number",
|
|
1755
|
+
value: card.number,
|
|
1756
|
+
onChange: (e) => setCard({ ...card, number: e.target.value })
|
|
1757
|
+
}
|
|
1758
|
+
)
|
|
1759
|
+
] }),
|
|
1760
|
+
/* @__PURE__ */ jsxs2("label", { children: [
|
|
1761
|
+
/* @__PURE__ */ jsx2("span", { children: "Expiry (MM/YY)" }),
|
|
1762
|
+
/* @__PURE__ */ jsx2(
|
|
1763
|
+
"input",
|
|
1764
|
+
{
|
|
1765
|
+
required: true,
|
|
1766
|
+
inputMode: "numeric",
|
|
1767
|
+
autoComplete: "cc-exp",
|
|
1768
|
+
placeholder: "MM/YY",
|
|
1769
|
+
value: card.expiry,
|
|
1770
|
+
onChange: (e) => setCard({ ...card, expiry: e.target.value })
|
|
1771
|
+
}
|
|
1772
|
+
)
|
|
1773
|
+
] }),
|
|
1774
|
+
/* @__PURE__ */ jsxs2("label", { children: [
|
|
1775
|
+
/* @__PURE__ */ jsx2("span", { children: "Security code" }),
|
|
1776
|
+
/* @__PURE__ */ jsx2(
|
|
1777
|
+
"input",
|
|
1778
|
+
{
|
|
1779
|
+
required: true,
|
|
1780
|
+
inputMode: "numeric",
|
|
1781
|
+
autoComplete: "cc-csc",
|
|
1782
|
+
value: card.cvn,
|
|
1783
|
+
onChange: (e) => setCard({ ...card, cvn: e.target.value })
|
|
1784
|
+
}
|
|
1785
|
+
)
|
|
1786
|
+
] }),
|
|
1787
|
+
/* @__PURE__ */ jsx2("p", { className: "sk-checkout__note", children: "Your card details go straight to our payment provider \u2014 they never reach this clinic's systems." })
|
|
1788
|
+
] }),
|
|
1789
|
+
/* @__PURE__ */ jsxs2("button", { type: "submit", className: "sk-checkout__pay", children: [
|
|
1790
|
+
"Pay ",
|
|
1791
|
+
formatMoney(resumableCheckout.amount)
|
|
1792
|
+
] })
|
|
1793
|
+
] })
|
|
1794
|
+
] });
|
|
1795
|
+
}
|
|
1796
|
+
if (stage === "active_elsewhere") {
|
|
1797
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1798
|
+
/* @__PURE__ */ jsx2("h2", { className: "sk-checkout__heading", children: "Checkout active elsewhere" }),
|
|
1799
|
+
/* @__PURE__ */ jsx2("p", { className: "sk-checkout__note", role: "status", children: "Finish checkout in your other tab, or close it and reload this page." }),
|
|
1800
|
+
/* @__PURE__ */ jsx2("a", { className: "sk-checkout__link", href: ordersHref, children: "Go to my orders" })
|
|
1801
|
+
] });
|
|
1802
|
+
}
|
|
1803
|
+
if (stage === "active") {
|
|
1804
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1805
|
+
/* @__PURE__ */ jsx2("h2", { className: "sk-checkout__heading", children: "Checkout is still opening" }),
|
|
1806
|
+
/* @__PURE__ */ jsx2("p", { className: "sk-checkout__note", role: "status", children: "Wait a moment, then reload this page before trying again." }),
|
|
1807
|
+
/* @__PURE__ */ jsx2("a", { className: "sk-checkout__link", href: ordersHref, children: "Go to my orders" })
|
|
1808
|
+
] });
|
|
1809
|
+
}
|
|
1810
|
+
if (stage === "pending") {
|
|
1811
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1812
|
+
/* @__PURE__ */ jsx2("h2", { className: "sk-checkout__heading", children: "Confirming your payment" }),
|
|
1813
|
+
/* @__PURE__ */ jsx2("p", { className: "sk-checkout__note", role: "status", children: error ?? "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." }),
|
|
1814
|
+
/* @__PURE__ */ jsx2("a", { className: "sk-checkout__link", href: ordersHref, children: "Go to my orders" })
|
|
1815
|
+
] });
|
|
1816
|
+
}
|
|
1817
|
+
if (stage === "three_ds" && iframeUrl) {
|
|
1818
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1819
|
+
/* @__PURE__ */ jsx2("h2", { className: "sk-checkout__heading", children: "Your bank needs to check this payment" }),
|
|
1820
|
+
/* @__PURE__ */ jsx2(
|
|
1821
|
+
"iframe",
|
|
1822
|
+
{
|
|
1823
|
+
ref: iframeElRef,
|
|
1824
|
+
className: "sk-checkout__3ds",
|
|
1825
|
+
src: iframeUrl,
|
|
1826
|
+
style: { visibility: "hidden", height: "1px" },
|
|
1827
|
+
title: "Card verification"
|
|
1828
|
+
}
|
|
1829
|
+
)
|
|
1830
|
+
] });
|
|
1831
|
+
}
|
|
1832
|
+
const busy = stage === "paying";
|
|
1833
|
+
if (!quote) {
|
|
1834
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1835
|
+
/* @__PURE__ */ jsx2("h2", { className: "sk-checkout__heading", children: givenName ? `Checkout \u2014 hello, ${givenName}` : "Checkout" }),
|
|
1836
|
+
/* @__PURE__ */ jsx2("p", { className: "sk-checkout__placeholder", role: "status", children: "Updating your order total\u2026" })
|
|
1837
|
+
] });
|
|
1838
|
+
}
|
|
1839
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1840
|
+
/* @__PURE__ */ jsx2("h2", { className: "sk-checkout__heading", children: givenName ? `Checkout \u2014 hello, ${givenName}` : "Checkout" }),
|
|
1841
|
+
error ? /* @__PURE__ */ jsx2("p", { className: "sk-checkout__error", role: "alert", children: error }) : null,
|
|
1842
|
+
/* @__PURE__ */ jsxs2("div", { className: "sk-checkout__summary", children: [
|
|
1843
|
+
/* @__PURE__ */ jsx2("ul", { className: "sk-checkout__lines", children: quote.lines.map((l) => /* @__PURE__ */ jsxs2("li", { children: [
|
|
1844
|
+
/* @__PURE__ */ jsxs2("span", { children: [
|
|
1845
|
+
l.title,
|
|
1846
|
+
" \xD7 ",
|
|
1847
|
+
l.quantity
|
|
1848
|
+
] }),
|
|
1849
|
+
/* @__PURE__ */ jsx2("span", { children: formatMoney(l.lineTotal) })
|
|
1850
|
+
] }, l.variantId)) }),
|
|
1851
|
+
/* @__PURE__ */ jsxs2("dl", { className: "sk-checkout__totals", children: [
|
|
1852
|
+
/* @__PURE__ */ jsxs2("div", { children: [
|
|
1853
|
+
/* @__PURE__ */ jsx2("dt", { children: "Subtotal" }),
|
|
1854
|
+
/* @__PURE__ */ jsx2("dd", { children: formatMoney(quote.subtotal) })
|
|
1855
|
+
] }),
|
|
1856
|
+
Number(quote.shippingTotal) > 0 ? /* @__PURE__ */ jsxs2("div", { children: [
|
|
1857
|
+
/* @__PURE__ */ jsx2("dt", { children: "Delivery" }),
|
|
1858
|
+
/* @__PURE__ */ jsx2("dd", { children: formatMoney(quote.shippingTotal) })
|
|
1859
|
+
] }) : null,
|
|
1860
|
+
/* @__PURE__ */ jsxs2("div", { children: [
|
|
1861
|
+
/* @__PURE__ */ jsx2("dt", { children: "GST included" }),
|
|
1862
|
+
/* @__PURE__ */ jsx2("dd", { children: formatMoney(quote.taxTotal) })
|
|
1863
|
+
] }),
|
|
1864
|
+
/* @__PURE__ */ jsxs2("div", { className: "sk-checkout__total-row", children: [
|
|
1865
|
+
/* @__PURE__ */ jsx2("dt", { children: "Total" }),
|
|
1866
|
+
/* @__PURE__ */ jsx2("dd", { children: formatMoney(quote.total) })
|
|
1867
|
+
] })
|
|
1868
|
+
] })
|
|
1869
|
+
] }),
|
|
1870
|
+
/* @__PURE__ */ jsxs2("form", { className: "sk-checkout__form", onSubmit: onPay, children: [
|
|
1871
|
+
quote.requiresShipping && !offersEitherMethod ? /* @__PURE__ */ jsx2("p", { className: "sk-checkout__notice", role: "status", children: "This clinic isn\u2019t taking online orders for delivery or collection right now. Please contact the clinic." }) : null,
|
|
1872
|
+
quote.requiresShipping && offersEitherMethod ? /* @__PURE__ */ jsxs2("fieldset", { className: "sk-checkout__fieldset", children: [
|
|
1873
|
+
offersBothMethods ? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1874
|
+
/* @__PURE__ */ jsx2("legend", { children: "How would you like to get this?" }),
|
|
1875
|
+
/* @__PURE__ */ jsxs2("label", { children: [
|
|
1876
|
+
/* @__PURE__ */ jsx2(
|
|
1877
|
+
"input",
|
|
1878
|
+
{
|
|
1879
|
+
type: "radio",
|
|
1880
|
+
name: "fulfilment",
|
|
1881
|
+
checked: fulfilment === "pickup",
|
|
1882
|
+
onChange: () => setFulfilment("pickup")
|
|
1883
|
+
}
|
|
1884
|
+
),
|
|
1885
|
+
"Collect from the clinic"
|
|
1886
|
+
] }),
|
|
1887
|
+
/* @__PURE__ */ jsxs2("label", { children: [
|
|
1888
|
+
/* @__PURE__ */ jsx2(
|
|
1889
|
+
"input",
|
|
1890
|
+
{
|
|
1891
|
+
type: "radio",
|
|
1892
|
+
name: "fulfilment",
|
|
1893
|
+
checked: fulfilment === "ship",
|
|
1894
|
+
onChange: () => setFulfilment("ship")
|
|
1895
|
+
}
|
|
1896
|
+
),
|
|
1897
|
+
"Deliver to me"
|
|
1898
|
+
] })
|
|
1899
|
+
] }) : /* @__PURE__ */ jsx2("legend", { children: fulfilmentPolicy.delivery ? "Delivery" : "Collection" }),
|
|
1900
|
+
fulfilment === "ship" ? /* @__PURE__ */ jsxs2("div", { className: "sk-checkout__ship", children: [
|
|
1901
|
+
/* @__PURE__ */ jsxs2("label", { children: [
|
|
1902
|
+
/* @__PURE__ */ jsx2("span", { children: "Delivery option" }),
|
|
1903
|
+
/* @__PURE__ */ jsxs2(
|
|
1904
|
+
"select",
|
|
1905
|
+
{
|
|
1906
|
+
required: true,
|
|
1907
|
+
value: shippingOptionId ?? "",
|
|
1908
|
+
onChange: (e) => setShippingOptionId(e.target.value || null),
|
|
1909
|
+
children: [
|
|
1910
|
+
/* @__PURE__ */ jsx2("option", { value: "", children: "Choose\u2026" }),
|
|
1911
|
+
shippingOptions.map((o) => /* @__PURE__ */ jsxs2("option", { value: o.id, children: [
|
|
1912
|
+
o.name,
|
|
1913
|
+
" \u2014 ",
|
|
1914
|
+
formatMoney(o.price)
|
|
1915
|
+
] }, o.id))
|
|
1916
|
+
]
|
|
1917
|
+
}
|
|
1918
|
+
)
|
|
1919
|
+
] }),
|
|
1920
|
+
savedAddresses && !addingAddress ? /* @__PURE__ */ jsxs2("div", { className: "sk-checkout__addresses", children: [
|
|
1921
|
+
/* @__PURE__ */ jsx2("span", { className: "sk-checkout__addresses-label", children: "Delivery address" }),
|
|
1922
|
+
savedAddresses.map((saved) => /* @__PURE__ */ jsxs2("label", { children: [
|
|
1923
|
+
/* @__PURE__ */ jsx2(
|
|
1924
|
+
"input",
|
|
1925
|
+
{
|
|
1926
|
+
type: "radio",
|
|
1927
|
+
name: "sk-checkout-saved-address",
|
|
1928
|
+
checked: savedAddressId === saved.id,
|
|
1929
|
+
onChange: () => setSavedAddressId(saved.id)
|
|
1930
|
+
}
|
|
1931
|
+
),
|
|
1932
|
+
/* @__PURE__ */ jsxs2("span", { children: [
|
|
1933
|
+
formatSavedAddress(saved),
|
|
1934
|
+
saved.isPrimary ? " \xB7 Default" : ""
|
|
1935
|
+
] })
|
|
1936
|
+
] }, saved.id)),
|
|
1937
|
+
/* @__PURE__ */ jsx2(
|
|
1938
|
+
"button",
|
|
1939
|
+
{
|
|
1940
|
+
type: "button",
|
|
1941
|
+
className: "sk-checkout__link-button",
|
|
1942
|
+
onClick: () => {
|
|
1943
|
+
setAddressBookError(null);
|
|
1944
|
+
setAddingAddress(true);
|
|
1945
|
+
},
|
|
1946
|
+
children: "Add a new address"
|
|
1947
|
+
}
|
|
1948
|
+
)
|
|
1949
|
+
] }) : null,
|
|
1950
|
+
!usingSavedAddress ? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1951
|
+
/* @__PURE__ */ jsx2(
|
|
1952
|
+
AddressPicker,
|
|
1953
|
+
{
|
|
1954
|
+
idPrefix: "sk-checkout",
|
|
1955
|
+
label: "Delivery address",
|
|
1956
|
+
value: address,
|
|
1957
|
+
onChange: setAddress,
|
|
1958
|
+
search: (query) => searchStoreAddresses(query, client),
|
|
1959
|
+
validate: (value) => validateStoreAddress(value, client)
|
|
1960
|
+
}
|
|
1961
|
+
),
|
|
1962
|
+
addingAddress ? /* @__PURE__ */ jsxs2("div", { className: "sk-checkout__address-actions", children: [
|
|
1963
|
+
/* @__PURE__ */ jsx2(
|
|
1964
|
+
"button",
|
|
1965
|
+
{
|
|
1966
|
+
type: "button",
|
|
1967
|
+
className: "sk-checkout__link-button",
|
|
1968
|
+
disabled: !address || savingAddress,
|
|
1969
|
+
onClick: () => void saveNewAddress(),
|
|
1970
|
+
children: savingAddress ? "Saving\u2026" : "Save address"
|
|
1971
|
+
}
|
|
1972
|
+
),
|
|
1973
|
+
/* @__PURE__ */ jsx2(
|
|
1974
|
+
"button",
|
|
1975
|
+
{
|
|
1976
|
+
type: "button",
|
|
1977
|
+
className: "sk-checkout__link-button",
|
|
1978
|
+
onClick: () => {
|
|
1979
|
+
setAddingAddress(false);
|
|
1980
|
+
setAddressBookError(null);
|
|
1981
|
+
setAddress(null);
|
|
1982
|
+
},
|
|
1983
|
+
children: "Cancel"
|
|
1984
|
+
}
|
|
1985
|
+
)
|
|
1986
|
+
] }) : null,
|
|
1987
|
+
addressBookError ? /* @__PURE__ */ jsx2("p", { className: "sk-checkout__notice", role: "status", children: addressBookError }) : null
|
|
1988
|
+
] }) : null
|
|
1989
|
+
] }) : null
|
|
1990
|
+
] }) : null,
|
|
1991
|
+
/* @__PURE__ */ jsxs2("fieldset", { className: "sk-checkout__fieldset", children: [
|
|
1992
|
+
/* @__PURE__ */ jsx2("legend", { children: "Card details" }),
|
|
1993
|
+
/* @__PURE__ */ jsxs2("label", { children: [
|
|
1994
|
+
/* @__PURE__ */ jsx2("span", { children: "Name on card" }),
|
|
1995
|
+
/* @__PURE__ */ jsx2(
|
|
1996
|
+
"input",
|
|
1997
|
+
{
|
|
1998
|
+
required: true,
|
|
1999
|
+
autoComplete: "cc-name",
|
|
2000
|
+
value: card.name,
|
|
2001
|
+
onChange: (e) => setCard({ ...card, name: e.target.value })
|
|
2002
|
+
}
|
|
2003
|
+
)
|
|
2004
|
+
] }),
|
|
2005
|
+
/* @__PURE__ */ jsxs2("label", { children: [
|
|
2006
|
+
/* @__PURE__ */ jsx2("span", { children: "Card number" }),
|
|
2007
|
+
/* @__PURE__ */ jsx2(
|
|
2008
|
+
"input",
|
|
2009
|
+
{
|
|
2010
|
+
required: true,
|
|
2011
|
+
inputMode: "numeric",
|
|
2012
|
+
autoComplete: "cc-number",
|
|
2013
|
+
value: card.number,
|
|
2014
|
+
onChange: (e) => setCard({ ...card, number: e.target.value })
|
|
2015
|
+
}
|
|
2016
|
+
)
|
|
2017
|
+
] }),
|
|
2018
|
+
/* @__PURE__ */ jsxs2("label", { children: [
|
|
2019
|
+
/* @__PURE__ */ jsx2("span", { children: "Expiry (MM/YY)" }),
|
|
2020
|
+
/* @__PURE__ */ jsx2(
|
|
2021
|
+
"input",
|
|
2022
|
+
{
|
|
2023
|
+
required: true,
|
|
2024
|
+
inputMode: "numeric",
|
|
2025
|
+
autoComplete: "cc-exp",
|
|
2026
|
+
placeholder: "MM/YY",
|
|
2027
|
+
value: card.expiry,
|
|
2028
|
+
onChange: (e) => setCard({ ...card, expiry: e.target.value })
|
|
2029
|
+
}
|
|
2030
|
+
)
|
|
2031
|
+
] }),
|
|
2032
|
+
/* @__PURE__ */ jsxs2("label", { children: [
|
|
2033
|
+
/* @__PURE__ */ jsx2("span", { children: "Security code" }),
|
|
2034
|
+
/* @__PURE__ */ jsx2(
|
|
2035
|
+
"input",
|
|
2036
|
+
{
|
|
2037
|
+
required: true,
|
|
2038
|
+
inputMode: "numeric",
|
|
2039
|
+
autoComplete: "cc-csc",
|
|
2040
|
+
value: card.cvn,
|
|
2041
|
+
onChange: (e) => setCard({ ...card, cvn: e.target.value })
|
|
2042
|
+
}
|
|
2043
|
+
)
|
|
2044
|
+
] }),
|
|
2045
|
+
/* @__PURE__ */ jsx2("p", { className: "sk-checkout__note", children: "Your card details go straight to our payment provider \u2014 they never reach this clinic's systems." })
|
|
2046
|
+
] }),
|
|
2047
|
+
/* @__PURE__ */ jsx2("button", { type: "submit", className: "sk-checkout__pay", disabled: busy, children: busy ? "Processing\u2026" : `Pay ${formatMoney(quote.total)}` })
|
|
2048
|
+
] })
|
|
2049
|
+
] });
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
export {
|
|
2053
|
+
AddressPicker,
|
|
2054
|
+
loadBpointScript,
|
|
2055
|
+
attachCardToAuthKey,
|
|
2056
|
+
CheckoutClient
|
|
2057
|
+
};
|