@patientos/website-kit 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -0
- package/dist/{checkout-block-Cog4H4I4.d.ts → booking-block-D-UOAPx3.d.ts} +1 -49
- 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-FSZ726DM.js → chunk-3T7UWKAH.js} +4 -308
- package/dist/{chunk-75FJ5AJB.js → chunk-4GKSSM5N.js} +578 -1608
- package/dist/chunk-4NF7SSIX.js +367 -0
- package/dist/chunk-673YEXAI.js +164 -0
- package/dist/chunk-7BUDUYXN.js +80 -0
- package/dist/chunk-FZ4WKWIE.js +310 -0
- package/dist/{chunk-7UPUEMXX.js → chunk-HYXTDUP2.js} +20 -5
- package/dist/chunk-IOC6QLB7.js +159 -0
- package/dist/chunk-S3ZQQOQT.js +2057 -0
- package/dist/chunk-WQSJ46TP.js +19 -0
- package/dist/chunk-ZNH6TSYP.js +167 -0
- package/dist/index.d.ts +11 -4
- package/dist/index.js +120 -33
- package/dist/islands-impl/cart-button.d.ts +19 -0
- package/dist/islands-impl/cart-button.js +13 -0
- package/dist/islands-impl/cart.d.ts +31 -0
- package/dist/islands-impl/cart.js +11 -0
- package/dist/islands-impl/checkout.d.ts +6 -0
- package/dist/islands-impl/checkout.js +13 -0
- package/dist/islands-impl/store.d.ts +22 -0
- package/dist/islands-impl/store.js +14 -0
- package/dist/islands-impl.d.ts +14 -10
- package/dist/islands-impl.js +22 -7
- package/dist/islands-registry.js +13 -4
- package/dist/{portal-account-8uQc_Ncx.d.ts → portal-account-DRJyr3nz.d.ts} +1 -82
- package/dist/{portal-account.client-BBe_q9yC.d.ts → portal-account.client-Bke4NsgW.d.ts} +2 -1
- package/dist/portal-booking-client.d.ts +3 -2
- package/dist/portal-booking-client.js +2 -1
- package/dist/portal-client-my3q5GME.d.ts +83 -0
- package/dist/store-block-CFxnsfKU.d.ts +37 -0
- package/dist/store-catalog-p7TFOpfk.d.ts +78 -0
- package/dist/store-catalog.d.ts +3 -0
- package/dist/store-catalog.js +11 -0
- package/dist/website-kit.css +196 -1
- package/package.json +31 -1
package/README.md
CHANGED
|
@@ -36,3 +36,89 @@ For a PatientOS clinic that explicitly registers `http://localhost:*`, the kit c
|
|
|
36
36
|
verified patient session back to a dynamically allocated localhost dev server. The
|
|
37
37
|
one-time return credential is scrubbed from the URL and exchanged for a tab-scoped
|
|
38
38
|
bearer; deployed sites continue to use host-only patient cookies.
|
|
39
|
+
|
|
40
|
+
## Store, cart and checkout
|
|
41
|
+
|
|
42
|
+
Use the runtime implementations in an ordinary hydrated React site. Import each surface
|
|
43
|
+
from its OWN subpath — the `islands-impl` barrel pulls the payment stack (BPOINT, the
|
|
44
|
+
address picker, Medicare) into every page that touches it:
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { StoreClient } from '@patientos/website-kit/islands-impl/store'
|
|
48
|
+
import { CartClient } from '@patientos/website-kit/islands-impl/cart'
|
|
49
|
+
import { CartButtonClient } from '@patientos/website-kit/islands-impl/cart-button'
|
|
50
|
+
import { CheckoutClient } from '@patientos/website-kit/islands-impl/checkout'
|
|
51
|
+
import '@patientos/website-kit/website-kit.css'
|
|
52
|
+
|
|
53
|
+
const storeApiOrigin = 'https://my.example-clinic.com'
|
|
54
|
+
|
|
55
|
+
<StoreClient storeApiOrigin={storeApiOrigin} productHandle="vitamin-d" />
|
|
56
|
+
<CartClient storeApiOrigin={storeApiOrigin} />
|
|
57
|
+
<CheckoutClient
|
|
58
|
+
storeApiOrigin={storeApiOrigin}
|
|
59
|
+
ordersHref="/account/orders"
|
|
60
|
+
completionHref="/checkout/complete"
|
|
61
|
+
/>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Use `productHandle` for one exact product or `categoryHandle` for a category-filtered
|
|
65
|
+
catalogue; an exact product takes precedence when both are supplied.
|
|
66
|
+
|
|
67
|
+
### Server-rendering the shop
|
|
68
|
+
|
|
69
|
+
Fetch the catalogue in your own **per-request** route loader and pass it as
|
|
70
|
+
`initialCatalog`. The grid then renders on the server — real product markup for a crawler
|
|
71
|
+
and no loading swap — and the island still refreshes in the background, so prices stay
|
|
72
|
+
honest. Never bake a catalogue into a statically built artifact.
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import { createStoreClient, fetchStoreCatalog } from '@patientos/website-kit'
|
|
76
|
+
import type { StoreCatalog } from '@patientos/website-kit'
|
|
77
|
+
|
|
78
|
+
// in the route loader (runs per request, on the server)
|
|
79
|
+
const catalog = await fetchStoreCatalog(createStoreClient({ apiOrigin: storeApiOrigin }))
|
|
80
|
+
|
|
81
|
+
// in the component
|
|
82
|
+
<StoreClient storeApiOrigin={storeApiOrigin} initialCatalog={catalog} hideCartLink />
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`fetchStoreCatalog` returns `null` on any failure, and `StoreClient` then falls back to
|
|
86
|
+
its own client fetch. Pass `hideCartLink` when your site has its own header cart.
|
|
87
|
+
|
|
88
|
+
### The header cart
|
|
89
|
+
|
|
90
|
+
`<CartButtonClient/>` is a count badge that opens a cart drawer. It costs **no requests**:
|
|
91
|
+
the count comes from the cart already in the shopper's browser, and the drawer quotes
|
|
92
|
+
`POST /api/store/quote` only when opened. The badge is absent (never `0`) until the client
|
|
93
|
+
effect runs, so it adds nothing to server-rendered HTML and your header stays cacheable.
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
<CartButtonClient
|
|
97
|
+
storeApiOrigin={storeApiOrigin}
|
|
98
|
+
label="Cart"
|
|
99
|
+
cartHref="/cart"
|
|
100
|
+
checkoutHref="/checkout"
|
|
101
|
+
/>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
To build your own affordance instead, import the cart module directly:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { cartItemCount, readCart, subscribeToCart } from '@patientos/website-kit/cart-storage'
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
It is browser-only by design (`readCart()` answers empty on the server) and is shared by
|
|
111
|
+
every surface in one page, so a store block adding an item updates your badge with no
|
|
112
|
+
shared React tree.
|
|
113
|
+
|
|
114
|
+
Omit `storeApiOrigin` when PatientOS serves the clinic website and store on one origin.
|
|
115
|
+
For a separately deployed site it must be the clinic's verified patient/API origin, and
|
|
116
|
+
the site Origin must be registered on that clinic's active web-form channel. The client
|
|
117
|
+
always sends `credentials: 'include'` and `cache: 'no-store'`; checkout remains patient
|
|
118
|
+
sign-in-required.
|
|
119
|
+
|
|
120
|
+
The hosting clinic site owns `completionHref`. It defaults to `/checkout/complete`, must
|
|
121
|
+
resolve on that same origin, and receives the paid order ID as its `order` query parameter.
|
|
122
|
+
Cross-origin or malformed destinations safely fall back to the default. Cart lines carry
|
|
123
|
+
package-owned opaque IDs so paid reconciliation removes only the exact line generation
|
|
124
|
+
captured by that checkout attempt.
|
|
@@ -71,52 +71,4 @@ type BookingBlockMarkerProps = {
|
|
|
71
71
|
};
|
|
72
72
|
declare function BookingBlock({ appointmentTypeId, practitionerId, appointmentTypeIds, heading, className, }: BookingBlockMarkerProps): React.ReactElement;
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
type StoreProps = {
|
|
76
|
-
/** Show one category only. Omitted ⇒ the whole published catalogue. */
|
|
77
|
-
categoryHandle?: string;
|
|
78
|
-
/** Heading rendered above the grid. */
|
|
79
|
-
title?: string;
|
|
80
|
-
/** Grid columns at the widest breakpoint. */
|
|
81
|
-
columns?: number;
|
|
82
|
-
};
|
|
83
|
-
/** Marker-only props: the island config plus presentation the page controls. */
|
|
84
|
-
type StoreMarkerProps = StoreProps & {
|
|
85
|
-
className?: string;
|
|
86
|
-
};
|
|
87
|
-
/**
|
|
88
|
-
* BUILD-TIME marker. Emits the mount point plus a static no-JS fallback — the catalogue
|
|
89
|
-
* itself is fetched at runtime (prices and stock must not be baked into a cached static
|
|
90
|
-
* artifact), so the fallback is a prompt, not a product list.
|
|
91
|
-
*/
|
|
92
|
-
declare function Store({ categoryHandle, title, columns, className, }: StoreMarkerProps): React.ReactElement;
|
|
93
|
-
|
|
94
|
-
/** JSON-safe island configuration. Crosses the build→runtime boundary — see `./islands`. */
|
|
95
|
-
type CartProps = Record<string, never>;
|
|
96
|
-
/** Marker-only props: presentation the page controls (the cart itself takes no config). */
|
|
97
|
-
type CartMarkerProps = {
|
|
98
|
-
className?: string;
|
|
99
|
-
};
|
|
100
|
-
/**
|
|
101
|
-
* BUILD-TIME marker. Emits the mount point plus a static no-JS fallback.
|
|
102
|
-
*
|
|
103
|
-
* The fallback shows NO line items — the static artifact is served to every visitor and
|
|
104
|
-
* is edge-cacheable, so it must never carry a patient's cart.
|
|
105
|
-
*/
|
|
106
|
-
declare function Cart({ className }: CartMarkerProps): React.ReactElement;
|
|
107
|
-
|
|
108
|
-
/** JSON-safe island configuration. Crosses the build→runtime boundary — see `./islands`. */
|
|
109
|
-
type CheckoutProps = Record<string, never>;
|
|
110
|
-
/** Marker-only props: presentation the page controls (checkout itself takes no config). */
|
|
111
|
-
type CheckoutMarkerProps = {
|
|
112
|
-
className?: string;
|
|
113
|
-
};
|
|
114
|
-
/**
|
|
115
|
-
* BUILD-TIME marker. Emits the mount point plus a static no-JS fallback.
|
|
116
|
-
*
|
|
117
|
-
* The fallback carries no order, no amount and no payment field: this document is a
|
|
118
|
-
* static artifact served to every visitor.
|
|
119
|
-
*/
|
|
120
|
-
declare function Checkout({ className }: CheckoutMarkerProps): React.ReactElement;
|
|
121
|
-
|
|
122
|
-
export { BookingBlock as B, Cart as C, type FunnelPublicApi as F, Store as S, type BookingBlockMarkerProps as a, type BookingBlockProps as b, type BookingTypeOption as c, type CartMarkerProps as d, type CartProps as e, CertificateFunnel as f, type CertificateFunnelMarkerProps as g, type CertificateFunnelProps as h, Checkout as i, type CheckoutMarkerProps as j, type CheckoutProps as k, type FunnelServiceOption as l, type StoreMarkerProps as m, type StoreProps as n };
|
|
74
|
+
export { BookingBlock as B, CertificateFunnel as C, type FunnelPublicApi as F, type BookingBlockMarkerProps as a, type BookingBlockProps as b, type BookingTypeOption as c, type CertificateFunnelMarkerProps as d, type CertificateFunnelProps as e, type FunnelServiceOption as f };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/** JSON-safe island configuration. Crosses the build→runtime boundary — see `./islands`. */
|
|
4
|
+
type CartProps = {
|
|
5
|
+
/** PatientOS patient/API origin. Omitted keeps the original same-origin contract. */
|
|
6
|
+
storeApiOrigin?: string;
|
|
7
|
+
};
|
|
8
|
+
/** Marker-only props: runtime config plus presentation the page controls. */
|
|
9
|
+
type CartMarkerProps = CartProps & {
|
|
10
|
+
className?: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* BUILD-TIME marker. Emits the mount point plus a static no-JS fallback.
|
|
14
|
+
*
|
|
15
|
+
* The fallback shows NO line items — the static artifact is served to every visitor and
|
|
16
|
+
* is edge-cacheable, so it must never carry a patient's cart.
|
|
17
|
+
*/
|
|
18
|
+
declare function Cart({ storeApiOrigin, className }: CartMarkerProps): React.ReactElement;
|
|
19
|
+
|
|
20
|
+
export { Cart as C, type CartMarkerProps as a, type CartProps as b };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/** JSON-safe island configuration. Crosses the build→runtime boundary — see `./islands`. */
|
|
4
|
+
type CartButtonProps = {
|
|
5
|
+
/** PatientOS patient/API origin. Omitted keeps the original same-origin contract. */
|
|
6
|
+
storeApiOrigin?: string;
|
|
7
|
+
/** Accessible label for the button. Defaults to "Cart". */
|
|
8
|
+
label?: string;
|
|
9
|
+
/** Where "View cart" and the no-JS fallback link point. Defaults to `/cart`. */
|
|
10
|
+
cartHref?: string;
|
|
11
|
+
/** Where the drawer's checkout action points. Defaults to `/checkout`. */
|
|
12
|
+
checkoutHref?: string;
|
|
13
|
+
};
|
|
14
|
+
/** Marker-only props: runtime config plus presentation the page controls. */
|
|
15
|
+
type CartButtonMarkerProps = CartButtonProps & {
|
|
16
|
+
className?: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* BUILD-TIME marker. The no-JS fallback is a plain link to the cart page — never a
|
|
20
|
+
* count, and never a drawer that cannot open.
|
|
21
|
+
*/
|
|
22
|
+
declare function CartButton({ storeApiOrigin, label, cartHref, checkoutHref, className, }: CartButtonMarkerProps): React.ReactElement;
|
|
23
|
+
|
|
24
|
+
export { CartButton as C, type CartButtonMarkerProps as a, type CartButtonProps as b };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cart, in the shopper's own browser (PAT-766).
|
|
3
|
+
*
|
|
4
|
+
* ── Why localStorage and not a table ──────────────────────────────────────────
|
|
5
|
+
* A retail cart holds no PHI and no commitment: it is a list of things somebody
|
|
6
|
+
* is thinking about. Persisting it server-side would buy a cart table, a sweeper
|
|
7
|
+
* for abandoned rows, a merge story for "signed in on a second device", and an
|
|
8
|
+
* anonymous-cart identity — all to reproduce what the browser already does for
|
|
9
|
+
* free. Nothing here is trusted: the server re-derives every price, every stock
|
|
10
|
+
* fact and every title on each quote and again inside the checkout mint, so the
|
|
11
|
+
* worst a tampered cart can do is change WHAT is bought, never what it costs.
|
|
12
|
+
*
|
|
13
|
+
* ── The `kind` discriminator is a RESERVATION ─────────────────────────────────
|
|
14
|
+
* The product requirement one-origin exists to serve is ONE cart spanning
|
|
15
|
+
* medication fills and retail items (docs/one-origin-islands.md §1). v1 sells
|
|
16
|
+
* retail only — but the storage shape carries `kind` from day one, and every
|
|
17
|
+
* function here PRESERVES a `fill` line it does not understand rather than
|
|
18
|
+
* dropping it. That matters: the day fills become purchasable, a patient who added
|
|
19
|
+
* one to an older build must not find it silently gone. The server refuses to
|
|
20
|
+
* price a fill line (`not_purchasable`), so a preserved line is inert, visible,
|
|
21
|
+
* and honest.
|
|
22
|
+
*
|
|
23
|
+
* ── Total by construction ─────────────────────────────────────────────────────
|
|
24
|
+
* Every read is defensive. Absent storage (SSR, private mode, a browser with it
|
|
25
|
+
* disabled), malformed JSON, a wrong version, a non-array `lines`, a line missing
|
|
26
|
+
* its variant — each yields an EMPTY cart, never a throw. A shop that white-screens
|
|
27
|
+
* because a stale key had a stray brace is worse than a shop that has forgotten
|
|
28
|
+
* your cart.
|
|
29
|
+
*/
|
|
30
|
+
/** The versioned storage key. A future shape change bumps this and starts clean. */
|
|
31
|
+
declare const CART_STORAGE_KEY = "patientos.cart.v1";
|
|
32
|
+
/** 'retail' is purchasable in v1; 'fill' is RESERVED and preserved but not sellable. */
|
|
33
|
+
type CartLineKind = "retail" | "fill";
|
|
34
|
+
type CartLine = {
|
|
35
|
+
lineId: string;
|
|
36
|
+
kind: CartLineKind;
|
|
37
|
+
variantId: string;
|
|
38
|
+
quantity: number;
|
|
39
|
+
};
|
|
40
|
+
type Cart = {
|
|
41
|
+
v: 1;
|
|
42
|
+
lines: CartLine[];
|
|
43
|
+
settledAttemptTokens?: string[];
|
|
44
|
+
};
|
|
45
|
+
type CartDraft = {
|
|
46
|
+
v: 1;
|
|
47
|
+
lines: readonly unknown[];
|
|
48
|
+
settledAttemptTokens?: string[];
|
|
49
|
+
};
|
|
50
|
+
type CartMutationResult = {
|
|
51
|
+
cart: Cart;
|
|
52
|
+
persisted: boolean;
|
|
53
|
+
status: "updated" | "cart_storage_unavailable" | "not_committed";
|
|
54
|
+
};
|
|
55
|
+
type AddToCartResult = {
|
|
56
|
+
cart: Cart;
|
|
57
|
+
status: "added" | "line_limit" | "quantity_limit" | "cart_storage_unavailable";
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* A FRESH empty cart, every call.
|
|
61
|
+
*
|
|
62
|
+
* Never hand out a shared instance: `addToCart` mutates the cart it reads, so a
|
|
63
|
+
* module-level singleton would accumulate lines across reads and survive a
|
|
64
|
+
* `clearCart` — a shop that remembers what you deleted.
|
|
65
|
+
*/
|
|
66
|
+
declare function emptyCart(): Cart;
|
|
67
|
+
/** A frozen empty cart for comparisons. Never mutated, never returned from a read. */
|
|
68
|
+
declare const EMPTY_CART: Readonly<Cart>;
|
|
69
|
+
declare function normalizeCartLines(entries: readonly unknown[]): CartLine[];
|
|
70
|
+
declare function readCart(): Cart;
|
|
71
|
+
declare function mutateCart(mutation: (latest: Cart) => CartDraft, beforePersist?: (proposed: Cart, signal: AbortSignal) => Promise<boolean>): Promise<CartMutationResult>;
|
|
72
|
+
type CartLockResult<T> = {
|
|
73
|
+
status: "completed";
|
|
74
|
+
cart: Cart;
|
|
75
|
+
result: T;
|
|
76
|
+
} | {
|
|
77
|
+
status: "cart_storage_unavailable";
|
|
78
|
+
cart: Cart;
|
|
79
|
+
} | {
|
|
80
|
+
status: "not_completed";
|
|
81
|
+
cart: Cart;
|
|
82
|
+
};
|
|
83
|
+
declare function withCartLock<T>(operation: (latest: Cart, signal: AbortSignal) => Promise<T>): Promise<CartLockResult<T>>;
|
|
84
|
+
declare function writeCart(cart: CartDraft): Promise<CartMutationResult>;
|
|
85
|
+
declare function addToCart(variantId: string, quantity?: number): Promise<AddToCartResult>;
|
|
86
|
+
declare function setCartQuantity(kind: CartLineKind, variantId: string, quantity: number): Promise<CartMutationResult>;
|
|
87
|
+
declare function adjustCartQuantity(kind: CartLineKind, variantId: string, delta: number, maximumBaseline?: number): Promise<CartMutationResult>;
|
|
88
|
+
declare function removeFromCart(variantId: string, kind?: CartLineKind): Promise<CartMutationResult>;
|
|
89
|
+
declare function clearCart(): Promise<CartMutationResult>;
|
|
90
|
+
declare function subtractPurchasedCartLines(purchasedLines: readonly unknown[], attemptToken?: string): Promise<CartMutationResult>;
|
|
91
|
+
/** Total item count — the header badge. */
|
|
92
|
+
declare function cartItemCount(cart?: Cart): number;
|
|
93
|
+
/** The wire shape posted to `/api/store/quote` and `/checkout/init`. */
|
|
94
|
+
declare function cartToWireLines(cart: {
|
|
95
|
+
v?: 1;
|
|
96
|
+
lines: readonly {
|
|
97
|
+
kind: CartLineKind;
|
|
98
|
+
variantId: string;
|
|
99
|
+
quantity: number;
|
|
100
|
+
}[];
|
|
101
|
+
}): {
|
|
102
|
+
kind: CartLineKind;
|
|
103
|
+
variantId: string;
|
|
104
|
+
quantity: number;
|
|
105
|
+
}[];
|
|
106
|
+
type Listener = (cart: Cart) => void;
|
|
107
|
+
/** Subscribe to cart changes (this tab AND other tabs). Returns an unsubscribe. */
|
|
108
|
+
declare function subscribeToCart(fn: Listener): () => void;
|
|
109
|
+
|
|
110
|
+
export { type AddToCartResult, CART_STORAGE_KEY, type Cart, type CartLine, type CartLineKind, type CartLockResult, type CartMutationResult, EMPTY_CART, addToCart, adjustCartQuantity, cartItemCount, cartToWireLines, clearCart, emptyCart, mutateCart, normalizeCartLines, readCart, removeFromCart, setCartQuantity, subscribeToCart, subtractPurchasedCartLines, withCartLock, writeCart };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CART_STORAGE_KEY,
|
|
3
|
+
EMPTY_CART,
|
|
4
|
+
addToCart,
|
|
5
|
+
adjustCartQuantity,
|
|
6
|
+
cartItemCount,
|
|
7
|
+
cartToWireLines,
|
|
8
|
+
clearCart,
|
|
9
|
+
emptyCart,
|
|
10
|
+
mutateCart,
|
|
11
|
+
normalizeCartLines,
|
|
12
|
+
readCart,
|
|
13
|
+
removeFromCart,
|
|
14
|
+
setCartQuantity,
|
|
15
|
+
subscribeToCart,
|
|
16
|
+
subtractPurchasedCartLines,
|
|
17
|
+
withCartLock,
|
|
18
|
+
writeCart
|
|
19
|
+
} from "./chunk-4NF7SSIX.js";
|
|
20
|
+
import "./chunk-MLKGABMK.js";
|
|
21
|
+
export {
|
|
22
|
+
CART_STORAGE_KEY,
|
|
23
|
+
EMPTY_CART,
|
|
24
|
+
addToCart,
|
|
25
|
+
adjustCartQuantity,
|
|
26
|
+
cartItemCount,
|
|
27
|
+
cartToWireLines,
|
|
28
|
+
clearCart,
|
|
29
|
+
emptyCart,
|
|
30
|
+
mutateCart,
|
|
31
|
+
normalizeCartLines,
|
|
32
|
+
readCart,
|
|
33
|
+
removeFromCart,
|
|
34
|
+
setCartQuantity,
|
|
35
|
+
subscribeToCart,
|
|
36
|
+
subtractPurchasedCartLines,
|
|
37
|
+
withCartLock,
|
|
38
|
+
writeCart
|
|
39
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/** JSON-safe island configuration. Crosses the build→runtime boundary — see `./islands`. */
|
|
4
|
+
type CheckoutProps = {
|
|
5
|
+
/** PatientOS patient/API origin. Omitted keeps the original same-origin contract. */
|
|
6
|
+
storeApiOrigin?: string;
|
|
7
|
+
/** Shopper-facing order history on the hosting site. */
|
|
8
|
+
ordersHref?: string;
|
|
9
|
+
/** Same-origin order confirmation route on the hosting clinic site. */
|
|
10
|
+
completionHref?: string;
|
|
11
|
+
};
|
|
12
|
+
/** Marker-only props: runtime config plus presentation the page controls. */
|
|
13
|
+
type CheckoutMarkerProps = CheckoutProps & {
|
|
14
|
+
className?: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* BUILD-TIME marker. Emits the mount point plus a static no-JS fallback.
|
|
18
|
+
*
|
|
19
|
+
* The fallback carries no order, no amount and no payment field: this document is a
|
|
20
|
+
* static artifact served to every visitor.
|
|
21
|
+
*/
|
|
22
|
+
declare function Checkout({ storeApiOrigin, ordersHref, completionHref, className, }: CheckoutMarkerProps): React.ReactElement;
|
|
23
|
+
|
|
24
|
+
export { Checkout as C, type CheckoutMarkerProps as a, type CheckoutProps as b };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CartLineList,
|
|
3
|
+
CartProblemList,
|
|
4
|
+
CartTotals,
|
|
5
|
+
UnsupportedCartLineList,
|
|
6
|
+
quoteSourceQuantities,
|
|
7
|
+
useCartQuote
|
|
8
|
+
} from "./chunk-IOC6QLB7.js";
|
|
9
|
+
import {
|
|
10
|
+
createStoreClient
|
|
11
|
+
} from "./chunk-7BUDUYXN.js";
|
|
12
|
+
import {
|
|
13
|
+
readCart,
|
|
14
|
+
subscribeToCart
|
|
15
|
+
} from "./chunk-4NF7SSIX.js";
|
|
16
|
+
|
|
17
|
+
// src/cart-block.client.tsx
|
|
18
|
+
import * as React from "react";
|
|
19
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
20
|
+
function CartClient({ storeApiOrigin }) {
|
|
21
|
+
const client = React.useMemo(
|
|
22
|
+
() => createStoreClient({ apiOrigin: storeApiOrigin }),
|
|
23
|
+
[storeApiOrigin]
|
|
24
|
+
);
|
|
25
|
+
const [cart, setCart] = React.useState(() => readCart());
|
|
26
|
+
const [mutationUnavailable, setMutationUnavailable] = React.useState(false);
|
|
27
|
+
const { quote, quoteSourceCart, failed } = useCartQuote(client, cart);
|
|
28
|
+
async function runMutation(mutation) {
|
|
29
|
+
const result = await mutation;
|
|
30
|
+
setMutationUnavailable(result.status === "cart_storage_unavailable");
|
|
31
|
+
}
|
|
32
|
+
React.useEffect(() => subscribeToCart(setCart), []);
|
|
33
|
+
if (cart.lines.length === 0) {
|
|
34
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
35
|
+
/* @__PURE__ */ jsx("h2", { className: "sk-cart__heading", children: "Your cart" }),
|
|
36
|
+
/* @__PURE__ */ jsx("p", { className: "sk-cart__placeholder", role: "status", children: "Your cart is empty." }),
|
|
37
|
+
/* @__PURE__ */ jsx("a", { className: "sk-cart__continue", href: "/store", children: "Browse the shop" })
|
|
38
|
+
] });
|
|
39
|
+
}
|
|
40
|
+
const quotedVariantIds = new Set(
|
|
41
|
+
quote?.lines.map((line) => line.variantId) ?? []
|
|
42
|
+
);
|
|
43
|
+
const unsupportedLines = cart.lines.filter(
|
|
44
|
+
(line) => line.kind !== "retail" || quote !== null && !quotedVariantIds.has(line.variantId)
|
|
45
|
+
);
|
|
46
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
47
|
+
/* @__PURE__ */ jsx("h2", { className: "sk-cart__heading", children: "Your cart" }),
|
|
48
|
+
quote ? /* @__PURE__ */ jsx(CartProblemList, { problems: quote.problems }) : null,
|
|
49
|
+
failed ? /* @__PURE__ */ jsx("p", { className: "sk-cart__placeholder", role: "alert", children: "We couldn't price your cart just now. Please try again in a moment." }) : null,
|
|
50
|
+
mutationUnavailable ? /* @__PURE__ */ jsx("p", { className: "sk-cart__placeholder", role: "alert", children: "This browser can\u2019t safely update the cart. Try a supported browser with site storage enabled." }) : null,
|
|
51
|
+
/* @__PURE__ */ jsx(
|
|
52
|
+
UnsupportedCartLineList,
|
|
53
|
+
{
|
|
54
|
+
lines: unsupportedLines,
|
|
55
|
+
onMutate: (m) => void runMutation(m)
|
|
56
|
+
}
|
|
57
|
+
),
|
|
58
|
+
quote ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
59
|
+
/* @__PURE__ */ jsx(
|
|
60
|
+
CartLineList,
|
|
61
|
+
{
|
|
62
|
+
lines: quote.lines,
|
|
63
|
+
sourceQuantities: quoteSourceQuantities(quoteSourceCart),
|
|
64
|
+
onMutate: (m) => void runMutation(m)
|
|
65
|
+
}
|
|
66
|
+
),
|
|
67
|
+
/* @__PURE__ */ jsx(CartTotals, { quote }),
|
|
68
|
+
quote.requiresShipping ? /* @__PURE__ */ jsx("p", { className: "sk-cart__note", children: "Delivery details are confirmed at checkout." }) : null,
|
|
69
|
+
/* @__PURE__ */ jsxs("div", { className: "sk-cart__actions", children: [
|
|
70
|
+
/* @__PURE__ */ jsx("a", { className: "sk-cart__continue", href: "/store", children: "Keep shopping" }),
|
|
71
|
+
unsupportedLines.length === 0 && quote.lines.length > 0 ? /* @__PURE__ */ jsx("a", { className: "sk-cart__checkout", href: "/checkout", children: "Checkout" }) : /* @__PURE__ */ jsx("span", { className: "sk-cart__checkout", "aria-disabled": "true", children: "Remove unsupported items to checkout" })
|
|
72
|
+
] })
|
|
73
|
+
] }) : /* @__PURE__ */ jsx("p", { className: "sk-cart__placeholder", role: "status", children: "Pricing your cart\u2026" })
|
|
74
|
+
] });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export {
|
|
78
|
+
CartClient
|
|
79
|
+
};
|