@webround/commerce 0.2.0 → 0.3.1
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 +3 -3
- package/dist/index.js +1 -1
- package/package.json +1 -29
- package/dist/react/index.d.ts +0 -23
- package/dist/react/index.js +0 -15
- package/dist/react/use-account.d.ts +0 -12
- package/dist/react/use-account.js +0 -9
- package/dist/react/use-addresses.d.ts +0 -20
- package/dist/react/use-addresses.js +0 -20
- package/dist/react/use-auth.d.ts +0 -24
- package/dist/react/use-auth.js +0 -20
- package/dist/react/use-cart.d.ts +0 -27
- package/dist/react/use-cart.js +0 -23
- package/dist/react/use-checkout.d.ts +0 -37
- package/dist/react/use-checkout.js +0 -33
- package/dist/react/use-customer-resource.d.ts +0 -5
- package/dist/react/use-customer-resource.js +0 -9
- package/dist/react/use-orders.d.ts +0 -14
- package/dist/react/use-orders.js +0 -6
- package/dist/react/use-resource.d.ts +0 -25
- package/dist/react/use-resource.js +0 -60
- package/dist/react/use-returns.d.ts +0 -14
- package/dist/react/use-returns.js +0 -7
- package/dist/react/use-reviews.d.ts +0 -14
- package/dist/react/use-reviews.js +0 -10
- package/dist/react/use-shop-address.d.ts +0 -12
- package/dist/react/use-shop-address.js +0 -39
- package/dist/react/use-shop.d.ts +0 -34
- package/dist/react/use-shop.js +0 -31
- package/dist/react/use-wishlist.d.ts +0 -15
- package/dist/react/use-wishlist.js +0 -12
- package/dist/returns/rules.d.ts +0 -7
- package/dist/returns/rules.js +0 -45
- package/dist/site.d.ts +0 -7
- package/dist/site.js +0 -41
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @webround/commerce
|
|
2
2
|
|
|
3
3
|
The logic of a Webround Commerce store (catalog, customers, cart and checkout), with no UI and no framework. A site is code (tsx) that uses it; what a
|
|
4
|
-
framework needs comes as an **adapter
|
|
4
|
+
framework needs comes as an **adapter**, a package of its own (`@webround/commerce-react`; `@webround/commerce-next` and others later), never in the core.
|
|
5
5
|
|
|
6
6
|
- It knows nothing of `@webround/core` or `@webround/editor`, and they know nothing of it. The editor edits the site's code as it always does.
|
|
7
7
|
- It is a plain npm package (for now a tarball in `webround-dev-container/vendor/`, see the Makefile: `make build-commerce`, `pack`, `publish-commerce`).
|
|
@@ -55,7 +55,7 @@ Two-factor is covered when the store's key service is there (setup, confirm →
|
|
|
55
55
|
JavaScript never sees; `credentials: 'include'`) and two cookies the CLIENT writes (so a server render can read them): `wrCsrfToken_<storeId>` (the CSRF token the login gave, 30 days, as long as the
|
|
56
56
|
refresh token; a refresh sends it IN A HEADER, `x-wr-csrf`) and `wrDeviceId_<storeId>` (a year). Cookies go through a port of two functions (`browserCookiePort`, `memoryCookiePort`). `AuthStore`
|
|
57
57
|
(`getState`, `subscribe` like the shop's, `restore`, `signup`, `login`, `verifyTwoFactor`, `logout`, `refresh`, `accessToken`, `authHeaders`): `unknown` → `signedOut` | `signedIn`; a login may be a `challenge`
|
|
58
|
-
(two-factor). It renews the JWT a minute before it ends (a timer) and before it is used; a renewal that could not reach the store is asked again after 2, 4, 8, 16 and 30 seconds, for as long as the JWT is good (a page that loaded with no network: ten asks); a `WakePort` (`browserWakePort`: tab visible again, `online`, `pageshow`) makes it look at the JWT against the clock when the page is back, since a timer does not run while a machine sleeps; `dispose()` stops the timer and the port; **The cart's calls** (`src/cart/`, `createCartApi(http)`; step 1 of the cart): `get`, `addItem`, `updateItem`, `removeItem`, `merge` (these need the JWT: pass the client of `createAuth`), `total` and `verifyPromotions` (with or without one: a guest's lines are in the page and `total` prices them). What the real store does, found by `smoke:cart` and written in the types: `addItem` ADDS to a variant that is already there and answers what happened, not the cart (ask with `get`); a cart has 99 different variants at most and ONE currency and cadence (`CART_LIMIT_REACHED`, `CART_CURRENCY_MISMATCH`, in `CART_ERROR_CODES`); the two totals count only the lines that are AVAILABLE (`isAvailable`); `total` leaves out a variant the store does not have (it does not refuse it) and gives a guest's lines no `id`; `verifyPromotions` with no codes, or with one the store does not know, is 404 `PROMOTION_INVALID`, and wants `cartTotal` as a text of digits; an option of a line is `{ optionName, tagValue, tagValueAlias }` in what `total` answers (the value in the language asked) and `{ optionName, optionValue }` in a customer's cart (the value as it was saved: another thing, not another name). Next: the cart's store (a guest's lines in `localStorage`, the customer's cart, `merge` at login, following `useAuth`), then `useCart`. **The cart's store** (`src/cart/store.ts`, `commerce.cart`; `useCart(commerce)` in `/react`): not logged in, the cart is a guest's: lines kept in the browser (`CartStorage` over a `KeyValuePort`, `localStorage` by default, memory where there is none; key `wrCart_<storeId>`) and priced by the store's `total`. Logged in, it is the customer's and every action is a call of the store's. When a customer logs in or signs up with lines in the browser they are merged with the store's `merge`, and forgotten ONLY once the store has them (a merge that could not be asked for loses nothing and is asked again at the next read; one the store refuses for the lines, 400/404/409/422, is not asked again until the next login; a 403 is asked again: the store answers it right after a signup). State: `lines`/`count` (what the cart IS: known at once) apart from `priced`/`isStale` (what it COSTS: the store's answer, which may be one change behind). Actions (`add`, `setQuantity`, `remove`, `refresh`) go one at a time; a customer's shows at once and is settled with one read when the last is done; a refused one THROWS what the store said and the cart goes back to what the store holds. The cart's `cartTrackingId` (the store's analytics id) is made the first time it is needed, sent with every read and pricing, kept from the guest's cart to the customer's after logging in, and forgotten when the cart is empty. Logging out gives the browser's cart back (empty if its lines were merged): the customer's does not come along. To step a quantity read it from `state.lines`, not from `priced`. A server render says `unknown` and zero items, as the first render in the browser does. **The account** (`src/account/`, `commerce.account`; `useAccount(commerce)` in `/react`): the requests a customer makes about their own account, each one call of the store's. `requestPasswordReset(email)`, `resendVerification()`, `requestAccountDeletion()` only ASK: the store sends an email and the page it leads to (where a password is chosen, an address is verified, a deletion is confirmed) is the store's own. `startTwoFactorSetup()` gives the secret and the address an authenticator app takes; `confirmTwoFactor(code)` turns it on, takes the new session (the customer is `twoFactorEnabled`) and gives the RECOVERY CODES, once (turning it off does not exist in the store). The newsletter is a strategy the store applies and `newsletterStrategy(authState)` tells the screen: a logged-in customer with a verified address is done AT ONCE with their own address (`direct: true`; the session is then read again so `customer.newsletter` says it); a customer whose address is not verified, or a guest (who has to say which address), gets an email with a link (`direct: false`). Asked by someone who is NOT logged in with a REGISTERED address (the store does not look at whether it is verified, for a guest): subscribing sends the owner an email and changes nothing (`direct: false`), unless the address is already subscribed, which it answers `direct: true` (so a guest can learn that an address is subscribed); cancelling sends an email only if it is subscribed, and answers `direct: false` either way. The store limits subscribing and cancelling to 5 a day each per IP. A signup's `newsletter: true` is applied only when the address is verified. Seen on the real store: a logged-in customer already subscribed gets `direct: false` and no email; a guest cancelling an address that was never confirmed gets `direct: false` (an email); a customer cancelling when not subscribed gets `direct: true`. **The addresses** (`src/addresses/`, `commerce.addresses`; `useAddresses(commerce)` in `/react`): a CUSTOMER's addresses are the store's (`/store-customers-addresses`): every action is a call of its and what is shown is what the store holds afterwards (making one the default takes it from the one that was). A GUEST's addresses are kept NOWHERE: in memory, for as long as the page lives (a checkout needs one to ship to and one to bill), with an id of their own (`local-…`); they do not go to a customer when somebody logs in, nor a customer's to a guest. `remember(input)` is what a checkout calls with an address just entered: it keeps it if the customer does not have it already (the same address whatever case and spacing; being a default does not make it another) and gives back the one that is kept. A store keeps 5 addresses for a customer by default (it depends on the plan); the one that goes over is answered with a **429 and no code** ("Too many store_customer_addresss. Limit is 5."), like a rate limit: `dataLimitOf(error)` tells it apart (from the message) and says the limit. **What the customer bought** (`src/orders/`, `commerce.orders`; `useOrders`, `useOrder`, `useSubscriptions`, `useSubscription` in `/react`): the customer's orders, one order and their subscriptions, a page at a time (`page`, `limit` 1 to 100 and 25 by default, `sortBy`, `sortDirection`, `lang`), as RESOURCES of the same cache the catalog is in (`useResource` shows them; a page keeps showing the one before while the next comes). They are the customer's own: the key says whose, there is no resource (`idle` in a hook) when nobody is logged in, and when the customer goes (a logout, another login) what was kept for them is forgotten (`createCustomerResources`, which is what the returns, the reviews and the rest of what a customer reads will be made with). `commerce.orders.refresh()` forgets what was kept (a customer who has just bought wants to see it). An order's items carry `orderItemId`, which a return request names. Held against a real store: the pages (empty, with their pagination), the refusals (404 for what is not the customer's, 400 for a page that is not one) and the cache; the shape of an order that is NOT empty is the types' (the store's code and documentation), to be held against a store that has orders. **Returns** (`src/returns/`, `commerce.returns`; `useReturnRequests`, `useReturnRequest`, `useGuestReturnSession` in `/react`): a CUSTOMER's requests are resources of the cache (their own, as the orders are: `list`, `get`), and `create(input)` / `cancel(id)` forget what was kept so the next read is what the store holds. The `orderItemId`s are the ones of an order's items (`commerce.orders`). Only a `pending` request can be cancelled (the store answers 400 to any other). A GUEST (someone who bought with no account) goes through a LINK: `guest.requestLink({ customerEmail, orderId | reference })` asks the store to email it to the address of the order (it answers the same whether the order exists or not; an order that belongs to an account is refused, 400: its customer logs in). **The link leads to the SITE's page `/returns?token=…`** (the store builds it from the draft's domain): the site needs that page, which reads `token` from the address and shows `useGuestReturnSession(commerce, token)` (the order and its items to choose from); `guest.create({ token, items, reason, shippingAddress, billingAddress })` and `guest.cancel(id, token)` prove who they are by the token (401 for one that is not good or has ended). Held against a real store: what it says to a customer with no orders (an empty page, 404 for an order or a request that is not theirs), to a guest asking for a link, and to a token that is not good; NOT held: a request that is made (it needs an order) and the link itself (it is emailed): their shapes are the types' (the store's code). **The wishlist** (`src/wishlist/`, `commerce.wishlist`; `useWishlist(commerce)` in `/react`): a CUSTOMER's, kept by the store (`/store-customer-wishlists`: the list and one call that is a SWITCH, adding a variant that is not in it and removing one that is). A guest has none: the switch refuses with `LOGIN_REQUIRED` (for the screen to ask them to log in) and the store is not asked. The state is what a heart needs: `variantIds` (which variants are in it, `has(variantId)` in the hook, `isInWishlist`), `count`, `enabled`. A switch shows at once and is settled with what the store holds (read again afterwards); one the store refuses throws what it said and the state goes back to what it was; switches go one at a time. A store can turn the wishlist off (403 "Wishlist is disabled for this store"): `enabled` is `false` from then and the screen can hide its hearts (nothing says it before the first ask: the store's `store_data` does not carry the flag). The store keeps 50 variants for each customer by default; the one that goes over is a 429 with no code (`dataLimitOf`). **Seen on the real store, and it is the store's to fix:** AT the limit even a removal is refused (the same 429): the rule counts the items plus one on every POST of the switch, and removing is the same POST, so a customer who has 50 can not take one out. **Reviews, written** (`src/reviews/`, `commerce.reviews`; `useMyReviews`, `useReviewAbility` in `/react`): a customer's own reviews (`mine`, a resource of the cache: theirs, none when nobody is logged in), and `create(variantId, { rating 1-5, title 2-100, comment 10-2000 })`, `update(id, patch)`, `remove(id)`: the store answers them in `data` and the facade gives that back unwrapped. The store lets only a customer whose address is VERIFIED write (`EMAIL_NOT_VERIFIED`, 400; a change asks for it too, a removal does not), one review for each variant (`REVIEW_ALREADY_EXISTS`, 409) and can turn reviews off (403). `reviewAbility(authState)` / `useReviewAbility` is the hint for the screen (`login`, `verify`, `allowed`: the store decides, the session may be one verification behind). A guest is asked to log in (`LOGIN_REQUIRED`) without asking the store. Writing, changing or removing forgets what was kept of the customer's reviews AND what the catalog kept of that product's reviews (`products.getReviews`; all of them after a removal, which does not say the product), so the next read shows it. Held against a real store: the customer's page (empty), the refusals and the store's rule; NOT held: a review that is written, changed and removed (it needs a verified address, which takes the link in the email): its shape is the types' (the store's code). **What the package does NOT check (decided 14/10).** The store is the source of truth, so the package does not validate what the store validates, and never refuses a request the store might accept: an address, a return, a review, a quantity, a page or a text that is too big or too small is SENT, and the store's 400 comes back as a `CommerceError` (`issues` lists the fields, `code` is what an interface translates). The types (`AddressInput`, `ReviewInput`, `CreateReturnInput`, `CartLineInput`…) are where a screen is told at compile time that something is not right: a type error is a warning, not a request that is not made. This keeps a version of the package working against a store whose rules moved. What stays is not validation: a guest has no wishlist or reviews (`LOGIN_REQUIRED`, the store is not asked: that will not change), `reviewAbility` and `newsletterStrategy` as hints for the screen (the store decides), the guest's cart keeps one line for each variant (adding to a variant adds), and `sameAddress` for `remember`. **`createCommerce({ storeId, commerceApiUrl, catalogApiUrl, … })`** makes what a page needs of ONE store together: `auth` (the session, over the commerce API), `catalog` (cached, over the catalog API: another address) and their `cache`; plain configuration, no environment. **`@webround/commerce/site`** is the store of THIS site, made from what it was built with (`wrSiteEnv` of `@webround/core`: `VITE_WR_DRAFT_ID`, which is the `storeId`, `VITE_WR_COMMERCE_API_URL`, `VITE_WR_CATALOG_API_URL`): `import { commerce } from '@webround/commerce/site'`, then `useAuth(commerce.auth)`. It is made the first time `commerce.auth`/`.catalog` is read, not when it is imported: a site built without the variables fails where it uses the store and says which are missing (`commerceFromEnv(env, options)` is the same with an environment you give). **`useAuth(auth)`** in `/react` (`auth` is what `createAuth` made, a module of the site makes it once and components import it; no Provider): `{ state, status, customer, challenge, actions }`, `actions` = `signup`, `login`, `verifyTwoFactor`, `cancelChallenge`, `logout` (one object, the same at every render). A call that fails THROWS what the store said: it belongs to the form that made it. The first component that mounts takes the customer back (`restore`, once); a server render says `unknown` and asks nothing; one refresh at a time; a refresh the store REFUSES (401/403) ends the session, one that cannot be reached
|
|
58
|
+
(two-factor). It renews the JWT a minute before it ends (a timer) and before it is used; a renewal that could not reach the store is asked again after 2, 4, 8, 16 and 30 seconds, for as long as the JWT is good (a page that loaded with no network: ten asks); a `WakePort` (`browserWakePort`: tab visible again, `online`, `pageshow`) makes it look at the JWT against the clock when the page is back, since a timer does not run while a machine sleeps; `dispose()` stops the timer and the port; **The cart's calls** (`src/cart/`, `createCartApi(http)`; step 1 of the cart): `get`, `addItem`, `updateItem`, `removeItem`, `merge` (these need the JWT: pass the client of `createAuth`), `total` and `verifyPromotions` (with or without one: a guest's lines are in the page and `total` prices them). What the real store does, found by `smoke:cart` and written in the types: `addItem` ADDS to a variant that is already there and answers what happened, not the cart (ask with `get`); a cart has 99 different variants at most and ONE currency and cadence (`CART_LIMIT_REACHED`, `CART_CURRENCY_MISMATCH`, in `CART_ERROR_CODES`); the two totals count only the lines that are AVAILABLE (`isAvailable`); `total` leaves out a variant the store does not have (it does not refuse it) and gives a guest's lines no `id`; `verifyPromotions` with no codes, or with one the store does not know, is 404 `PROMOTION_INVALID`, and wants `cartTotal` as a text of digits; an option of a line is `{ optionName, tagValue, tagValueAlias }` in what `total` answers (the value in the language asked) and `{ optionName, optionValue }` in a customer's cart (the value as it was saved: another thing, not another name). Next: the cart's store (a guest's lines in `localStorage`, the customer's cart, `merge` at login, following `useAuth`), then `useCart`. **The cart's store** (`src/cart/store.ts`, `commerce.cart`; `useCart(commerce)` in `/react`): not logged in, the cart is a guest's: lines kept in the browser (`CartStorage` over a `KeyValuePort`, `localStorage` by default, memory where there is none; key `wrCart_<storeId>`) and priced by the store's `total`. Logged in, it is the customer's and every action is a call of the store's. When a customer logs in or signs up with lines in the browser they are merged with the store's `merge`, and forgotten ONLY once the store has them (a merge that could not be asked for loses nothing and is asked again at the next read; one the store refuses for the lines, 400/404/409/422, is not asked again until the next login; a 403 is asked again: the store answers it right after a signup). State: `lines`/`count` (what the cart IS: known at once) apart from `priced`/`isStale` (what it COSTS: the store's answer, which may be one change behind). Actions (`add`, `setQuantity`, `remove`, `refresh`) go one at a time; a customer's shows at once and is settled with one read when the last is done; a refused one THROWS what the store said and the cart goes back to what the store holds. The cart's `cartTrackingId` (the store's analytics id) is made the first time it is needed, sent with every read and pricing, kept from the guest's cart to the customer's after logging in, and forgotten when the cart is empty. Logging out gives the browser's cart back (empty if its lines were merged): the customer's does not come along. To step a quantity read it from `state.lines`, not from `priced`. A server render says `unknown` and zero items, as the first render in the browser does. **The account** (`src/account/`, `commerce.account`; `useAccount(commerce)` in `/react`): the requests a customer makes about their own account, each one call of the store's. `requestPasswordReset(email)`, `resendVerification()`, `requestAccountDeletion()` only ASK: the store sends an email and the page it leads to (where a password is chosen, an address is verified, a deletion is confirmed) is the store's own. `startTwoFactorSetup()` gives the secret and the address an authenticator app takes; `confirmTwoFactor(code)` turns it on, takes the new session (the customer is `twoFactorEnabled`) and gives the RECOVERY CODES, once (turning it off does not exist in the store). The newsletter is a strategy the store applies and `newsletterStrategy(authState)` tells the screen: a logged-in customer with a verified address is done AT ONCE with their own address (`direct: true`; the session is then read again so `customer.newsletter` says it); a customer whose address is not verified, or a guest (who has to say which address), gets an email with a link (`direct: false`). Asked by someone who is NOT logged in with a REGISTERED address (the store does not look at whether it is verified, for a guest): subscribing sends the owner an email and changes nothing (`direct: false`), unless the address is already subscribed, which it answers `direct: true` (so a guest can learn that an address is subscribed); cancelling sends an email only if it is subscribed, and answers `direct: false` either way. The store limits subscribing and cancelling to 5 a day each per IP. A signup's `newsletter: true` is applied only when the address is verified. Seen on the real store: a logged-in customer already subscribed gets `direct: false` and no email; a guest cancelling an address that was never confirmed gets `direct: false` (an email); a customer cancelling when not subscribed gets `direct: true`. **The addresses** (`src/addresses/`, `commerce.addresses`; `useAddresses(commerce)` in `/react`): a CUSTOMER's addresses are the store's (`/store-customers-addresses`): every action is a call of its and what is shown is what the store holds afterwards (making one the default takes it from the one that was). A GUEST's addresses are kept NOWHERE: in memory, for as long as the page lives (a checkout needs one to ship to and one to bill), with an id of their own (`local-…`); they do not go to a customer when somebody logs in, nor a customer's to a guest. `remember(input)` is what a checkout calls with an address just entered: it keeps it if the customer does not have it already (the same address whatever case and spacing; being a default does not make it another) and gives back the one that is kept. A store keeps 5 addresses for a customer by default (it depends on the plan); the one that goes over is answered with a **429 and no code** ("Too many store_customer_addresss. Limit is 5."), like a rate limit: `dataLimitOf(error)` tells it apart (from the message) and says the limit. **What the customer bought** (`src/orders/`, `commerce.orders`; `useOrders`, `useOrder`, `useSubscriptions`, `useSubscription` in `/react`): the customer's orders, one order and their subscriptions, a page at a time (`page`, `limit` 1 to 100 and 25 by default, `sortBy`, `sortDirection`, `lang`), as RESOURCES of the same cache the catalog is in (`useResource` shows them; a page keeps showing the one before while the next comes). They are the customer's own: the key says whose, there is no resource (`idle` in a hook) when nobody is logged in, and when the customer goes (a logout, another login) what was kept for them is forgotten (`createCustomerResources`, which is what the returns, the reviews and the rest of what a customer reads will be made with). `commerce.orders.refresh()` forgets what was kept (a customer who has just bought wants to see it). An order's items carry `orderItemId`, which a return request names. Held against a real store: the pages (empty, with their pagination), the refusals (404 for what is not the customer's, 400 for a page that is not one) and the cache; the shape of an order that is NOT empty is the types' (the store's code and documentation), to be held against a store that has orders. **Returns** (`src/returns/`, `commerce.returns`; `useReturnRequests`, `useReturnRequest`, `useGuestReturnSession` in `/react`): a CUSTOMER's requests are resources of the cache (their own, as the orders are: `list`, `get`), and `create(input)` / `cancel(id)` forget what was kept so the next read is what the store holds. The `orderItemId`s are the ones of an order's items (`commerce.orders`). Only a `pending` request can be cancelled (the store answers 400 to any other). A GUEST (someone who bought with no account) goes through a LINK: `guest.requestLink({ customerEmail, orderId | reference })` asks the store to email it to the address of the order (it answers the same whether the order exists or not; an order that belongs to an account is refused, 400: its customer logs in). **The link leads to the SITE's page `/returns?token=…`** (the store builds it from the draft's domain): the site needs that page, which reads `token` from the address and shows `useGuestReturnSession(commerce, token)` (the order and its items to choose from); `guest.create({ token, items, reason, shippingAddress, billingAddress })` and `guest.cancel(id, token)` prove who they are by the token (401 for one that is not good or has ended). Held against a real store: what it says to a customer with no orders (an empty page, 404 for an order or a request that is not theirs), to a guest asking for a link, and to a token that is not good; NOT held: a request that is made (it needs an order) and the link itself (it is emailed): their shapes are the types' (the store's code). **The wishlist** (`src/wishlist/`, `commerce.wishlist`; `useWishlist(commerce)` in `/react`): a CUSTOMER's, kept by the store (`/store-customer-wishlists`: the list and one call that is a SWITCH, adding a variant that is not in it and removing one that is). A guest has none: the switch refuses with `LOGIN_REQUIRED` (for the screen to ask them to log in) and the store is not asked. The state is what a heart needs: `variantIds` (which variants are in it, `has(variantId)` in the hook, `isInWishlist`), `count`, `enabled`. A switch shows at once and is settled with what the store holds (read again afterwards); one the store refuses throws what it said and the state goes back to what it was; switches go one at a time. A store can turn the wishlist off (403 "Wishlist is disabled for this store"): `enabled` is `false` from then and the screen can hide its hearts (nothing says it before the first ask: the store's `store_data` does not carry the flag). The store keeps 50 variants for each customer by default; the one that goes over is a 429 with no code (`dataLimitOf`). **Seen on the real store, and it is the store's to fix:** AT the limit even a removal is refused (the same 429): the rule counts the items plus one on every POST of the switch, and removing is the same POST, so a customer who has 50 can not take one out. **Reviews, written** (`src/reviews/`, `commerce.reviews`; `useMyReviews`, `useReviewAbility` in `/react`): a customer's own reviews (`mine`, a resource of the cache: theirs, none when nobody is logged in), and `create(variantId, { rating 1-5, title 2-100, comment 10-2000 })`, `update(id, patch)`, `remove(id)`: the store answers them in `data` and the facade gives that back unwrapped. The store lets only a customer whose address is VERIFIED write (`EMAIL_NOT_VERIFIED`, 400; a change asks for it too, a removal does not), one review for each variant (`REVIEW_ALREADY_EXISTS`, 409) and can turn reviews off (403). `reviewAbility(authState)` / `useReviewAbility` is the hint for the screen (`login`, `verify`, `allowed`: the store decides, the session may be one verification behind). A guest is asked to log in (`LOGIN_REQUIRED`) without asking the store. Writing, changing or removing forgets what was kept of the customer's reviews AND what the catalog kept of that product's reviews (`products.getReviews`; all of them after a removal, which does not say the product), so the next read shows it. Held against a real store: the customer's page (empty), the refusals and the store's rule; NOT held: a review that is written, changed and removed (it needs a verified address, which takes the link in the email): its shape is the types' (the store's code). **What the package does NOT check (decided 14/10).** The store is the source of truth, so the package does not validate what the store validates, and never refuses a request the store might accept: an address, a return, a review, a quantity, a page or a text that is too big or too small is SENT, and the store's 400 comes back as a `CommerceError` (`issues` lists the fields, `code` is what an interface translates). The types (`AddressInput`, `ReviewInput`, `CreateReturnInput`, `CartLineInput`…) are where a screen is told at compile time that something is not right: a type error is a warning, not a request that is not made. This keeps a version of the package working against a store whose rules moved. What stays is not validation: a guest has no wishlist or reviews (`LOGIN_REQUIRED`, the store is not asked: that will not change), `reviewAbility` and `newsletterStrategy` as hints for the screen (the store decides), the guest's cart keeps one line for each variant (adding to a variant adds), and `sameAddress` for `remember`. **`createCommerce({ storeId, commerceApiUrl, catalogApiUrl, … })`** makes what a page needs of ONE store together: `auth` (the session, over the commerce API), `catalog` (cached, over the catalog API: another address) and their `cache`; plain configuration, no environment. **`@webround/commerce-react/site`** is the store of THIS site, made from what it was built with (`wrSiteEnv` of `@webround/core`: `VITE_WR_DRAFT_ID`, which is the `storeId`, `VITE_WR_COMMERCE_API_URL`, `VITE_WR_CATALOG_API_URL`): `import { commerce } from '@webround/commerce-react/site'`, then `useAuth(commerce.auth)`. It is made the first time `commerce.auth`/`.catalog` is read, not when it is imported: a site built without the variables fails where it uses the store and says which are missing (`commerceFromEnv(env, options)` is the same with an environment you give). **`useAuth(auth)`** in `/react` (`auth` is what `createAuth` made, a module of the site makes it once and components import it; no Provider): `{ state, status, customer, challenge, actions }`, `actions` = `signup`, `login`, `verifyTwoFactor`, `cancelChallenge`, `logout` (one object, the same at every render). A call that fails THROWS what the store said: it belongs to the form that made it. The first component that mounts takes the customer back (`restore`, once); a server render says `unknown` and asks nothing; one refresh at a time; a refresh the store REFUSES (401/403) ends the session, one that cannot be reached
|
|
59
59
|
does not (it keeps what is good, with `error`); a renewal that comes back after a logout does not bring the session back; logout never throws. `createAuth` makes the store with a client that carries no
|
|
60
60
|
JWT (it asks for the JWT) and gives the site another that carries it. Seen against the real store: it answers a signup/login from inside its transaction, so a call made at once can be refused
|
|
61
61
|
(a refresh: 401 "Invalid Token"; a call with the JWT: 403 "Customer not found"; a logout: 401 "Token mismatch"; the second step of a two-factor login: 404 "Challenge not found"): it
|
|
@@ -79,7 +79,7 @@ answer to an older question is thrown away. `isStale` says the answer on show is
|
|
|
79
79
|
A refused preview throws what the store said, leaves it in `state.error` and keeps the last good one on show as stale. `ensureQuote()` is what a page does by itself: it asks unless the cart is empty or this very question was already asked (by anyone:
|
|
80
80
|
a refusal is not asked again by itself, any number of components can call it); `useCheckout` calls it when the cart or the choices change (`autoQuote: false` turns that off). Nothing is checked here: a cart with no lines, a missing delivery or a
|
|
81
81
|
code that does not exist is sent, and the store's refusal comes back. What the checkout refuses on purpose has a `code` (`CHECKOUT_ERROR_CODES`: `CHECKOUT_BLOCKED` with `{ hookId, code, target }`, `CHECKOUT_HOOK_FAILED`, `SHIPPING_METHOD_UNAVAILABLE`) and
|
|
82
|
-
`checkoutRefusalOf(error)` reads it with its `meta` typed. **Lines with not enough stock** (19/10): the preview does not refuse them, it SETS THEM APART: `quote.unavailable` has them (`QuoteUnavailableLine`: priced like the others but without discounts, with `availableStock`; the whole line even if some units exist), and `quote.items` and `quote.totals` count only what can be bought. The preview is still asked WITH every line (it is what says which ones are unavailable); the payment is not: `pay()` leaves out the lines the last preview said are unavailable (and their delivery method), and when none is left it throws `NOTHING_TO_PAY` without asking the store. A page pays with a fresh preview; the store's own 400 (`Insufficient stock…`, the last unit sold between the preview and the payment) is what is left, and the page asks for the preview again. **The preview describes the lines too** (19/10): each line of `items` and of `unavailable` carries `variantName`, `productName`, `variantSlug`, `skuCode`, `coverUrl`, `options`, `allowedDeliveryMethods` (what `deliveryMethods` can choose from), `availableStock`, `allowBackorder` and `maxOrderQuantity` (`QuoteLinePresentation`): in the v2 flow it is the ONE call that prices a cart and describes it, and it replaces `/cart/total` (the store reads these from OpenSearch like `/cart/total` does). A site that works this way makes its store with `cartPricing: false` (`CartStoreOptions.pricing`; `@webround/commerce/site` does): a guest's lines are kept and counted, `priced` stays null and `/cart/total` is never asked; a customer's cart is still read with `/cart/get`, which is where its lines are. **Delivery**: the preview does not need the customer's delivery choice for every line: one without a choice gets the product's default (shipping, else pickup, else none) in the line's `deliveryMethod`, and `allowedDeliveryMethods` says what else it could be; `pay()` sends the customer's choice or, for a line they did not choose for, the one the last preview gave it. **Tracking**: the store's preview sends the cart's analytics event (`cartTrackingId`, as `/cart/total` did), so a site that never calls `/cart/total` is still tracked. **Paying**: `pay(returnUrls?)` builds the order's body from the same choices, `email` (a guest's: who the order is for) and, for an invoice, both codes (a missing one goes as an empty text for the store to
|
|
82
|
+
`checkoutRefusalOf(error)` reads it with its `meta` typed. **Lines with not enough stock** (19/10): the preview does not refuse them, it SETS THEM APART: `quote.unavailable` has them (`QuoteUnavailableLine`: priced like the others but without discounts, with `availableStock`; the whole line even if some units exist), and `quote.items` and `quote.totals` count only what can be bought. The preview is still asked WITH every line (it is what says which ones are unavailable); the payment is not: `pay()` leaves out the lines the last preview said are unavailable (and their delivery method), and when none is left it throws `NOTHING_TO_PAY` without asking the store. A page pays with a fresh preview; the store's own 400 (`Insufficient stock…`, the last unit sold between the preview and the payment) is what is left, and the page asks for the preview again. **The preview describes the lines too** (19/10): each line of `items` and of `unavailable` carries `variantName`, `productName`, `variantSlug`, `skuCode`, `coverUrl`, `options`, `allowedDeliveryMethods` (what `deliveryMethods` can choose from), `availableStock`, `allowBackorder` and `maxOrderQuantity` (`QuoteLinePresentation`): in the v2 flow it is the ONE call that prices a cart and describes it, and it replaces `/cart/total` (the store reads these from OpenSearch like `/cart/total` does). A site that works this way makes its store with `cartPricing: false` (`CartStoreOptions.pricing`; `@webround/commerce-react/site` does): a guest's lines are kept and counted, `priced` stays null and `/cart/total` is never asked; a customer's cart is still read with `/cart/get`, which is where its lines are. **Delivery**: the preview does not need the customer's delivery choice for every line: one without a choice gets the product's default (shipping, else pickup, else none) in the line's `deliveryMethod`, and `allowedDeliveryMethods` says what else it could be; `pay()` sends the customer's choice or, for a line they did not choose for, the one the last preview gave it. **Tracking**: the store's preview sends the cart's analytics event (`cartTrackingId`, as `/cart/total` did), so a site that never calls `/cart/total` is still tracked. **Paying**: `pay(returnUrls?)` builds the order's body from the same choices, `email` (a guest's: who the order is for) and, for an invoice, both codes (a missing one goes as an empty text for the store to
|
|
83
83
|
refuse); it does not redirect (the site sends the customer to `url`) and is asked once per order. Where the customer comes back to is the argument, else `createCommerce({ returnUrls })`, else `browserReturnUrls` (this page's origin: `/checkout/success?clean_cart=true`
|
|
84
84
|
and `/`, what the site has always sent; the store accepts only its own origin, an allowed one or localhost). **After the payment** (the success page): `commerce.cart.clear()` (a guest's lines and tracking id forgotten in the browser; a customer's cart,
|
|
85
85
|
which the store emptied by itself, is read again) or `commerce.checkout.complete()` (that, and the choices and the preview forgotten). `commerce.checkout` needs `checkoutApiUrl`: a store made without it works, and it throws, saying so, where the checkout is read.
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @webround/commerce: the logic of a Webround Commerce store (its catalog, cart, customers, checkout), with no UI and no framework.
|
|
2
|
-
// What a framework needs to use it comes as
|
|
2
|
+
// What a framework needs to use it comes as a package of its own (`@webround/commerce-react`, one day `@webround/commerce-next`…), never in here.
|
|
3
3
|
export { cacheKey, cachePrefix, createCache } from './cache.js';
|
|
4
4
|
export { canonicalize, canonicalJson } from './canonical.js';
|
|
5
5
|
export { CommerceError, commerceErrorFromResponse, dataLimitOf, isCommerceError, toCommerceError } from './errors.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webround/commerce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -10,16 +10,6 @@
|
|
|
10
10
|
"import": "./dist/index.js",
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"development": "./src/index.ts"
|
|
13
|
-
},
|
|
14
|
-
"./react": {
|
|
15
|
-
"import": "./dist/react/index.js",
|
|
16
|
-
"types": "./dist/react/index.d.ts",
|
|
17
|
-
"development": "./src/react/index.ts"
|
|
18
|
-
},
|
|
19
|
-
"./site": {
|
|
20
|
-
"import": "./dist/site.js",
|
|
21
|
-
"types": "./dist/site.d.ts",
|
|
22
|
-
"development": "./src/site.ts"
|
|
23
13
|
}
|
|
24
14
|
},
|
|
25
15
|
"files": [
|
|
@@ -41,25 +31,7 @@
|
|
|
41
31
|
"smoke:wishlist": "node smoke/wishlist.smoke.ts",
|
|
42
32
|
"smoke:reviews": "node smoke/reviews.smoke.ts"
|
|
43
33
|
},
|
|
44
|
-
"peerDependencies": {
|
|
45
|
-
"react": ">=19",
|
|
46
|
-
"@webround/core": "^0.2.0"
|
|
47
|
-
},
|
|
48
|
-
"peerDependenciesMeta": {
|
|
49
|
-
"react": {
|
|
50
|
-
"optional": true
|
|
51
|
-
},
|
|
52
|
-
"@webround/core": {
|
|
53
|
-
"optional": true
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
34
|
"devDependencies": {
|
|
57
|
-
"@types/jsdom": "^30.0.0",
|
|
58
|
-
"@types/react": "^19.3.0",
|
|
59
|
-
"@types/react-dom": "^19.3.0",
|
|
60
|
-
"jsdom": "^26.1.0",
|
|
61
|
-
"react": "^19.3.0",
|
|
62
|
-
"react-dom": "^19.3.0",
|
|
63
35
|
"typescript": "^5.5.0"
|
|
64
36
|
},
|
|
65
37
|
"publishConfig": {
|
package/dist/react/index.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export { useResource } from './use-resource.js';
|
|
2
|
-
export type { UseResourceOptions, UseResourceResult } from './use-resource.js';
|
|
3
|
-
export { useShop } from './use-shop.js';
|
|
4
|
-
export type { UseShopOptions, UseShopResult } from './use-shop.js';
|
|
5
|
-
export { useShopAddress } from './use-shop-address.js';
|
|
6
|
-
export type { UseShopAddressOptions } from './use-shop-address.js';
|
|
7
|
-
export { authActions, useAuth } from './use-auth.js';
|
|
8
|
-
export type { AuthActions, UseAuthResult } from './use-auth.js';
|
|
9
|
-
export { cartActions, useCart } from './use-cart.js';
|
|
10
|
-
export type { CartActions, UseCartResult } from './use-cart.js';
|
|
11
|
-
export { useAccount } from './use-account.js';
|
|
12
|
-
export type { UseAccountResult } from './use-account.js';
|
|
13
|
-
export { checkoutActions, useCheckout } from './use-checkout.js';
|
|
14
|
-
export type { CheckoutActions, UseCheckoutOptions, UseCheckoutResult } from './use-checkout.js';
|
|
15
|
-
export { addressActions, useAddresses } from './use-addresses.js';
|
|
16
|
-
export type { AddressActions, UseAddressesResult } from './use-addresses.js';
|
|
17
|
-
export { useCustomerResource } from './use-customer-resource.js';
|
|
18
|
-
export { useOrder, useOrders, useSubscription, useSubscriptions } from './use-orders.js';
|
|
19
|
-
export { useGuestReturnSession, useReturnRequest, useReturnRequests } from './use-returns.js';
|
|
20
|
-
export { useWishlist } from './use-wishlist.js';
|
|
21
|
-
export type { UseWishlistResult } from './use-wishlist.js';
|
|
22
|
-
export { useMyReviews, useReviewAbility } from './use-reviews.js';
|
|
23
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/react/index.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// @webround/commerce/react: what a React component needs to use a store (React 19). The logic is `@webround/commerce`'s; this only binds it to components.
|
|
2
|
-
export { useResource } from './use-resource.js';
|
|
3
|
-
export { useShop } from './use-shop.js';
|
|
4
|
-
export { useShopAddress } from './use-shop-address.js';
|
|
5
|
-
export { authActions, useAuth } from './use-auth.js';
|
|
6
|
-
export { cartActions, useCart } from './use-cart.js';
|
|
7
|
-
export { useAccount } from './use-account.js';
|
|
8
|
-
export { checkoutActions, useCheckout } from './use-checkout.js';
|
|
9
|
-
export { addressActions, useAddresses } from './use-addresses.js';
|
|
10
|
-
export { useCustomerResource } from './use-customer-resource.js';
|
|
11
|
-
export { useOrder, useOrders, useSubscription, useSubscriptions } from './use-orders.js';
|
|
12
|
-
export { useGuestReturnSession, useReturnRequest, useReturnRequests } from './use-returns.js';
|
|
13
|
-
export { useWishlist } from './use-wishlist.js';
|
|
14
|
-
export { useMyReviews, useReviewAbility } from './use-reviews.js';
|
|
15
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { type Account, type NewsletterStrategy } from '../account/account.js';
|
|
2
|
-
import type { Auth } from '../auth/auth.js';
|
|
3
|
-
export interface UseAccountResult {
|
|
4
|
-
/** What subscribing to, or cancelling, the newsletter will do now. */
|
|
5
|
-
newsletter: NewsletterStrategy;
|
|
6
|
-
actions: Account;
|
|
7
|
-
}
|
|
8
|
-
export declare function useAccount(commerce: {
|
|
9
|
-
account: Account;
|
|
10
|
-
auth: Pick<Auth, 'store'>;
|
|
11
|
-
}): UseAccountResult;
|
|
12
|
-
//# sourceMappingURL=use-account.d.ts.map
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { useMemo, useSyncExternalStore } from 'react';
|
|
2
|
-
import { newsletterStrategy } from '../account/account.js';
|
|
3
|
-
export function useAccount(commerce) {
|
|
4
|
-
const { store } = commerce.auth;
|
|
5
|
-
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
6
|
-
const newsletter = useMemo(() => newsletterStrategy(state), [state.status, state.customer?.email, state.customer?.emailVerified]);
|
|
7
|
-
return { newsletter, actions: commerce.account };
|
|
8
|
-
}
|
|
9
|
-
//# sourceMappingURL=use-account.js.map
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { AddressBook, AddressBookState } from '../addresses/book.js';
|
|
2
|
-
export interface AddressActions {
|
|
3
|
-
save: AddressBook['save'];
|
|
4
|
-
remember: AddressBook['remember'];
|
|
5
|
-
update: AddressBook['update'];
|
|
6
|
-
remove: AddressBook['remove'];
|
|
7
|
-
setDefault: AddressBook['setDefault'];
|
|
8
|
-
refresh: AddressBook['refresh'];
|
|
9
|
-
}
|
|
10
|
-
export interface UseAddressesResult {
|
|
11
|
-
state: AddressBookState;
|
|
12
|
-
addresses: AddressBookState['addresses'];
|
|
13
|
-
actions: AddressActions;
|
|
14
|
-
}
|
|
15
|
-
export declare function addressActions(book: AddressBook): AddressActions;
|
|
16
|
-
/** `commerce` is what `createCommerce` made (or `@webround/commerce/site`'s). Any number of components can use it: the book starts once. A server render says `unknown` and no addresses. */
|
|
17
|
-
export declare function useAddresses(commerce: {
|
|
18
|
-
addresses: AddressBook;
|
|
19
|
-
}): UseAddressesResult;
|
|
20
|
-
//# sourceMappingURL=use-addresses.d.ts.map
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useSyncExternalStore } from 'react';
|
|
2
|
-
export function addressActions(book) {
|
|
3
|
-
return {
|
|
4
|
-
save: (input) => book.save(input),
|
|
5
|
-
remember: (input) => book.remember(input),
|
|
6
|
-
update: (id, patch) => book.update(id, patch),
|
|
7
|
-
remove: (id) => book.remove(id),
|
|
8
|
-
setDefault: (id, kind) => book.setDefault(id, kind),
|
|
9
|
-
refresh: () => book.refresh(),
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
/** `commerce` is what `createCommerce` made (or `@webround/commerce/site`'s). Any number of components can use it: the book starts once. A server render says `unknown` and no addresses. */
|
|
13
|
-
export function useAddresses(commerce) {
|
|
14
|
-
const { addresses: book } = commerce;
|
|
15
|
-
const state = useSyncExternalStore(book.subscribe, book.getState, book.getState);
|
|
16
|
-
const actions = useMemo(() => addressActions(book), [book]);
|
|
17
|
-
useEffect(() => { void book.start(); }, [book]);
|
|
18
|
-
return { state, addresses: state.addresses, actions };
|
|
19
|
-
}
|
|
20
|
-
//# sourceMappingURL=use-addresses.js.map
|
package/dist/react/use-auth.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { Auth } from '../auth/auth.js';
|
|
2
|
-
import type { AuthState, AuthStatus, AuthStore, Customer } from '../auth/store.js';
|
|
3
|
-
import type { TwoFactorChallenge } from '../auth/types.js';
|
|
4
|
-
/** What changes the session. A call that fails THROWS what the store said (a wrong password, a validation list in `issues`): it belongs to the form that made it. */
|
|
5
|
-
export interface AuthActions {
|
|
6
|
-
signup: AuthStore['signup'];
|
|
7
|
-
login: AuthStore['login'];
|
|
8
|
-
verifyTwoFactor: AuthStore['verifyTwoFactor'];
|
|
9
|
-
cancelChallenge: AuthStore['cancelChallenge'];
|
|
10
|
-
logout: AuthStore['logout'];
|
|
11
|
-
}
|
|
12
|
-
export interface UseAuthResult {
|
|
13
|
-
state: AuthState;
|
|
14
|
-
/** `unknown` until it is known whether a customer is kept (and on a server render: no window, nobody to take back). */
|
|
15
|
-
status: AuthStatus;
|
|
16
|
-
customer: Customer | null;
|
|
17
|
-
/** The code of the authenticator app is wanted (status `challenge`). */
|
|
18
|
-
challenge: TwoFactorChallenge | null;
|
|
19
|
-
actions: AuthActions;
|
|
20
|
-
}
|
|
21
|
-
export declare function authActions(store: AuthStore): AuthActions;
|
|
22
|
-
/** `auth` is what `createAuth` made. Any number of components can use it: the customer is taken back once. */
|
|
23
|
-
export declare function useAuth(auth: Pick<Auth, 'store'>): UseAuthResult;
|
|
24
|
-
//# sourceMappingURL=use-auth.d.ts.map
|
package/dist/react/use-auth.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useSyncExternalStore } from 'react';
|
|
2
|
-
export function authActions(store) {
|
|
3
|
-
return {
|
|
4
|
-
signup: (input, options) => store.signup(input, options),
|
|
5
|
-
login: (input) => store.login(input),
|
|
6
|
-
verifyTwoFactor: (code) => store.verifyTwoFactor(code),
|
|
7
|
-
cancelChallenge: () => store.cancelChallenge(),
|
|
8
|
-
logout: () => store.logout(),
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
/** `auth` is what `createAuth` made. Any number of components can use it: the customer is taken back once. */
|
|
12
|
-
export function useAuth(auth) {
|
|
13
|
-
const { store } = auth;
|
|
14
|
-
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
15
|
-
const actions = useMemo(() => authActions(store), [store]);
|
|
16
|
-
// Not in render: a server render asks nothing. `restore` is one call for whoever asks while it is under way, and nothing once it is known
|
|
17
|
-
useEffect(() => { void store.restore(); }, [store]);
|
|
18
|
-
return { state, status: state.status, customer: state.customer, challenge: state.challenge, actions };
|
|
19
|
-
}
|
|
20
|
-
//# sourceMappingURL=use-auth.js.map
|
package/dist/react/use-cart.d.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { CartState, CartStore } from '../cart/store.js';
|
|
2
|
-
export interface CartActions {
|
|
3
|
-
add: CartStore['add'];
|
|
4
|
-
setQuantity: CartStore['setQuantity'];
|
|
5
|
-
remove: CartStore['remove'];
|
|
6
|
-
/** Empties the cart (a guest's and a customer's). */
|
|
7
|
-
clear: CartStore['clear'];
|
|
8
|
-
refresh: CartStore['refresh'];
|
|
9
|
-
}
|
|
10
|
-
export interface UseCartResult {
|
|
11
|
-
state: CartState;
|
|
12
|
-
/** How many items are in the cart: known before the store has priced anything. */
|
|
13
|
-
count: number;
|
|
14
|
-
/** What the store says the cart costs (null until it said); `state.isStale` when it is one change behind. */
|
|
15
|
-
priced: CartState['priced'];
|
|
16
|
-
actions: CartActions;
|
|
17
|
-
}
|
|
18
|
-
export declare function cartActions(store: CartStore): CartActions;
|
|
19
|
-
/**
|
|
20
|
-
* To step a quantity (a "+" button), read it from `state.lines`, not from `priced`: `lines` is what the cart is NOW (a change shows in it at once), `priced` is the store's answer, which can be one change behind
|
|
21
|
-
* (`state.isStale`), so three quick clicks on a "+" that reads `priced` ask for the same quantity three times.
|
|
22
|
-
*/
|
|
23
|
-
/** `commerce` is what `createCommerce` made (or `@webround/commerce/site`'s). Any number of components can use it: the cart starts once. A server render says `unknown` and zero items, as the first render in the browser does. */
|
|
24
|
-
export declare function useCart(commerce: {
|
|
25
|
-
cart: CartStore;
|
|
26
|
-
}): UseCartResult;
|
|
27
|
-
//# sourceMappingURL=use-cart.d.ts.map
|
package/dist/react/use-cart.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useSyncExternalStore } from 'react';
|
|
2
|
-
export function cartActions(store) {
|
|
3
|
-
return {
|
|
4
|
-
add: (line) => store.add(line),
|
|
5
|
-
setQuantity: (variantId, quantity) => store.setQuantity(variantId, quantity),
|
|
6
|
-
remove: (variantId) => store.remove(variantId),
|
|
7
|
-
clear: () => store.clear(),
|
|
8
|
-
refresh: () => store.refresh(),
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* To step a quantity (a "+" button), read it from `state.lines`, not from `priced`: `lines` is what the cart is NOW (a change shows in it at once), `priced` is the store's answer, which can be one change behind
|
|
13
|
-
* (`state.isStale`), so three quick clicks on a "+" that reads `priced` ask for the same quantity three times.
|
|
14
|
-
*/
|
|
15
|
-
/** `commerce` is what `createCommerce` made (or `@webround/commerce/site`'s). Any number of components can use it: the cart starts once. A server render says `unknown` and zero items, as the first render in the browser does. */
|
|
16
|
-
export function useCart(commerce) {
|
|
17
|
-
const { cart } = commerce;
|
|
18
|
-
const state = useSyncExternalStore(cart.subscribe, cart.getState, cart.getState);
|
|
19
|
-
const actions = useMemo(() => cartActions(cart), [cart]);
|
|
20
|
-
useEffect(() => { void cart.start(); }, [cart]);
|
|
21
|
-
return { state, count: state.count, priced: state.priced, actions };
|
|
22
|
-
}
|
|
23
|
-
//# sourceMappingURL=use-cart.js.map
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { CheckoutState, CheckoutStore } from '../checkout/store.js';
|
|
2
|
-
export interface CheckoutActions {
|
|
3
|
-
setDeliveryMethod: CheckoutStore['setDeliveryMethod'];
|
|
4
|
-
setShippingAddress: CheckoutStore['setShippingAddress'];
|
|
5
|
-
setBillingAddress: CheckoutStore['setBillingAddress'];
|
|
6
|
-
selectShippingMethod: CheckoutStore['selectShippingMethod'];
|
|
7
|
-
setPromotionCodes: CheckoutStore['setPromotionCodes'];
|
|
8
|
-
setAdditionalData: CheckoutStore['setAdditionalData'];
|
|
9
|
-
setEmail: CheckoutStore['setEmail'];
|
|
10
|
-
setInvoice: CheckoutStore['setInvoice'];
|
|
11
|
-
/** Asks the preview now (what it asked automatically is asked again). Throws what the store refuses. */
|
|
12
|
-
quote: CheckoutStore['quote'];
|
|
13
|
-
/** Starts the payment; give the customer the `url` it answers (`location.assign(url)`). Throws what the store refuses. */
|
|
14
|
-
pay: CheckoutStore['pay'];
|
|
15
|
-
reset: CheckoutStore['reset'];
|
|
16
|
-
/** The payment is done (the success page): empties the cart and forgets the checkout. */
|
|
17
|
-
complete: CheckoutStore['complete'];
|
|
18
|
-
}
|
|
19
|
-
export interface UseCheckoutOptions {
|
|
20
|
-
/**
|
|
21
|
-
* Ask the preview by itself when the cart or what the customer chose changes (default: yes). Not for an empty cart, and not again for what already failed (ask again with `actions.quote`). A refusal is not thrown
|
|
22
|
-
* here: it is in `state.error`.
|
|
23
|
-
*/
|
|
24
|
-
autoQuote?: boolean;
|
|
25
|
-
}
|
|
26
|
-
export interface UseCheckoutResult {
|
|
27
|
-
state: CheckoutState;
|
|
28
|
-
/** What the store says it costs (null until it said); `state.isStale` when it is the answer to something the customer has since changed. */
|
|
29
|
-
quote: CheckoutState['quote'];
|
|
30
|
-
actions: CheckoutActions;
|
|
31
|
-
}
|
|
32
|
-
export declare function checkoutActions(store: CheckoutStore): CheckoutActions;
|
|
33
|
-
/** `commerce` is what `createCommerce` made (or `@webround/commerce/site`'s). Reading `commerce.checkout` needs the checkout API's address. */
|
|
34
|
-
export declare function useCheckout(commerce: {
|
|
35
|
-
checkout: CheckoutStore;
|
|
36
|
-
}, options?: UseCheckoutOptions): UseCheckoutResult;
|
|
37
|
-
//# sourceMappingURL=use-checkout.d.ts.map
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useSyncExternalStore } from 'react';
|
|
2
|
-
export function checkoutActions(store) {
|
|
3
|
-
return {
|
|
4
|
-
setDeliveryMethod: (variantId, method) => store.setDeliveryMethod(variantId, method),
|
|
5
|
-
setShippingAddress: (address) => store.setShippingAddress(address),
|
|
6
|
-
setBillingAddress: (address) => store.setBillingAddress(address),
|
|
7
|
-
selectShippingMethod: (id) => store.selectShippingMethod(id),
|
|
8
|
-
setPromotionCodes: (codes) => store.setPromotionCodes(codes),
|
|
9
|
-
setAdditionalData: (data) => store.setAdditionalData(data),
|
|
10
|
-
setEmail: (email) => store.setEmail(email),
|
|
11
|
-
setInvoice: (invoice) => store.setInvoice(invoice),
|
|
12
|
-
quote: (options) => store.quote(options),
|
|
13
|
-
pay: (returnUrls) => store.pay(returnUrls),
|
|
14
|
-
reset: () => store.reset(),
|
|
15
|
-
complete: () => store.complete(),
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
/** `commerce` is what `createCommerce` made (or `@webround/commerce/site`'s). Reading `commerce.checkout` needs the checkout API's address. */
|
|
19
|
-
export function useCheckout(commerce, options = {}) {
|
|
20
|
-
const { checkout } = commerce;
|
|
21
|
-
const autoQuote = options.autoQuote ?? true;
|
|
22
|
-
const state = useSyncExternalStore(checkout.subscribe, checkout.getState, checkout.getState);
|
|
23
|
-
const actions = useMemo(() => checkoutActions(checkout), [checkout]);
|
|
24
|
-
useEffect(() => { checkout.start(); }, [checkout]);
|
|
25
|
-
// Which questions were asked is the store's to remember (one for all the components): a refusal is not asked again by itself, and the same question is never asked twice
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
if (!autoQuote)
|
|
28
|
-
return;
|
|
29
|
-
checkout.ensureQuote().catch(() => { });
|
|
30
|
-
}, [checkout, autoQuote, state.requestKey, state.lineCount]);
|
|
31
|
-
return { state, quote: state.quote, actions };
|
|
32
|
-
}
|
|
33
|
-
//# sourceMappingURL=use-checkout.js.map
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { Auth } from '../auth/auth.js';
|
|
2
|
-
import type { Resource } from '../resource.js';
|
|
3
|
-
import { type UseResourceOptions, type UseResourceResult } from './use-resource.js';
|
|
4
|
-
export declare function useCustomerResource<T>(auth: Pick<Auth, 'store'>, make: () => Resource<T> | null, options?: UseResourceOptions): UseResourceResult<T>;
|
|
5
|
-
//# sourceMappingURL=use-customer-resource.d.ts.map
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { useSyncExternalStore } from 'react';
|
|
2
|
-
import { useResource } from './use-resource.js';
|
|
3
|
-
// A resource that is about the logged-in customer, shown by a component: the same as `useResource`, plus that the component follows who is logged in. Nobody logged in is no resource (`idle`), and logging in or out draws
|
|
4
|
-
// the component again with the right one (the cache forgets what was the customer's when they go).
|
|
5
|
-
export function useCustomerResource(auth, make, options) {
|
|
6
|
-
useSyncExternalStore(auth.store.subscribe, auth.store.getState, auth.store.getState);
|
|
7
|
-
return useResource(make(), options);
|
|
8
|
-
}
|
|
9
|
-
//# sourceMappingURL=use-customer-resource.js.map
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { Auth } from '../auth/auth.js';
|
|
2
|
-
import type { Orders } from '../orders/orders.js';
|
|
3
|
-
import type { Order, OrderPage, PageQuery, Subscription, SubscriptionPage } from '../orders/types.js';
|
|
4
|
-
import type { UseResourceOptions, UseResourceResult } from './use-resource.js';
|
|
5
|
-
type Source = {
|
|
6
|
-
auth: Pick<Auth, 'store'>;
|
|
7
|
-
orders: Orders;
|
|
8
|
-
};
|
|
9
|
-
export declare const useOrders: (commerce: Source, query?: PageQuery, options?: UseResourceOptions) => UseResourceResult<OrderPage>;
|
|
10
|
-
export declare const useOrder: (commerce: Source, id: string | null | undefined) => UseResourceResult<Order>;
|
|
11
|
-
export declare const useSubscriptions: (commerce: Source, query?: PageQuery, options?: UseResourceOptions) => UseResourceResult<SubscriptionPage>;
|
|
12
|
-
export declare const useSubscription: (commerce: Source, id: string | null | undefined) => UseResourceResult<Subscription>;
|
|
13
|
-
export {};
|
|
14
|
-
//# sourceMappingURL=use-orders.d.ts.map
|
package/dist/react/use-orders.js
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { useCustomerResource } from './use-customer-resource.js';
|
|
2
|
-
export const useOrders = (commerce, query, options = { keepPrevious: true }) => useCustomerResource(commerce.auth, () => commerce.orders.list(query), options);
|
|
3
|
-
export const useOrder = (commerce, id) => useCustomerResource(commerce.auth, () => (id ? commerce.orders.get(id) : null));
|
|
4
|
-
export const useSubscriptions = (commerce, query, options = { keepPrevious: true }) => useCustomerResource(commerce.auth, () => commerce.orders.subscriptions.list(query), options);
|
|
5
|
-
export const useSubscription = (commerce, id) => useCustomerResource(commerce.auth, () => (id ? commerce.orders.subscriptions.get(id) : null));
|
|
6
|
-
//# sourceMappingURL=use-orders.js.map
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { Resource } from '../resource.js';
|
|
2
|
-
export interface UseResourceOptions {
|
|
3
|
-
/**
|
|
4
|
-
* When the question changes (another page of the results, another search), keep showing the answer of the one before until the new one comes, instead of nothing: `data` is that
|
|
5
|
-
* answer and `isStale` says it is not the one asked for. Off by default: what is shown is always the answer to what is asked.
|
|
6
|
-
*/
|
|
7
|
-
keepPrevious?: boolean;
|
|
8
|
-
}
|
|
9
|
-
export interface UseResourceResult<T> {
|
|
10
|
-
/** `idle`: nothing is asked (no resource was given). `pending`: the answer is being waited for. `ready`: it is there. `error`: it could not be had. */
|
|
11
|
-
status: 'idle' | 'pending' | 'ready' | 'error';
|
|
12
|
-
/** The answer to what is asked, or (with `keepPrevious`, while the new one is awaited) the answer to the question before; undefined when there is none. */
|
|
13
|
-
data: T | undefined;
|
|
14
|
-
/** Why it failed (a `CommerceError` for what the store did, or a network error): set with `status: 'error'`, undefined otherwise. */
|
|
15
|
-
error: unknown;
|
|
16
|
-
/** The answer to what is asked is being waited for. */
|
|
17
|
-
isFetching: boolean;
|
|
18
|
-
/** `data` is the answer to an earlier question (`keepPrevious`), not to the one asked now. */
|
|
19
|
-
isStale: boolean;
|
|
20
|
-
/** Asks again after an error. It does nothing in any other state: what is there is not asked for again (to forget it, invalidate it in the catalog). */
|
|
21
|
-
retry: () => void;
|
|
22
|
-
}
|
|
23
|
-
/** A resource asked for by a component: `null`/`undefined` is "not asked yet" (a slug that is not known, a search that is not made). */
|
|
24
|
-
export declare function useResource<T>(resource: Resource<T> | null | undefined, options?: UseResourceOptions): UseResourceResult<T>;
|
|
25
|
-
//# sourceMappingURL=use-resource.d.ts.map
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
|
2
|
-
const NOTHING = { status: 'idle' };
|
|
3
|
-
const noSubscription = () => () => undefined;
|
|
4
|
-
/** A resource asked for by a component: `null`/`undefined` is "not asked yet" (a slug that is not known, a search that is not made). */
|
|
5
|
-
export function useResource(resource, options = {}) {
|
|
6
|
-
const keepPrevious = options.keepPrevious === true;
|
|
7
|
-
const key = resource?.key;
|
|
8
|
-
// A resource is made again at every render, and the same key is the same question: only a new key is a new one (what is subscribed to must not change on every render)
|
|
9
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
10
|
-
const asked = useMemo(() => resource ?? null, [key]);
|
|
11
|
-
const subscribe = useCallback((listener) => (asked ? asked.subscribe(listener) : noSubscription()), [asked]);
|
|
12
|
-
const read = useCallback(() => (asked ? asked.snapshot() : NOTHING), [asked]);
|
|
13
|
-
// The server has the same state as the client: what the cache holds when it renders (a prerender fills it before)
|
|
14
|
-
const snapshot = useSyncExternalStore(subscribe, read, read);
|
|
15
|
-
// Asks when the question is there, and gives it up when it is gone: a question nobody waits for any more is not asked (the cache sees to that)
|
|
16
|
-
const waiting = useRef(null);
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
if (!asked)
|
|
19
|
-
return;
|
|
20
|
-
const controller = (waiting.current = new AbortController());
|
|
21
|
-
if (asked.snapshot().status !== 'ready')
|
|
22
|
-
asked.load({ signal: controller.signal }).catch(() => undefined); // the failure is the state of the key, read above
|
|
23
|
-
return () => { controller.abort(); if (waiting.current === controller)
|
|
24
|
-
waiting.current = null; };
|
|
25
|
-
}, [asked]);
|
|
26
|
-
// What is watched was forgotten (it was invalidated in the catalog): it is asked again, so that what is on the screen is never "nothing" for good
|
|
27
|
-
const forgotten = asked !== null && snapshot.status === 'idle';
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
if (forgotten && waiting.current && asked && asked.snapshot().status === 'idle')
|
|
30
|
-
asked.load({ signal: waiting.current.signal }).catch(() => undefined);
|
|
31
|
-
}, [forgotten, asked]);
|
|
32
|
-
const retrying = useRef(null);
|
|
33
|
-
useEffect(() => () => { retrying.current?.abort(); retrying.current = null; }, [asked]);
|
|
34
|
-
const retry = useCallback(() => {
|
|
35
|
-
if (!asked || asked.snapshot().status !== 'error')
|
|
36
|
-
return;
|
|
37
|
-
retrying.current?.abort();
|
|
38
|
-
const controller = (retrying.current = new AbortController());
|
|
39
|
-
asked.load({ signal: controller.signal }).catch(() => undefined);
|
|
40
|
-
}, [asked]);
|
|
41
|
-
// The last answer that was shown, for `keepPrevious`: kept once it is on the screen
|
|
42
|
-
const shown = useRef(undefined);
|
|
43
|
-
const hasShown = useRef(false);
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
if (snapshot.status === 'ready') {
|
|
46
|
-
shown.current = snapshot.data;
|
|
47
|
-
hasShown.current = true;
|
|
48
|
-
}
|
|
49
|
-
}, [snapshot]);
|
|
50
|
-
if (!asked)
|
|
51
|
-
return { status: 'idle', data: undefined, error: undefined, isFetching: false, isStale: false, retry };
|
|
52
|
-
if (snapshot.status === 'ready')
|
|
53
|
-
return { status: 'ready', data: snapshot.data, error: undefined, isFetching: false, isStale: false, retry };
|
|
54
|
-
if (snapshot.status === 'error')
|
|
55
|
-
return { status: 'error', data: undefined, error: snapshot.error, isFetching: false, isStale: false, retry };
|
|
56
|
-
// `idle` here is a question that is about to be asked (the effect has not run yet): it is awaited, there is no frame in which it is "nothing"
|
|
57
|
-
const stale = keepPrevious && hasShown.current;
|
|
58
|
-
return { status: 'pending', data: stale ? shown.current : undefined, error: undefined, isFetching: true, isStale: stale, retry };
|
|
59
|
-
}
|
|
60
|
-
//# sourceMappingURL=use-resource.js.map
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { Auth } from '../auth/auth.js';
|
|
2
|
-
import type { Returns } from '../returns/returns.js';
|
|
3
|
-
import type { GuestReturnSession, PageQuery, ReturnRequest, ReturnRequestPage } from '../returns/types.js';
|
|
4
|
-
import { type UseResourceOptions, type UseResourceResult } from './use-resource.js';
|
|
5
|
-
type Source = {
|
|
6
|
-
auth: Pick<Auth, 'store'>;
|
|
7
|
-
returns: Returns;
|
|
8
|
-
};
|
|
9
|
-
export declare const useReturnRequests: (commerce: Source, query?: PageQuery, options?: UseResourceOptions) => UseResourceResult<ReturnRequestPage>;
|
|
10
|
-
export declare const useReturnRequest: (commerce: Source, id: string | null | undefined) => UseResourceResult<ReturnRequest>;
|
|
11
|
-
/** The order a guest's link opens (the `token` of the page's address; none yet is no question). */
|
|
12
|
-
export declare const useGuestReturnSession: (commerce: Pick<Source, "returns">, token: string | null | undefined) => UseResourceResult<GuestReturnSession>;
|
|
13
|
-
export {};
|
|
14
|
-
//# sourceMappingURL=use-returns.d.ts.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { useCustomerResource } from './use-customer-resource.js';
|
|
2
|
-
import { useResource } from './use-resource.js';
|
|
3
|
-
export const useReturnRequests = (commerce, query, options = { keepPrevious: true }) => useCustomerResource(commerce.auth, () => commerce.returns.list(query), options);
|
|
4
|
-
export const useReturnRequest = (commerce, id) => useCustomerResource(commerce.auth, () => (id ? commerce.returns.get(id) : null));
|
|
5
|
-
/** The order a guest's link opens (the `token` of the page's address; none yet is no question). */
|
|
6
|
-
export const useGuestReturnSession = (commerce, token) => useResource(token ? commerce.returns.guest.session(token) : null);
|
|
7
|
-
//# sourceMappingURL=use-returns.js.map
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { Auth } from '../auth/auth.js';
|
|
2
|
-
import { type ReviewAbility } from '../reviews/rules.js';
|
|
3
|
-
import type { Reviews } from '../reviews/reviews.js';
|
|
4
|
-
import type { MyReviewPage, MyReviewsQuery } from '../reviews/types.js';
|
|
5
|
-
import type { UseResourceOptions, UseResourceResult } from './use-resource.js';
|
|
6
|
-
type Source = {
|
|
7
|
-
auth: Pick<Auth, 'store'>;
|
|
8
|
-
reviews: Reviews;
|
|
9
|
-
};
|
|
10
|
-
export declare const useMyReviews: (commerce: Source, query?: MyReviewsQuery, options?: UseResourceOptions) => UseResourceResult<MyReviewPage>;
|
|
11
|
-
/** Who can write a review now: a guest has to log in, a customer whose address is not verified has to verify it, one whose is can. It follows the session. */
|
|
12
|
-
export declare function useReviewAbility(commerce: Pick<Source, 'auth'>): ReviewAbility;
|
|
13
|
-
export {};
|
|
14
|
-
//# sourceMappingURL=use-reviews.d.ts.map
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { useSyncExternalStore } from 'react';
|
|
2
|
-
import { reviewAbility } from '../reviews/rules.js';
|
|
3
|
-
import { useCustomerResource } from './use-customer-resource.js';
|
|
4
|
-
export const useMyReviews = (commerce, query, options = { keepPrevious: true }) => useCustomerResource(commerce.auth, () => commerce.reviews.mine(query), options);
|
|
5
|
-
/** Who can write a review now: a guest has to log in, a customer whose address is not verified has to verify it, one whose is can. It follows the session. */
|
|
6
|
-
export function useReviewAbility(commerce) {
|
|
7
|
-
const state = useSyncExternalStore(commerce.auth.store.subscribe, commerce.auth.store.getState, commerce.auth.store.getState);
|
|
8
|
-
return reviewAbility(state);
|
|
9
|
-
}
|
|
10
|
-
//# sourceMappingURL=use-reviews.js.map
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { AddressOptions } from '../shop/address.js';
|
|
2
|
-
import type { ShopStore } from '../shop/store.js';
|
|
3
|
-
export interface UseShopAddressOptions extends AddressOptions {
|
|
4
|
-
/** The query string of the address NOW, as the router gives it (`location.search`). */
|
|
5
|
-
search: string;
|
|
6
|
-
/** Goes to this query string, on the same page, adding a history entry (`navigate({ search })` of react-router). */
|
|
7
|
-
navigate: (search: string) => void;
|
|
8
|
-
/** The collection the page is for (from its route): it is not in the query. */
|
|
9
|
-
collectionId?: string;
|
|
10
|
-
}
|
|
11
|
-
export declare function useShopAddress(options: UseShopAddressOptions): ShopStore;
|
|
12
|
-
//# sourceMappingURL=use-shop-address.d.ts.map
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef } from 'react';
|
|
2
|
-
import { createAddressShopStore } from '../shop/address-store.js';
|
|
3
|
-
export function useShopAddress(options) {
|
|
4
|
-
const { search, collectionId, limit } = options;
|
|
5
|
-
// What the port calls is always the latest the site gave, without the store being made again at every render
|
|
6
|
-
const navigate = useRef(options.navigate);
|
|
7
|
-
navigate.current = options.navigate;
|
|
8
|
-
const listeners = useRef(new Set());
|
|
9
|
-
// What the address says now: what the router said last, or what was written since (until the router says it too)
|
|
10
|
-
const shown = useRef(search);
|
|
11
|
-
const routerSaid = useRef(search);
|
|
12
|
-
// The router says something else than before (the back button, a link): THAT is the address, already in this render (no frame with the old one). Only a change of what the router says counts:
|
|
13
|
-
// a render that comes between a write and the router's answer must not take the address back.
|
|
14
|
-
if (routerSaid.current !== search) {
|
|
15
|
-
routerSaid.current = search;
|
|
16
|
-
shown.current = search;
|
|
17
|
-
}
|
|
18
|
-
const port = useMemo(() => ({
|
|
19
|
-
read: () => shown.current,
|
|
20
|
-
write(next) {
|
|
21
|
-
shown.current = next;
|
|
22
|
-
for (const listener of [...listeners.current])
|
|
23
|
-
listener();
|
|
24
|
-
navigate.current(next);
|
|
25
|
-
},
|
|
26
|
-
subscribe(listener) {
|
|
27
|
-
listeners.current.add(listener);
|
|
28
|
-
return () => { listeners.current.delete(listener); };
|
|
29
|
-
},
|
|
30
|
-
}), []);
|
|
31
|
-
// Whoever listens (another component with the same store) is told that the address may have changed: if what the shop says is the same, it is the same state and nothing is drawn again
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
for (const listener of [...listeners.current])
|
|
34
|
-
listener();
|
|
35
|
-
}, [search]);
|
|
36
|
-
// A new store when the page's collection or the shop's limit change: it is the same address read with other defaults
|
|
37
|
-
return useMemo(() => createAddressShopStore(port, { collectionId, limit }), [port, collectionId, limit]);
|
|
38
|
-
}
|
|
39
|
-
//# sourceMappingURL=use-shop-address.js.map
|
package/dist/react/use-shop.d.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { CachedCatalog } from '../catalog/cached.js';
|
|
2
|
-
import type { ProductPage, ProductSearch, StoreDataResponse } from '../catalog/types.js';
|
|
3
|
-
import { type SortOption } from '../shop/sort.js';
|
|
4
|
-
import type { ShopState } from '../shop/state.js';
|
|
5
|
-
import { type ShopActions, type ShopStore } from '../shop/store.js';
|
|
6
|
-
import type { SearchContext } from '../shop/to-search.js';
|
|
7
|
-
import { type UseResourceResult } from './use-resource.js';
|
|
8
|
-
export interface UseShopOptions {
|
|
9
|
-
catalog: CachedCatalog;
|
|
10
|
-
/** What the question needs besides what the customer chose: the currency, the language, the cadence, what the products are loaded with. */
|
|
11
|
-
context: SearchContext;
|
|
12
|
-
/** The state the shop starts in (only when the hook keeps the state itself: it is not read again). */
|
|
13
|
-
initial?: ShopState;
|
|
14
|
-
/** Where the state lives, when it is not in memory in the hook (an address bar, a store the page owns). */
|
|
15
|
-
store?: ShopStore;
|
|
16
|
-
/** While a new question is awaited, keep showing the answer to the one before (marked `isStale`), so a listing does not empty at every click. On by default. */
|
|
17
|
-
keepPrevious?: boolean;
|
|
18
|
-
}
|
|
19
|
-
export interface UseShopResult {
|
|
20
|
-
/** What the customer chose. */
|
|
21
|
-
state: ShopState;
|
|
22
|
-
/** What changes it. */
|
|
23
|
-
actions: ShopActions;
|
|
24
|
-
/** The store's own data (its tags, collections, filters, currencies): what the controls are drawn from. */
|
|
25
|
-
storeData: UseResourceResult<StoreDataResponse>;
|
|
26
|
-
/** The orders that can be offered now (with the store's data in hand, and a text or not); none until it is. */
|
|
27
|
-
sortOptions: SortOption[];
|
|
28
|
-
/** The question the catalog is being asked now, or null while the store's data is awaited. */
|
|
29
|
-
question: ProductSearch | null;
|
|
30
|
-
/** The products. While the store's data is awaited this is `pending` (or its error, with a `retry` that asks for the store again). */
|
|
31
|
-
results: UseResourceResult<ProductPage>;
|
|
32
|
-
}
|
|
33
|
-
export declare function useShop(options: UseShopOptions): UseShopResult;
|
|
34
|
-
//# sourceMappingURL=use-shop.d.ts.map
|
package/dist/react/use-shop.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { useCallback, useMemo, useRef, useSyncExternalStore } from 'react';
|
|
2
|
-
import { canonicalJson } from '../canonical.js';
|
|
3
|
-
import { shopQuestion } from '../shop/question.js';
|
|
4
|
-
import { sortOptionsOf } from '../shop/sort.js';
|
|
5
|
-
import { createShopStore, shopActions } from '../shop/store.js';
|
|
6
|
-
import { useResource } from './use-resource.js';
|
|
7
|
-
export function useShop(options) {
|
|
8
|
-
const { catalog, context, keepPrevious = true } = options;
|
|
9
|
-
// The state: in memory, in the hook, unless a store was given
|
|
10
|
-
const own = useRef(null);
|
|
11
|
-
if (!options.store && !own.current)
|
|
12
|
-
own.current = createShopStore(options.initial);
|
|
13
|
-
const store = options.store ?? own.current;
|
|
14
|
-
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
15
|
-
const actions = useMemo(() => shopActions(store), [store]);
|
|
16
|
-
const storeData = useResource(catalog.resource.storeData.get(context.langCode === undefined ? undefined : { langCode: context.langCode }));
|
|
17
|
-
const data = storeData.data?.data;
|
|
18
|
-
// A new `context` object at every render is the same context: only what is in it makes a new question
|
|
19
|
-
const contextKey = canonicalJson(context);
|
|
20
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
21
|
-
const asked = useMemo(() => shopQuestion(catalog, data, state, context), [catalog, data, state, contextKey]);
|
|
22
|
-
const products = useResource(asked?.resource ?? null, { keepPrevious });
|
|
23
|
-
const sortOptions = useMemo(() => (data ? sortOptionsOf(data) : []), [data]);
|
|
24
|
-
// With no question yet the products are waiting for the store's data (or failed with it): say that, so the screen has one thing to look at
|
|
25
|
-
const retry = useCallback(() => storeData.retry(), [storeData]);
|
|
26
|
-
const results = asked
|
|
27
|
-
? products
|
|
28
|
-
: { status: storeData.status === 'error' ? 'error' : 'pending', data: undefined, error: storeData.error, isFetching: storeData.status !== 'error', isStale: false, retry };
|
|
29
|
-
return { state, actions, storeData, sortOptions, question: asked?.question ?? null, results };
|
|
30
|
-
}
|
|
31
|
-
//# sourceMappingURL=use-shop.js.map
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { type Wishlist, type WishlistState } from '../wishlist/wishlist.js';
|
|
2
|
-
export interface UseWishlistResult {
|
|
3
|
-
state: WishlistState;
|
|
4
|
-
count: number;
|
|
5
|
-
/** Whether a variant is in the wishlist. */
|
|
6
|
-
has(variantId: string): boolean;
|
|
7
|
-
/** The switch: adds a variant that is not in the wishlist, removes one that is. Throws what the store refuses; `LOGIN_REQUIRED` for a guest. */
|
|
8
|
-
toggle: Wishlist['toggle'];
|
|
9
|
-
refresh: Wishlist['refresh'];
|
|
10
|
-
}
|
|
11
|
-
/** `commerce` is what `createCommerce` made (or `@webround/commerce/site`'s). Any number of components can use it (a heart on each product): the wishlist starts once. A server render says `unknown` and nothing in it. */
|
|
12
|
-
export declare function useWishlist(commerce: {
|
|
13
|
-
wishlist: Wishlist;
|
|
14
|
-
}): UseWishlistResult;
|
|
15
|
-
//# sourceMappingURL=use-wishlist.d.ts.map
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react';
|
|
2
|
-
import { isInWishlist } from '../wishlist/wishlist.js';
|
|
3
|
-
/** `commerce` is what `createCommerce` made (or `@webround/commerce/site`'s). Any number of components can use it (a heart on each product): the wishlist starts once. A server render says `unknown` and nothing in it. */
|
|
4
|
-
export function useWishlist(commerce) {
|
|
5
|
-
const { wishlist } = commerce;
|
|
6
|
-
const state = useSyncExternalStore(wishlist.subscribe, wishlist.getState, wishlist.getState);
|
|
7
|
-
useEffect(() => { void wishlist.start(); }, [wishlist]);
|
|
8
|
-
const has = useCallback((variantId) => isInWishlist(state, variantId), [state.variantIds]);
|
|
9
|
-
const actions = useMemo(() => ({ toggle: (variantId) => wishlist.toggle(variantId), refresh: () => wishlist.refresh() }), [wishlist]);
|
|
10
|
-
return { state, count: state.count, has, toggle: actions.toggle, refresh: actions.refresh };
|
|
11
|
-
}
|
|
12
|
-
//# sourceMappingURL=use-wishlist.js.map
|
package/dist/returns/rules.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { type AddressIssue } from '../addresses/rules.js';
|
|
2
|
-
import { type CreateGuestReturnInput, type CreateReturnInput } from './types.js';
|
|
3
|
-
/** What is wrong with a return request, in the shape the store lists it (`path`, `message`). The order or the token is the caller's to check (it is an id, not something to write). */
|
|
4
|
-
export declare function returnIssues(input: Pick<CreateReturnInput, 'items' | 'reason' | 'shippingAddress' | 'billingAddress'>): AddressIssue[];
|
|
5
|
-
/** The request as it is, or the refusal the store would have given (400, what it did not like in `issues`). */
|
|
6
|
-
export declare function checkedReturn<T extends CreateReturnInput | CreateGuestReturnInput>(input: T): T;
|
|
7
|
-
//# sourceMappingURL=rules.d.ts.map
|
package/dist/returns/rules.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { CommerceError } from '../errors.js';
|
|
2
|
-
import { addressIssues } from '../addresses/rules.js';
|
|
3
|
-
import { RETURN_LIMITS } from './types.js';
|
|
4
|
-
// What a return request has to meet (the store's own rule), said before asking: a form can show what is wrong without a round trip. Pure.
|
|
5
|
-
/** What is wrong with a return request, in the shape the store lists it (`path`, `message`). The order or the token is the caller's to check (it is an id, not something to write). */
|
|
6
|
-
export function returnIssues(input) {
|
|
7
|
-
const issues = [];
|
|
8
|
-
if (!Array.isArray(input.items) || input.items.length < 1)
|
|
9
|
-
issues.push({ path: ['items'], message: 'Choose at least one item to return' });
|
|
10
|
-
else if (input.items.length > RETURN_LIMITS.items)
|
|
11
|
-
issues.push({ path: ['items'], message: `At most ${RETURN_LIMITS.items} items` });
|
|
12
|
-
else {
|
|
13
|
-
const seen = new Set();
|
|
14
|
-
input.items.forEach((item, i) => {
|
|
15
|
-
if (!item || typeof item.orderItemId !== 'string' || item.orderItemId === '')
|
|
16
|
-
issues.push({ path: ['items', String(i), 'orderItemId'], message: 'Required' });
|
|
17
|
-
else if (seen.has(item.orderItemId))
|
|
18
|
-
issues.push({ path: ['items', String(i), 'orderItemId'], message: 'The same item twice' });
|
|
19
|
-
else
|
|
20
|
-
seen.add(item.orderItemId);
|
|
21
|
-
if (!item || !Number.isInteger(item.quantity) || item.quantity < 1 || item.quantity > RETURN_LIMITS.quantity)
|
|
22
|
-
issues.push({ path: ['items', String(i), 'quantity'], message: `A whole number from 1 to ${RETURN_LIMITS.quantity}` });
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
if (input.reason !== undefined && input.reason.length > RETURN_LIMITS.reason)
|
|
26
|
-
issues.push({ path: ['reason'], message: `At most ${RETURN_LIMITS.reason} characters` });
|
|
27
|
-
for (const which of ['shippingAddress', 'billingAddress']) {
|
|
28
|
-
const address = input[which];
|
|
29
|
-
if (!address) {
|
|
30
|
-
issues.push({ path: [which], message: 'Required' });
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
for (const issue of addressIssues(address))
|
|
34
|
-
issues.push({ path: [which, ...issue.path], message: issue.message });
|
|
35
|
-
}
|
|
36
|
-
return issues;
|
|
37
|
-
}
|
|
38
|
-
/** The request as it is, or the refusal the store would have given (400, what it did not like in `issues`). */
|
|
39
|
-
export function checkedReturn(input) {
|
|
40
|
-
const issues = returnIssues(input);
|
|
41
|
-
if (issues.length > 0)
|
|
42
|
-
throw new CommerceError({ kind: 'http', status: 400, code: null, message: issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; '), issues });
|
|
43
|
-
return input;
|
|
44
|
-
}
|
|
45
|
-
//# sourceMappingURL=rules.js.map
|
package/dist/site.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { type WrSiteEnv } from '@webround/core/site-env';
|
|
2
|
-
import { type Commerce, type CommerceConfig } from './commerce.js';
|
|
3
|
-
/** The store `env` describes. Throws, naming what is missing, when it does not say enough. */
|
|
4
|
-
export declare function commerceFromEnv(env: WrSiteEnv, options?: Omit<CommerceConfig, 'storeId' | 'commerceApiUrl' | 'catalogApiUrl' | 'checkoutApiUrl'>): Commerce;
|
|
5
|
-
/** This site's store: its session, its catalog, its cache. Made the first time one of them is read. */
|
|
6
|
-
export declare const commerce: Commerce;
|
|
7
|
-
//# sourceMappingURL=site.d.ts.map
|
package/dist/site.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { wrSiteEnv, WR_SITE_ENV_VARS } from '@webround/core/site-env';
|
|
2
|
-
import { createCommerce } from './commerce.js';
|
|
3
|
-
// `@webround/commerce/site`: the store of THIS site, made from what the site was built with (`VITE_WR_*`, set by the Worker that runs the draft; `wrSiteEnv` of @webround/core is the one place that reads them).
|
|
4
|
-
//
|
|
5
|
-
// import { commerce } from '@webround/commerce/site';
|
|
6
|
-
// const { actions } = useAuth(commerce.auth);
|
|
7
|
-
//
|
|
8
|
-
// Nothing is made until it is used: a page that never touches the store costs nothing, and a site built without the variables fails WHERE it uses the store, saying which one is missing, not when it loads.
|
|
9
|
-
/** The store `env` describes. Throws, naming what is missing, when it does not say enough. */
|
|
10
|
-
export function commerceFromEnv(env, options = {}) {
|
|
11
|
-
const { draftId, commerceApiUrl, catalogApiUrl, checkoutApiUrl } = env;
|
|
12
|
-
if (draftId === undefined || commerceApiUrl === undefined || catalogApiUrl === undefined) {
|
|
13
|
-
const missing = [
|
|
14
|
-
draftId === undefined ? WR_SITE_ENV_VARS.draftId : null,
|
|
15
|
-
commerceApiUrl === undefined ? WR_SITE_ENV_VARS.commerceApiUrl : null,
|
|
16
|
-
catalogApiUrl === undefined ? WR_SITE_ENV_VARS.catalogApiUrl : null,
|
|
17
|
-
].filter((name) => name !== null);
|
|
18
|
-
throw new Error(`@webround/commerce: this site was built without ${missing.join(', ')}, so it does not know which store to talk to.`);
|
|
19
|
-
}
|
|
20
|
-
// The checkout's address is not needed to run the store: a site without it fails where it uses the checkout, saying so
|
|
21
|
-
return createCommerce({ ...options, storeId: draftId, commerceApiUrl, catalogApiUrl, ...(checkoutApiUrl !== undefined ? { checkoutApiUrl } : {}) });
|
|
22
|
-
}
|
|
23
|
-
let made;
|
|
24
|
-
// This site prices and describes its cart with the checkout's preview (`commerce.checkout`): `/cart/total` is never asked
|
|
25
|
-
const store = () => (made ?? (made = commerceFromEnv(wrSiteEnv(), { cartPricing: false })));
|
|
26
|
-
/** This site's store: its session, its catalog, its cache. Made the first time one of them is read. */
|
|
27
|
-
export const commerce = {
|
|
28
|
-
get storeId() { return store().storeId; },
|
|
29
|
-
get auth() { return store().auth; },
|
|
30
|
-
get catalog() { return store().catalog; },
|
|
31
|
-
get cache() { return store().cache; },
|
|
32
|
-
get cart() { return store().cart; },
|
|
33
|
-
get checkout() { return store().checkout; },
|
|
34
|
-
get account() { return store().account; },
|
|
35
|
-
get addresses() { return store().addresses; },
|
|
36
|
-
get orders() { return store().orders; },
|
|
37
|
-
get returns() { return store().returns; },
|
|
38
|
-
get wishlist() { return store().wishlist; },
|
|
39
|
-
get reviews() { return store().reviews; },
|
|
40
|
-
};
|
|
41
|
-
//# sourceMappingURL=site.js.map
|