@develemit/storefront 0.6.0 → 0.8.0
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/CHANGELOG.md +100 -0
- package/README.md +17 -95
- package/dist/components/add-to-cart-button/add-to-cart-button.d.ts +1 -0
- package/dist/components/checkout-success/checkout-success-view.d.ts +16 -1
- package/dist/components/checkout-success/labels.d.ts +3 -0
- package/dist/index.js +186 -151
- package/dist/index.js.map +1 -1
- package/dist/styles.css +71 -3
- package/dist/views/product-detail-view.d.ts +3 -1
- package/docs/cart.md +111 -0
- package/package.json +12 -3
package/docs/cart.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Cart and checkout
|
|
2
|
+
|
|
3
|
+
[← @develemit/storefront](../README.md)
|
|
4
|
+
|
|
5
|
+
## Cart
|
|
6
|
+
|
|
7
|
+
Wrap the app in `CartProvider` (inside `StorefrontProvider`). State is a
|
|
8
|
+
snapshot of `CartLine`s; prices and availability are re-verified server-side
|
|
9
|
+
at checkout. Persistence goes through a `CartStorage` (`load`/`save`); the
|
|
10
|
+
default is `localStorageCartStorage(key)`, which hydrates after mount and never
|
|
11
|
+
touches `window` during render.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
<CartProvider>
|
|
15
|
+
<CartButton />
|
|
16
|
+
<CartDrawer summary={{ onCheckout, checkoutLabel: 'Checkout' }} />
|
|
17
|
+
</CartProvider>
|
|
18
|
+
|
|
19
|
+
<ProductDetailView
|
|
20
|
+
product={product}
|
|
21
|
+
slots={{ actions: <AddToCartButton product={product} /> }}
|
|
22
|
+
/>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`useCart()` exposes `add` (returns `{ added, capped, reason? }`), `remove`,
|
|
26
|
+
`setQuantity`, `clear`, `open`/`close`, and the derived `itemCount` and
|
|
27
|
+
`subtotalCents`. A product's `quantity` is the line's `maxQuantity`, so
|
|
28
|
+
one-of-a-kind items can only be added once. Every label is overridable
|
|
29
|
+
through a `labels` prop; the defaults are exported (`defaultCartDrawerLabels`,
|
|
30
|
+
`defaultCartLineItemLabels`, `defaultAddToCartLabels`, `defaultCheckoutLabels`,
|
|
31
|
+
`defaultCheckoutSuccessLabels`, `defaultAvailabilityLabels`) so you can spread
|
|
32
|
+
and change a few. The pure cart logic (`cartReducer`, `addLine`, `emptyCart`)
|
|
33
|
+
is exported for hosts that keep the cart outside React.
|
|
34
|
+
|
|
35
|
+
## Checkout
|
|
36
|
+
|
|
37
|
+
`useCheckout({ startCheckout, onRedirect?, onDiscountInvalid?, labels? })`
|
|
38
|
+
wraps the host's checkout call (a server action or a fetch) and returns
|
|
39
|
+
`{ checkout, busy, message, clearMessage }`. `startCheckout(items, { discountCode })`
|
|
40
|
+
resolves to a `CheckoutResult`: `{ url }`, `{ error: 'out_of_stock', productIds }`
|
|
41
|
+
or `{ error: 'discount_invalid', reason }`. On the server, `toCheckoutResult(error)`
|
|
42
|
+
from `@develemit/storefront/client` maps `CheckoutConflictError` and
|
|
43
|
+
`DiscountInvalidError` to those results (other errors are rethrown). The hook
|
|
44
|
+
removes out-of-stock lines, sets `message`, and redirects with
|
|
45
|
+
`window.location.assign` unless `onRedirect` is given. Copy is overridable
|
|
46
|
+
through `labels` (`outOfStock`, `unavailable`, `discountReasons`).
|
|
47
|
+
|
|
48
|
+
## Success page
|
|
49
|
+
|
|
50
|
+
`<CheckoutSuccessView order={order} locale? labels? />` renders an order from
|
|
51
|
+
`getOrderBySession`: lines, subtotal, discount row (only when applied),
|
|
52
|
+
shipping, total, ship-to address (only when you pass an order that includes
|
|
53
|
+
`shippingAddress`; the public order-by-session route never returns it),
|
|
54
|
+
tracking, and a message per `OrderStatus` (`pending` tells the buyer to expect
|
|
55
|
+
an email). It has no hooks, so a server component can render it. Labels live in
|
|
56
|
+
`labels` (including `labels.status`). `<ClearCartOnMount />` empties the cart
|
|
57
|
+
once, even under React strict mode. Styles are `.sf-checkout-success*`.
|
|
58
|
+
|
|
59
|
+
Extension points, all optional: `slots={{ header, beforeLines, afterTotals,
|
|
60
|
+
actions }}` (ReactNode) put your own content under the heading, above the
|
|
61
|
+
lines, below the totals, and in the footer. `showReference` adds a
|
|
62
|
+
`labels.reference` row (default `Order {id}`; `{id}` becomes the short
|
|
63
|
+
reference such as `#A1B2C3D4`), and `showSummaryHeading` adds an
|
|
64
|
+
`<h2>` from `labels.summaryHeading`. `continueHref` renders a link labelled
|
|
65
|
+
`labels.continue` unless `slots.actions` is given; pass `renderLink` to route it
|
|
66
|
+
through your framework's link (the view uses no hooks, so it can't read the
|
|
67
|
+
provider's).
|
|
68
|
+
|
|
69
|
+
## Discount codes
|
|
70
|
+
|
|
71
|
+
`CartSummary` (and `CartDrawer` through `summary`) takes an optional `discount`
|
|
72
|
+
prop. The kit stays framework-agnostic, so the host performs the API call (a
|
|
73
|
+
server action in Next):
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
<CartDrawer
|
|
77
|
+
summary={{
|
|
78
|
+
onCheckout: checkout,
|
|
79
|
+
discount: {
|
|
80
|
+
onPreview: async (code, items) => {
|
|
81
|
+
// call client.previewDiscount; map DiscountInvalidError to a reason
|
|
82
|
+
return { ok: true, code, discountCents: 480 };
|
|
83
|
+
// or { ok: false, reason: 'exhausted' }
|
|
84
|
+
},
|
|
85
|
+
onApply: (code) => setDiscountCode(code), // null when removed or dropped
|
|
86
|
+
},
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
It renders a collapsible "Have a code?" field, a "Discount (CODE) −$4.80" row
|
|
92
|
+
with Remove, and a message for a rejected code. When the cart changes
|
|
93
|
+
(debounced, `debounceMs`, default 300) the code is previewed again and dropped
|
|
94
|
+
with its reason if it no longer applies. Pass the applied code to
|
|
95
|
+
`createCheckout`. `discountReasonMessage(reason, overrides?)` returns the same
|
|
96
|
+
text for a reason, for showing a code rejected at checkout. Override copy with `labels.discount` (`toggle`, `field`,
|
|
97
|
+
`apply`, `remove`, `line`, `unavailable`, and `reasons` per rejection reason).
|
|
98
|
+
|
|
99
|
+
## Polish hooks
|
|
100
|
+
|
|
101
|
+
- `CartDrawer` `slots={{ empty, summaryNote }}`: `empty` replaces the
|
|
102
|
+
`labels.empty` text (for example with a link back to the shop); `summaryNote`
|
|
103
|
+
renders under the subtotal and Checkout button. An empty cart shows no
|
|
104
|
+
summary or Checkout button. Close and Remove are at least 44×44px.
|
|
105
|
+
- `CartButton` `icon` (rendered before the label), `labelHidden` (visually
|
|
106
|
+
hides the label, keeping an accessible name like "Cart, 2 items") and
|
|
107
|
+
`hideZeroCount` (omits the count at 0; the accessible name becomes
|
|
108
|
+
"Cart, empty").
|
|
109
|
+
- `--sf-motion-duration` (default `0ms`, no motion) drives the drawer's
|
|
110
|
+
slide-in and the backdrop's fade, in and out. It is forced to `0ms` under
|
|
111
|
+
`prefers-reduced-motion: reduce`. Example: `--sf-motion-duration: 200ms`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@develemit/storefront",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Brand-agnostic React storefront: components, page views, cart, checkout and a typed API client.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/develemit/develemit-commerce.git",
|
|
8
|
+
"directory": "packages/storefront"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/develemit/develemit-commerce/blob/main/docs/getting-started.md",
|
|
4
11
|
"type": "module",
|
|
5
12
|
"main": "./dist/index.js",
|
|
6
13
|
"module": "./dist/index.js",
|
|
@@ -26,7 +33,9 @@
|
|
|
26
33
|
},
|
|
27
34
|
"files": [
|
|
28
35
|
"dist",
|
|
29
|
-
"
|
|
36
|
+
"docs",
|
|
37
|
+
"!**/*.tsbuildinfo",
|
|
38
|
+
"CHANGELOG.md"
|
|
30
39
|
],
|
|
31
40
|
"nx": {
|
|
32
41
|
"name": "storefront",
|
|
@@ -44,6 +53,6 @@
|
|
|
44
53
|
"access": "public"
|
|
45
54
|
},
|
|
46
55
|
"dependencies": {
|
|
47
|
-
"@develemit/commerce-contracts": "0.
|
|
56
|
+
"@develemit/commerce-contracts": "0.8.0"
|
|
48
57
|
}
|
|
49
58
|
}
|