@webround/commerce 0.4.0 → 0.4.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 +19 -12
- package/dist/answer.d.ts +11 -0
- package/dist/answer.js +13 -0
- package/dist/cart/store.d.ts +3 -3
- package/dist/cart/store.js +8 -8
- package/dist/catalog/cached.d.ts +4 -4
- package/dist/catalog/products.d.ts +8 -8
- package/dist/catalog/products.js +9 -5
- package/dist/catalog/store-data.d.ts +4 -4
- package/dist/catalog/store-data.js +1 -1
- package/dist/catalog/types.d.ts +8 -11
- package/dist/customer-resources.d.ts +4 -0
- package/dist/customer-resources.js +9 -0
- package/dist/index.d.ts +1 -1
- package/dist/orders/api.d.ts +3 -3
- package/dist/orders/api.js +7 -5
- package/dist/orders/index.d.ts +2 -2
- package/dist/orders/orders.d.ts +18 -5
- package/dist/orders/orders.js +11 -1
- package/dist/orders/types.d.ts +19 -2
- package/dist/returns/api.d.ts +7 -7
- package/dist/returns/api.js +11 -9
- package/dist/returns/index.d.ts +2 -2
- package/dist/returns/returns.d.ts +23 -12
- package/dist/returns/returns.js +9 -2
- package/dist/returns/types.d.ts +14 -2
- package/dist/reviews/api.d.ts +2 -2
- package/dist/reviews/api.js +3 -2
- package/dist/reviews/index.d.ts +2 -2
- package/dist/reviews/reviews.d.ts +12 -6
- package/dist/reviews/reviews.js +9 -4
- package/dist/reviews/types.d.ts +11 -3
- package/dist/wishlist/wishlist.d.ts +2 -1
- package/dist/wishlist/wishlist.js +5 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,15 +29,20 @@ store: `smoke/catalog.smoke.ts`, "how a search combines".
|
|
|
29
29
|
|
|
30
30
|
`ShopStore` (`createShopStore`, `shopActions`) is where the state lives: an interface of three methods (`getState`, `subscribe`, `set`), in memory by default; the same state object until
|
|
31
31
|
something really changes (a change that leaves it as it was tells nobody); one that keeps it in the address bar is another implementation. `shopQuestion(catalog, storeData, state,
|
|
32
|
-
context)` is the state made into a `Resource` of the cached catalog (null until the store's data is there). `/react` binds them: `useShop({
|
|
32
|
+
context)` is the state made into a `Resource` of the cached catalog (null until the store's data is there). `/react` binds them: `useShop(commerce, { context, initial?, store?,
|
|
33
33
|
keepPrevious? })` gives, each by its name: `state` (what the customer chose), `actions` (what changes it), `question` (the exact `ProductSearch` the catalog is being asked: `state` made a request
|
|
34
|
-
with `context`), `products` and `pagination` (the answer to `question
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
with `context`), `products` and `pagination` (the answer to `question`), `status`/`error`/`isFetching`/`isStale`/`retry` (how asking the products stands; `keepPrevious` on by default, so a listing does
|
|
35
|
+
not empty at every click), and the store's data (`store`, `collections`, `tags`, `attributes`, `filters`) and `sortOptions`. Every key that comes from the backend is `undefined` until it is there. The
|
|
36
|
+
store's data alone is `useStoreData(commerce, { langCode? })`: the same names plus the asking's, and it is the same question `useShop` asks (one request).
|
|
37
|
+
|
|
38
|
+
**How data comes out, everywhere (the rule of the shape).** Every read gives an object whose keys NAME what is in it, the same without React and in the hook: `await commerce.catalog.products.search(q)` is
|
|
39
|
+
`{ products, pagination }`, `await commerce.orders.list(q)` is `{ orders, pagination }`, `await commerce.orders.get(id)` is `{ order }`, `useOrders(commerce, q)` is `{ orders, pagination, status, error,
|
|
40
|
+
isFetching, isStale, retry }`: the hook is the call's keys plus how asking stands (the same five fields in every hook that reads from the backend: they win over an answer's own names). Lists are plural
|
|
41
|
+
(`products`, `tags`, `returnRequests`), a single thing singular (`product`, `order`, `returnRequest`, `returnSession`). A customer's reads (`orders`, `returns`, `reviews`) are refused with
|
|
42
|
+
`LOGIN_REQUIRED` when nobody is logged in; what a framework binds to (`Resource`) is at `.resource` (`commerce.orders.resource.list(q)`: `null` when nobody is logged in), as the catalog's is at
|
|
43
|
+
`commerce.catalog.resource`. What changes is a store, and a store gives the same names: `commerce.cart.getState()` and `useCart(commerce)` are `{ lines, count, cart, owner, … }`, `useWishlist(commerce)` is
|
|
44
|
+
`{ wishlist, count, has, … }`, `useAddresses(commerce)` is `{ addresses, … }`, `useAuth(commerce)` is `{ status, customer, challenge, … }`; what changes them is always `actions`. The first argument of every
|
|
45
|
+
hook is `commerce`. The text of the search box is the VIEW's (what is being typed); `actions.setSearch` is what is searched (on Enter, or after a pause the component decides): every state
|
|
41
46
|
change goes back to page 1 and asks the catalog, so a keystroke must not be one.
|
|
42
47
|
|
|
43
48
|
**The address** (`src/shop/address.ts`, `address-store.ts`; `useShopAddress` in `/react`): the state written in the query string and read back, so a search is a link, "back" goes back through
|
|
@@ -61,7 +66,7 @@ Two-factor is covered when the store's key service is there (setup, confirm →
|
|
|
61
66
|
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
|
|
62
67
|
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`
|
|
63
68
|
(`getState`, `subscribe` like the shop's, `restore`, `signup`, `login`, `verifyTwoFactor`, `logout`, `refresh`, `accessToken`, `authHeaders`): `unknown` → `signedOut` | `signedIn`; a login may be a `challenge`
|
|
64
|
-
(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
|
|
69
|
+
(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 `cart`/`isStale` (what it COSTS: the cart as the store prices it: 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 `lines`, not from `cart`. 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`), through the same cache the catalog is in (`await commerce.orders.list(q)` gives `{ orders, pagination }`; the cache's `Resource` is `commerce.orders.resource.list(q)`, which `useOrders` shows; 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 `{ review }` (`remove` gives `{ id }`); the customer's own list is `reviews.list(q)`, `{ reviews, pagination }`. 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
|
|
65
70
|
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
|
|
66
71
|
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
|
|
67
72
|
(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
|
|
@@ -85,7 +90,7 @@ answer to an older question is thrown away. `isStale` says the answer on show is
|
|
|
85
90
|
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:
|
|
86
91
|
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
|
|
87
92
|
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
|
|
88
|
-
`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, `
|
|
93
|
+
`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, `cart` 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
|
|
89
94
|
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`
|
|
90
95
|
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,
|
|
91
96
|
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.
|
|
@@ -99,8 +104,10 @@ running development API: a hook (it needs an `https` address and the key service
|
|
|
99
104
|
|
|
100
105
|
## Rules that do not bend
|
|
101
106
|
|
|
102
|
-
1. **The backend's
|
|
103
|
-
|
|
107
|
+
1. **The backend's contract is never altered, and what it answers is only NAMED.** What is asked and what comes back over HTTP is the backend's (`{ data }` envelope included), read as it is. Where an answer comes in
|
|
108
|
+
(`src/answer.ts`: `opened`, `entity`) the wrapper is opened ONCE and its `data` is given under its own name (`products`, `orders`, `order`…), so what a call gives, what the cache keeps and what a hook shows are
|
|
109
|
+
the same keys (see "How data comes out"). Nothing else is touched: no field is renamed, "cleaned" or fixed. If a field looks wrong, say so; do not fix it here. `wr-commerce-catalog` (the catalog API) is
|
|
110
|
+
never modified from this repo's work. (Until 0.4.0 the methods gave the envelope as the backend sent it; Luca decided on 1/11 that one shape everywhere matters more than the envelope.)
|
|
104
111
|
2. **Types come from what the catalog really sends, not from its OpenAPI** (it is wrong in places). `smoke/` checks the types against a real store; when a real answer
|
|
105
112
|
does not match, the TYPE changes (`Format` and nullable `label`/`icon` were found that way).
|
|
106
113
|
3. **No defaults for what the caller chooses.** `include*` options are sent only when given. This is a data layer, not a set of opinions.
|
package/dist/answer.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** A wrapped answer (`{ data, pagination… }`) with `data` under its own name: `opened('orders', { data: [...], pagination })` is `{ orders: [...], pagination }`. What else was in the wrapper stays. */
|
|
2
|
+
export declare function opened<K extends string, V, R extends object>(key: K, wire: {
|
|
3
|
+
data: V;
|
|
4
|
+
} & R): {
|
|
5
|
+
[P in K]: V;
|
|
6
|
+
} & R;
|
|
7
|
+
/** An answer that came bare (one order) under its own name: `entity('order', order)` is `{ order }`. */
|
|
8
|
+
export declare function entity<K extends string, V>(key: K, value: V): {
|
|
9
|
+
[P in K]: V;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=answer.d.ts.map
|
package/dist/answer.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// What the package gives back is always an object whose keys say what is in it (`products`, `orders`, `product`, `order`…), the same in a call, in the cache and in what a framework binds to it: never a `data` that
|
|
2
|
+
// has to be opened. The backends do not say it that way (a list is `{ data: [...], pagination }`, a record `{ data: {...} }` or bare), so each call opens what its backend sent ONCE, here, where the answer comes in.
|
|
3
|
+
// The HTTP contract of the backends is not touched: it is read as it is and named on the way in.
|
|
4
|
+
/** A wrapped answer (`{ data, pagination… }`) with `data` under its own name: `opened('orders', { data: [...], pagination })` is `{ orders: [...], pagination }`. What else was in the wrapper stays. */
|
|
5
|
+
export function opened(key, wire) {
|
|
6
|
+
const { data, ...rest } = wire;
|
|
7
|
+
return { [key]: data, ...rest };
|
|
8
|
+
}
|
|
9
|
+
/** An answer that came bare (one order) under its own name: `entity('order', order)` is `{ order }`. */
|
|
10
|
+
export function entity(key, value) {
|
|
11
|
+
return { [key]: value };
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=answer.js.map
|
package/dist/cart/store.d.ts
CHANGED
|
@@ -11,8 +11,8 @@ export interface CartState {
|
|
|
11
11
|
lines: CartLineInput[];
|
|
12
12
|
/** How many items: the sum of the quantities (what a badge says; known before the store has priced anything). */
|
|
13
13
|
count: number;
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/** The cart as the store prices it (the lines with their names, pictures and totals), or null until it said. While `isStale`, it is the answer to the lines before the last change. */
|
|
15
|
+
cart: Cart | null;
|
|
16
16
|
isStale: boolean;
|
|
17
17
|
/** An action or a call is under way. */
|
|
18
18
|
busy: boolean;
|
|
@@ -50,7 +50,7 @@ export interface CartStoreOptions {
|
|
|
50
50
|
langCode?: () => string | undefined;
|
|
51
51
|
/**
|
|
52
52
|
* Whether a guest's cart is priced with `/cart/total` after every change (default: yes). A site that prices and describes its cart with the checkout's preview (`commerce.checkout`: its lines carry the names, the
|
|
53
|
-
* pictures, the stock and the totals) says no: the guest's lines are kept and counted, `
|
|
53
|
+
* pictures, the stock and the totals) says no: the guest's lines are kept and counted, `cart` stays null and `/cart/total` is never called. A customer's cart is still read with `/cart/get`: it is where its lines are.
|
|
54
54
|
*/
|
|
55
55
|
pricing?: boolean;
|
|
56
56
|
}
|
package/dist/cart/store.js
CHANGED
|
@@ -9,7 +9,7 @@ const isDefinite = (error) => error.kind === 'http' && (error.status === 400 ||
|
|
|
9
9
|
export function createCartStore(options) {
|
|
10
10
|
const { api, auth, storage } = options;
|
|
11
11
|
const lang = () => options.langCode?.();
|
|
12
|
-
const holder = createStateHolder({ owner: 'unknown', lines: [], count: 0,
|
|
12
|
+
const holder = createStateHolder({ owner: 'unknown', lines: [], count: 0, cart: null, isStale: false, busy: false, error: null, lastMerge: null });
|
|
13
13
|
const set = holder.set;
|
|
14
14
|
// Counts the times the cart changed hands: an answer that comes after that is about a cart that is not this one any more
|
|
15
15
|
let epoch = 0;
|
|
@@ -27,7 +27,7 @@ export function createCartStore(options) {
|
|
|
27
27
|
const lines = holder.get().lines;
|
|
28
28
|
if (lines.length === 0) {
|
|
29
29
|
storage.forgetTrackingId();
|
|
30
|
-
set({
|
|
30
|
+
set({ cart: EMPTY_CART, isStale: false, error: null });
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
// A site that prices with the checkout's preview does not ask `/cart/total`: the lines are all the guest's cart is
|
|
@@ -40,7 +40,7 @@ export function createCartStore(options) {
|
|
|
40
40
|
if (epoch !== mine)
|
|
41
41
|
return;
|
|
42
42
|
// Lines that changed meanwhile have a pricing of their own coming: this answer is one change behind
|
|
43
|
-
set({ priced, isStale: holder.get().lines !== lines, error: null });
|
|
43
|
+
set({ cart: priced, isStale: holder.get().lines !== lines, error: null });
|
|
44
44
|
}
|
|
45
45
|
catch (error) {
|
|
46
46
|
if (epoch === mine)
|
|
@@ -96,7 +96,7 @@ export function createCartStore(options) {
|
|
|
96
96
|
if (priced.items.length === 0)
|
|
97
97
|
storage.forgetTrackingId();
|
|
98
98
|
const lines = linesOf(priced);
|
|
99
|
-
set(withLines(lines, { priced, isStale: false, error: mergeError }));
|
|
99
|
+
set(withLines(lines, { cart: priced, isStale: false, error: mergeError }));
|
|
100
100
|
}
|
|
101
101
|
catch (error) {
|
|
102
102
|
if (epoch === mine)
|
|
@@ -105,7 +105,7 @@ export function createCartStore(options) {
|
|
|
105
105
|
}
|
|
106
106
|
/** What the store holds of a variant in the customer's cart: the line's own id, asking again for the cart first when it is not known (a line just added). */
|
|
107
107
|
async function lineIdOf(variantId) {
|
|
108
|
-
const find = () => holder.get().
|
|
108
|
+
const find = () => holder.get().cart?.items.find((i) => i.variantId === variantId)?.id;
|
|
109
109
|
return find() ?? (await loadCustomer(), find());
|
|
110
110
|
}
|
|
111
111
|
/** A customer's action: what the customer sees changes at once; the call goes in its turn; when the last one is done the cart is read again, so what is shown is what the store holds, a refusal included. */
|
|
@@ -128,7 +128,7 @@ export function createCartStore(options) {
|
|
|
128
128
|
epoch++;
|
|
129
129
|
pricing = null;
|
|
130
130
|
const lines = storage.lines();
|
|
131
|
-
set(withLines(lines, { owner: 'guest',
|
|
131
|
+
set(withLines(lines, { owner: 'guest', cart: null, isStale: lines.length > 0, error: null, lastMerge: null }));
|
|
132
132
|
void schedulePricing();
|
|
133
133
|
}
|
|
134
134
|
function toCustomer() {
|
|
@@ -137,7 +137,7 @@ export function createCartStore(options) {
|
|
|
137
137
|
mergeRefused = false;
|
|
138
138
|
mergeError = null;
|
|
139
139
|
// The guest's lines stay in sight until the merge is done: the badge does not blink to 0
|
|
140
|
-
set({ owner: 'customer',
|
|
140
|
+
set({ owner: 'customer', cart: null, isStale: true, error: null });
|
|
141
141
|
void queue.run(loadCustomer);
|
|
142
142
|
}
|
|
143
143
|
const following = followAuth(auth, { current: () => holder.get().owner, toGuest, toCustomer });
|
|
@@ -210,7 +210,7 @@ export function createCartStore(options) {
|
|
|
210
210
|
pricing = null;
|
|
211
211
|
storage.clearLines();
|
|
212
212
|
storage.forgetTrackingId();
|
|
213
|
-
set(withLines([], {
|
|
213
|
+
set(withLines([], { cart: EMPTY_CART, isStale: false, error: null }));
|
|
214
214
|
},
|
|
215
215
|
dispose: following.stop,
|
|
216
216
|
};
|
package/dist/catalog/cached.d.ts
CHANGED
|
@@ -2,22 +2,22 @@ import { type Cache } from '../cache.js';
|
|
|
2
2
|
import { type Resource } from '../resource.js';
|
|
3
3
|
import type { HttpClient } from '../http.js';
|
|
4
4
|
import { type Catalog } from './catalog.js';
|
|
5
|
-
import type { ProductByIdInput, ProductBySlugInput,
|
|
5
|
+
import type { ProductAnswer, ProductByIdInput, ProductBySlugInput, ProductPage, ProductRelatedInput, ProductReviewsInput, ProductSearch, ReviewPage, StoreData } from './types.js';
|
|
6
6
|
/** What can be forgotten from the cache: one of the catalog's calls. */
|
|
7
7
|
export type CatalogCall = 'products.search' | 'products.getBySlug' | 'products.getById' | 'products.getReviews' | 'products.getRelated' | 'storeData.get';
|
|
8
8
|
export type { Resource };
|
|
9
9
|
export interface CatalogResources {
|
|
10
10
|
products: {
|
|
11
11
|
search(input: ProductSearch): Resource<ProductPage>;
|
|
12
|
-
getBySlug(variantSlug: string, input: ProductBySlugInput): Resource<
|
|
13
|
-
getById(id: string, input: ProductByIdInput): Resource<
|
|
12
|
+
getBySlug(variantSlug: string, input: ProductBySlugInput): Resource<ProductAnswer>;
|
|
13
|
+
getById(id: string, input: ProductByIdInput): Resource<ProductAnswer>;
|
|
14
14
|
getReviews(id: string, input?: ProductReviewsInput): Resource<ReviewPage>;
|
|
15
15
|
getRelated(id: string, input: ProductRelatedInput): Resource<ProductPage>;
|
|
16
16
|
};
|
|
17
17
|
storeData: {
|
|
18
18
|
get(input?: {
|
|
19
19
|
langCode?: string;
|
|
20
|
-
}): Resource<
|
|
20
|
+
}): Resource<StoreData>;
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
import type { HttpClient } from '../http.js';
|
|
2
|
-
import type { CallOptions, ProductByIdInput, ProductBySlugInput,
|
|
2
|
+
import type { CallOptions, ProductAnswer, ProductByIdInput, ProductBySlugInput, ProductPage, ProductRelatedInput, ProductReviewsInput, ProductSearch, ReviewPage } from './types.js';
|
|
3
3
|
export interface ProductsApi {
|
|
4
4
|
/**
|
|
5
|
-
* `POST /public/products`: the store's products, filtered, sorted and paginated as asked
|
|
5
|
+
* `POST /public/products`: the store's products, filtered, sorted and paginated as asked: `{ products, pagination }`. Nothing is added to what is asked (no page, no limit, no sorting): the server's own
|
|
6
6
|
* defaults apply to what is left out. The body goes out in its canonical form (keys and lists in order), so the same search is always the same request.
|
|
7
7
|
*/
|
|
8
8
|
search(input: ProductSearch, options?: CallOptions): Promise<ProductPage>;
|
|
9
9
|
/**
|
|
10
|
-
* `POST /public/products/by-slug/{variantSlug}`: the product that has a variant with this slug, with all it has
|
|
10
|
+
* `POST /public/products/by-slug/{variantSlug}`: the product that has a variant with this slug, with all it has: `{ product }`. A product has no slug: it is the VARIANT's. What is loaded with it is for
|
|
11
11
|
* `input`'s `include*` to say.
|
|
12
12
|
*/
|
|
13
|
-
getBySlug(variantSlug: string, input: ProductBySlugInput, options?: CallOptions): Promise<
|
|
14
|
-
/** `POST /public/products/{id}`: the product with this id, with all it has
|
|
15
|
-
getById(id: string, input: ProductByIdInput, options?: CallOptions): Promise<
|
|
13
|
+
getBySlug(variantSlug: string, input: ProductBySlugInput, options?: CallOptions): Promise<ProductAnswer>;
|
|
14
|
+
/** `POST /public/products/{id}`: the product with this id, with all it has: `{ product }`. */
|
|
15
|
+
getById(id: string, input: ProductByIdInput, options?: CallOptions): Promise<ProductAnswer>;
|
|
16
16
|
/**
|
|
17
|
-
* `POST /public/products/{id}/reviews`: the product's reviews, a page of them, with how many there are for each grade
|
|
17
|
+
* `POST /public/products/{id}/reviews`: the product's reviews, a page of them, with how many there are for each grade: `{ reviews, pagination, breakdown }`. The id is the product's (a UUID: the catalog refuses anything else with a 400).
|
|
18
18
|
* A product with no reviews is an empty page, not an error.
|
|
19
19
|
*/
|
|
20
20
|
getReviews(id: string, input?: ProductReviewsInput, options?: CallOptions): Promise<ReviewPage>;
|
|
21
21
|
/**
|
|
22
|
-
* `POST /public/products/{id}/related`: the products related to this one (up to ten, the most relevant first), in the same shape as a search's
|
|
22
|
+
* `POST /public/products/{id}/related`: the products related to this one (up to ten, the most relevant first), in the same shape as a search's: `{ products, pagination }`. The id is the product's (a UUID).
|
|
23
23
|
* A product with nothing related is an empty page, not an error.
|
|
24
24
|
*/
|
|
25
25
|
getRelated(id: string, input: ProductRelatedInput, options?: CallOptions): Promise<ProductPage>;
|
package/dist/catalog/products.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import { entity, opened } from '../answer.js';
|
|
1
2
|
import { canonicalize } from '../canonical.js';
|
|
2
3
|
import { named } from './named.js';
|
|
3
4
|
export function createProductsApi(http) {
|
|
5
|
+
const products = async (request) => opened('products', await request);
|
|
6
|
+
const product = async (request) => entity('product', (await request).data);
|
|
7
|
+
const reviews = async (request) => opened('reviews', await request);
|
|
4
8
|
return {
|
|
5
|
-
search: (input, options) => http.request({ method: 'POST', path: '/public/products', body: canonicalize(input), signal: options?.signal }),
|
|
6
|
-
getBySlug: (variantSlug, input, options) => http.request({ method: 'POST', path: `/public/products/by-slug/${named('a variant slug', variantSlug)}`, body: canonicalize(input), signal: options?.signal }),
|
|
7
|
-
getById: (id, input, options) => http.request({ method: 'POST', path: `/public/products/${named('an id', id)}`, body: canonicalize(input), signal: options?.signal }),
|
|
8
|
-
getRelated: (id, input, options) => http.request({ method: 'POST', path: `/public/products/${named('an id', id)}/related`, body: canonicalize(input), signal: options?.signal }),
|
|
9
|
-
getReviews: (id, input, options) => http.request({ method: 'POST', path: `/public/products/${named('an id', id)}/reviews`, body: canonicalize(input ?? {}), signal: options?.signal }),
|
|
9
|
+
search: (input, options) => products(http.request({ method: 'POST', path: '/public/products', body: canonicalize(input), signal: options?.signal })),
|
|
10
|
+
getBySlug: (variantSlug, input, options) => product(http.request({ method: 'POST', path: `/public/products/by-slug/${named('a variant slug', variantSlug)}`, body: canonicalize(input), signal: options?.signal })),
|
|
11
|
+
getById: (id, input, options) => product(http.request({ method: 'POST', path: `/public/products/${named('an id', id)}`, body: canonicalize(input), signal: options?.signal })),
|
|
12
|
+
getRelated: (id, input, options) => products(http.request({ method: 'POST', path: `/public/products/${named('an id', id)}/related`, body: canonicalize(input), signal: options?.signal })),
|
|
13
|
+
getReviews: (id, input, options) => reviews(http.request({ method: 'POST', path: `/public/products/${named('an id', id)}/reviews`, body: canonicalize(input ?? {}), signal: options?.signal })),
|
|
10
14
|
};
|
|
11
15
|
}
|
|
12
16
|
//# sourceMappingURL=products.js.map
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { HttpClient } from '../http.js';
|
|
2
|
-
import type { CallOptions,
|
|
2
|
+
import type { CallOptions, StoreData } from './types.js';
|
|
3
3
|
export interface StoreDataApi {
|
|
4
4
|
/**
|
|
5
|
-
* `GET /public/store_data`: what the store is made of for a customer: its settings, the collections, the filters, the tags and
|
|
6
|
-
*
|
|
5
|
+
* `GET /public/store_data`: what the store is made of for a customer: `{ store, collections, filters, tags, attributes }` (its settings, the collections, the filters, and the tags and attributes to filter and sort by).
|
|
6
|
+
* The catalog sends it as `{ data: … }`: it is opened here. `langCode` is the language the names come in: `default` (or none) is the store's default language, and one the store does not use gets it too.
|
|
7
7
|
*/
|
|
8
8
|
get(input?: {
|
|
9
9
|
langCode?: string;
|
|
10
|
-
}, options?: CallOptions): Promise<
|
|
10
|
+
}, options?: CallOptions): Promise<StoreData>;
|
|
11
11
|
}
|
|
12
12
|
export declare function createStoreDataApi(http: HttpClient): StoreDataApi;
|
|
13
13
|
//# sourceMappingURL=store-data.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export function createStoreDataApi(http) {
|
|
2
2
|
return {
|
|
3
|
-
get: (input, options) => http.request({ path: '/public/store_data', query: { langCode: input?.langCode }, signal: options?.signal }),
|
|
3
|
+
get: async (input, options) => (await http.request({ path: '/public/store_data', query: { langCode: input?.langCode }, signal: options?.signal })).data,
|
|
4
4
|
};
|
|
5
5
|
}
|
|
6
6
|
//# sourceMappingURL=store-data.js.map
|
package/dist/catalog/types.d.ts
CHANGED
|
@@ -179,9 +179,9 @@ export interface Pagination {
|
|
|
179
179
|
total: number;
|
|
180
180
|
totalPages: number;
|
|
181
181
|
}
|
|
182
|
-
/** The answer of `POST /public/products`. */
|
|
182
|
+
/** The answer of `POST /public/products` (and of the related products): the catalog sends the list as `data`, here it is `products`. */
|
|
183
183
|
export interface ProductPage {
|
|
184
|
-
|
|
184
|
+
products: ProductSummary[];
|
|
185
185
|
pagination: Pagination;
|
|
186
186
|
}
|
|
187
187
|
export interface Review {
|
|
@@ -197,9 +197,9 @@ export interface Review {
|
|
|
197
197
|
createdAt: string | null;
|
|
198
198
|
updatedAt: string | null;
|
|
199
199
|
}
|
|
200
|
-
/** The answer of `POST /public/products/{id}/reviews
|
|
200
|
+
/** The answer of `POST /public/products/{id}/reviews`: the catalog sends the list as `data`, here it is `reviews`. */
|
|
201
201
|
export interface ReviewPage {
|
|
202
|
-
|
|
202
|
+
reviews: Review[];
|
|
203
203
|
pagination: Pagination;
|
|
204
204
|
/** How many reviews there are for each grade, over all of the product's reviews that match `rating` (a grade nobody gave is not there). */
|
|
205
205
|
breakdown: Partial<Record<'1' | '2' | '3' | '4' | '5', number>>;
|
|
@@ -315,9 +315,9 @@ export interface ProductDetail {
|
|
|
315
315
|
/** All the visible variants. */
|
|
316
316
|
variants: VariantDetail[];
|
|
317
317
|
}
|
|
318
|
-
/** The answer of `POST /public/products/by-slug/{slug}` and `POST /public/products/{id}
|
|
319
|
-
export interface
|
|
320
|
-
|
|
318
|
+
/** The answer of `POST /public/products/by-slug/{slug}` and `POST /public/products/{id}`: the catalog sends the product as `data`, here it is `product`. */
|
|
319
|
+
export interface ProductAnswer {
|
|
320
|
+
product: ProductDetail;
|
|
321
321
|
}
|
|
322
322
|
/** The store's own settings. What the server has not got comes with its default (`EUR`, `once`, `IT`, `default`, a list with the default one). */
|
|
323
323
|
export interface StoreSettings {
|
|
@@ -407,6 +407,7 @@ export interface StoreFilter {
|
|
|
407
407
|
type: 'price' | 'date' | 'number';
|
|
408
408
|
conditions: StoreFilterCondition[];
|
|
409
409
|
}
|
|
410
|
+
/** The answer of `GET /public/store_data` (the catalog sends it as `data`: it is opened, these keys are the answer). */
|
|
410
411
|
export interface StoreData {
|
|
411
412
|
store: StoreSettings;
|
|
412
413
|
tags: StoreTag[];
|
|
@@ -414,8 +415,4 @@ export interface StoreData {
|
|
|
414
415
|
collections: StoreCollection[];
|
|
415
416
|
filters: StoreFilter[];
|
|
416
417
|
}
|
|
417
|
-
/** The answer of `GET /public/store_data`, as the catalog sends it. */
|
|
418
|
-
export interface StoreDataResponse {
|
|
419
|
-
data: StoreData;
|
|
420
|
-
}
|
|
421
418
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -2,6 +2,10 @@ import { type Cache } from './cache.js';
|
|
|
2
2
|
import type { CallOptions } from './catalog/types.js';
|
|
3
3
|
import type { AuthStore } from './auth/store.js';
|
|
4
4
|
import { type Resource, type ResourceIdentity } from './resource.js';
|
|
5
|
+
/** What a customer's call says when nobody is logged in: what the store itself answers to a request with no customer (the same code the wishlist and the reviews use). */
|
|
6
|
+
export declare const CUSTOMER_LOGIN_REQUIRED = "LOGIN_REQUIRED";
|
|
7
|
+
/** A customer's resource, answered: its answer, or (nobody is logged in: there is no resource) the refusal, so a call never gives `null` where a framework's binding would. */
|
|
8
|
+
export declare function loadForCustomer<T>(resource: Resource<T> | null): Promise<T>;
|
|
5
9
|
export interface CustomerResources {
|
|
6
10
|
/** A question about the logged-in customer's data, or null when nobody is. `subject` is what the call is about (an order's id). */
|
|
7
11
|
resource<T>(call: string, subject: string | undefined, params: unknown, fetch: (options: CallOptions) => Promise<T>): Resource<T> | null;
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { cachePrefix } from './cache.js';
|
|
2
|
+
import { CommerceError } from './errors.js';
|
|
2
3
|
import { createResourceFactory } from './resource.js';
|
|
4
|
+
/** What a customer's call says when nobody is logged in: what the store itself answers to a request with no customer (the same code the wishlist and the reviews use). */
|
|
5
|
+
export const CUSTOMER_LOGIN_REQUIRED = 'LOGIN_REQUIRED';
|
|
6
|
+
/** A customer's resource, answered: its answer, or (nobody is logged in: there is no resource) the refusal, so a call never gives `null` where a framework's binding would. */
|
|
7
|
+
export function loadForCustomer(resource) {
|
|
8
|
+
return resource
|
|
9
|
+
? resource.load()
|
|
10
|
+
: Promise.reject(new CommerceError({ kind: 'http', status: 401, code: CUSTOMER_LOGIN_REQUIRED, message: 'Log in to see this.' }));
|
|
11
|
+
}
|
|
3
12
|
export function createCustomerResources(options) {
|
|
4
13
|
const { cache, identity, auth } = options;
|
|
5
14
|
const make = createResourceFactory(cache, identity);
|
package/dist/index.d.ts
CHANGED
|
@@ -28,5 +28,5 @@ export * from './wishlist/index.js';
|
|
|
28
28
|
export * from './shop/index.js';
|
|
29
29
|
export type { ProductsApi } from './catalog/products.js';
|
|
30
30
|
export type { StoreDataApi } from './catalog/store-data.js';
|
|
31
|
-
export type { AssetDetail, AssetSummary, AssetType, AttributeDetail, AttributeSummary, Availability, CallOptions, Format, OptionDetail, OptionValueDetail, Pagination, PriceDetail, PriceRange, ProductByIdInput, ProductBySlugInput, ProductDetail, ProductDetailInput,
|
|
31
|
+
export type { AssetDetail, AssetSummary, AssetType, AttributeDetail, AttributeSummary, Availability, CallOptions, Format, OptionDetail, OptionValueDetail, Pagination, PriceDetail, PriceRange, ProductAnswer, ProductByIdInput, ProductBySlugInput, ProductDetail, ProductDetailInput, ProductLoad, ProductPage, ProductRelatedInput, ProductReviewsInput, ProductSearch, ProductSortField, ProductSummary, Review, ReviewPage, ReviewStats, SortDirection, StoreAttribute, StoreCollection, StoreData, StoreFilter, StoreFilterCondition, StoreSettings, StoreTag, StoreTagValue, TagDetail, TagSummary, TagType, TagValueDetail, TagValueSummary, ValueFormat, VariantDetail, VariantSummary, } from './catalog/types.js';
|
|
32
32
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/orders/api.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { HttpClient } from '../http.js';
|
|
2
2
|
import type { CallOptions } from '../catalog/types.js';
|
|
3
3
|
import type { AccessTokenOptions } from '../auth/api.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { OrderAnswer, OrderPage, PageQuery, SubscriptionAnswer, SubscriptionPage } from './types.js';
|
|
5
5
|
export interface OrderApi {
|
|
6
6
|
/** `GET /store-customer-orders`: the customer's orders, a page at a time (the newest first). */
|
|
7
7
|
list(input?: PageQuery, options?: CallOptions & AccessTokenOptions): Promise<OrderPage>;
|
|
8
8
|
/** `GET /store-customer-orders/:id`: one order, with its items. 404 for one that is not the customer's. */
|
|
9
|
-
get(id: string, options?: CallOptions & AccessTokenOptions): Promise<
|
|
9
|
+
get(id: string, options?: CallOptions & AccessTokenOptions): Promise<OrderAnswer>;
|
|
10
10
|
/** `GET /store-customer-orders/subscriptions`: the customer's subscriptions (what they pay for again and again), a page at a time. */
|
|
11
11
|
listSubscriptions(input?: PageQuery, options?: CallOptions & AccessTokenOptions): Promise<SubscriptionPage>;
|
|
12
12
|
/** `GET /store-customer-orders/subscriptions/:id`. */
|
|
13
|
-
getSubscription(id: string, options?: CallOptions & AccessTokenOptions): Promise<
|
|
13
|
+
getSubscription(id: string, options?: CallOptions & AccessTokenOptions): Promise<SubscriptionAnswer>;
|
|
14
14
|
}
|
|
15
15
|
export declare function createOrderApi(http: HttpClient): OrderApi;
|
|
16
16
|
//# sourceMappingURL=api.d.ts.map
|
package/dist/orders/api.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
import { entity, opened } from '../answer.js';
|
|
2
|
+
// The calls of commerce-core that are about what a customer bought (`/store-customer-orders`), each asking one thing and giving back what the store answers, named: `{ orders, pagination }`, `{ order }`… (the store sends a page
|
|
3
|
+
// as `{ data, pagination }` and an order bare: opened here, once). All need the JWT of the customer, and all read.
|
|
2
4
|
const bearer = (options) => (options?.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : {});
|
|
3
5
|
const part = (id) => encodeURIComponent(id);
|
|
4
6
|
const query = (q) => ({ page: q?.page, limit: q?.limit, sortBy: q?.sortBy, sortDirection: q?.sortDirection, lang: q?.lang });
|
|
5
7
|
export function createOrderApi(http) {
|
|
6
8
|
return {
|
|
7
|
-
list: (input, options) => http.request({ path: '/store-customer-orders', query: query(input), headers: bearer(options), signal: options?.signal }),
|
|
8
|
-
get: (id, options) => http.request({ path: `/store-customer-orders/${part(id)}`, headers: bearer(options), signal: options?.signal }),
|
|
9
|
-
listSubscriptions: (input, options) => http.request({ path: '/store-customer-orders/subscriptions', query: query(input), headers: bearer(options), signal: options?.signal }),
|
|
10
|
-
getSubscription: (id, options) => http.request({ path: `/store-customer-orders/subscriptions/${part(id)}`, headers: bearer(options), signal: options?.signal }),
|
|
9
|
+
list: async (input, options) => opened('orders', await http.request({ path: '/store-customer-orders', query: query(input), headers: bearer(options), signal: options?.signal })),
|
|
10
|
+
get: async (id, options) => entity('order', await http.request({ path: `/store-customer-orders/${part(id)}`, headers: bearer(options), signal: options?.signal })),
|
|
11
|
+
listSubscriptions: async (input, options) => opened('subscriptions', await http.request({ path: '/store-customer-orders/subscriptions', query: query(input), headers: bearer(options), signal: options?.signal })),
|
|
12
|
+
getSubscription: async (id, options) => entity('subscription', await http.request({ path: `/store-customer-orders/subscriptions/${part(id)}`, headers: bearer(options), signal: options?.signal })),
|
|
11
13
|
};
|
|
12
14
|
}
|
|
13
15
|
//# sourceMappingURL=api.js.map
|
package/dist/orders/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createOrderApi } from './api.js';
|
|
2
2
|
export type { OrderApi } from './api.js';
|
|
3
3
|
export { createOrders } from './orders.js';
|
|
4
|
-
export type { Orders } from './orders.js';
|
|
5
|
-
export type { Order, OrderAddress, OrderItem, OrderPage, Page, PageInfo, PageQuery, Subscription, SubscriptionPage } from './types.js';
|
|
4
|
+
export type { Orders, OrdersResources } from './orders.js';
|
|
5
|
+
export type { Order, OrderAddress, OrderAnswer, OrderItem, OrderPage, Page, PageInfo, PageQuery, Subscription, SubscriptionAnswer, SubscriptionPage } from './types.js';
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/orders/orders.d.ts
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CustomerResources } from '../customer-resources.js';
|
|
2
2
|
import type { Resource } from '../resource.js';
|
|
3
3
|
import type { OrderApi } from './api.js';
|
|
4
|
-
import type {
|
|
5
|
-
export interface
|
|
4
|
+
import type { OrderAnswer, OrderPage, PageQuery, SubscriptionAnswer, SubscriptionPage } from './types.js';
|
|
5
|
+
export interface OrdersResources {
|
|
6
6
|
list(query?: PageQuery): Resource<OrderPage> | null;
|
|
7
|
-
get(id: string): Resource<
|
|
7
|
+
get(id: string): Resource<OrderAnswer> | null;
|
|
8
8
|
subscriptions: {
|
|
9
9
|
list(query?: PageQuery): Resource<SubscriptionPage> | null;
|
|
10
|
-
get(id: string): Resource<
|
|
10
|
+
get(id: string): Resource<SubscriptionAnswer> | null;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface Orders {
|
|
14
|
+
/** A page of the customer's orders: `{ orders, pagination }`. */
|
|
15
|
+
list(query?: PageQuery): Promise<OrderPage>;
|
|
16
|
+
/** One order: `{ order }`. */
|
|
17
|
+
get(id: string): Promise<OrderAnswer>;
|
|
18
|
+
subscriptions: {
|
|
19
|
+
/** A page of the customer's subscriptions: `{ subscriptions, pagination }`. */
|
|
20
|
+
list(query?: PageQuery): Promise<SubscriptionPage>;
|
|
21
|
+
/** One subscription: `{ subscription }`. */
|
|
22
|
+
get(id: string): Promise<SubscriptionAnswer>;
|
|
11
23
|
};
|
|
24
|
+
resource: OrdersResources;
|
|
12
25
|
/** Forgets what was kept of orders and subscriptions, so the next question is asked again. */
|
|
13
26
|
refresh(): void;
|
|
14
27
|
}
|
package/dist/orders/orders.js
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
|
+
import { loadForCustomer } from '../customer-resources.js';
|
|
1
2
|
export function createOrders(options) {
|
|
2
3
|
const { api, customer } = options;
|
|
3
4
|
// Nothing asked and an empty ask are the same question
|
|
4
5
|
const asked = (q) => q ?? {};
|
|
5
|
-
|
|
6
|
+
const resource = {
|
|
6
7
|
list: (query) => customer.resource('orders.list', undefined, asked(query), (o) => api.list(query, o)),
|
|
7
8
|
get: (id) => customer.resource('orders.get', id, undefined, (o) => api.get(id, o)),
|
|
8
9
|
subscriptions: {
|
|
9
10
|
list: (query) => customer.resource('subscriptions.list', undefined, asked(query), (o) => api.listSubscriptions(query, o)),
|
|
10
11
|
get: (id) => customer.resource('subscriptions.get', id, undefined, (o) => api.getSubscription(id, o)),
|
|
11
12
|
},
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
list: (query) => loadForCustomer(resource.list(query)),
|
|
16
|
+
get: (id) => loadForCustomer(resource.get(id)),
|
|
17
|
+
subscriptions: {
|
|
18
|
+
list: (query) => loadForCustomer(resource.subscriptions.list(query)),
|
|
19
|
+
get: (id) => loadForCustomer(resource.subscriptions.get(id)),
|
|
20
|
+
},
|
|
21
|
+
resource,
|
|
12
22
|
refresh() {
|
|
13
23
|
for (const call of ['orders.list', 'orders.get', 'subscriptions.list', 'subscriptions.get'])
|
|
14
24
|
customer.forget(call);
|
package/dist/orders/types.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface PageInfo {
|
|
|
13
13
|
total: number;
|
|
14
14
|
totalPages: number;
|
|
15
15
|
}
|
|
16
|
+
/** How the store wraps a page of a list (`{ data, pagination }`): read as it is, and named where it comes in (`opened`, answer.ts). What the package gives back is `OrderPage`, `ReturnRequestPage`… */
|
|
16
17
|
export interface Page<T> {
|
|
17
18
|
data: T[];
|
|
18
19
|
pagination: PageInfo;
|
|
@@ -83,6 +84,22 @@ export interface Subscription {
|
|
|
83
84
|
/** The order the subscription came from. */
|
|
84
85
|
order: Order | null;
|
|
85
86
|
}
|
|
86
|
-
|
|
87
|
-
export
|
|
87
|
+
/** A page of the customer's orders. */
|
|
88
|
+
export interface OrderPage {
|
|
89
|
+
orders: Order[];
|
|
90
|
+
pagination: PageInfo;
|
|
91
|
+
}
|
|
92
|
+
/** One order. */
|
|
93
|
+
export interface OrderAnswer {
|
|
94
|
+
order: Order;
|
|
95
|
+
}
|
|
96
|
+
/** A page of the customer's subscriptions. */
|
|
97
|
+
export interface SubscriptionPage {
|
|
98
|
+
subscriptions: Subscription[];
|
|
99
|
+
pagination: PageInfo;
|
|
100
|
+
}
|
|
101
|
+
/** One subscription. */
|
|
102
|
+
export interface SubscriptionAnswer {
|
|
103
|
+
subscription: Subscription;
|
|
104
|
+
}
|
|
88
105
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/returns/api.d.ts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import type { HttpClient } from '../http.js';
|
|
2
2
|
import type { CallOptions } from '../catalog/types.js';
|
|
3
3
|
import type { AccessTokenOptions } from '../auth/api.js';
|
|
4
|
-
import type { CreateGuestReturnInput, CreateReturnInput,
|
|
4
|
+
import type { CreateGuestReturnInput, CreateReturnInput, GuestReturnSessionAnswer, GuestVerifyAnswer, GuestVerifyInput, PageQuery, ReturnRequestAnswer, ReturnRequestPage } from './types.js';
|
|
5
5
|
export interface ReturnApi {
|
|
6
6
|
/** `GET /store-customer-return-requests` (JWT): the customer's requests, a page at a time. */
|
|
7
7
|
list(input?: PageQuery, options?: CallOptions & AccessTokenOptions): Promise<ReturnRequestPage>;
|
|
8
8
|
/** `GET /store-customer-return-requests/:id` (JWT). */
|
|
9
|
-
get(id: string, options?: CallOptions & AccessTokenOptions): Promise<
|
|
9
|
+
get(id: string, options?: CallOptions & AccessTokenOptions): Promise<ReturnRequestAnswer>;
|
|
10
10
|
/** `POST /store-customer-return-requests` (JWT): asks to return some of the items of an order (`201`). The store emails the customer (and the shop). */
|
|
11
|
-
create(input: CreateReturnInput, options?: CallOptions & AccessTokenOptions): Promise<
|
|
11
|
+
create(input: CreateReturnInput, options?: CallOptions & AccessTokenOptions): Promise<ReturnRequestAnswer>;
|
|
12
12
|
/** `DELETE /store-customer-return-requests/:id` (JWT): cancels a request that is still `pending` (400 for any other); gives back the one it cancelled. */
|
|
13
|
-
cancel(id: string, options?: CallOptions & AccessTokenOptions): Promise<
|
|
13
|
+
cancel(id: string, options?: CallOptions & AccessTokenOptions): Promise<ReturnRequestAnswer>;
|
|
14
14
|
/**
|
|
15
15
|
* `POST …/guest/verify` (no JWT): asks the store to email a link to the address of the order, for a guest. It answers the same whether the order exists or not. An order that belongs to an account is refused
|
|
16
16
|
* (400): its customer has to log in.
|
|
17
17
|
*/
|
|
18
18
|
verifyGuest(input: GuestVerifyInput, options?: CallOptions): Promise<GuestVerifyAnswer>;
|
|
19
19
|
/** `GET …/guest/session?token=` (no JWT): the order the link's token opens. 401 for a token that is not good or has ended. */
|
|
20
|
-
guestSession(token: string, options?: CallOptions): Promise<
|
|
20
|
+
guestSession(token: string, options?: CallOptions): Promise<GuestReturnSessionAnswer>;
|
|
21
21
|
/** `POST …/guest` (no JWT, the token in the body): a guest asks to return some of the items of their order (`201`). */
|
|
22
|
-
createGuest(input: CreateGuestReturnInput, options?: CallOptions): Promise<
|
|
22
|
+
createGuest(input: CreateGuestReturnInput, options?: CallOptions): Promise<ReturnRequestAnswer>;
|
|
23
23
|
/** `DELETE …/guest/:id?token=` (no JWT): cancels a guest's request that is still `pending`. */
|
|
24
|
-
cancelGuest(id: string, token: string, options?: CallOptions): Promise<
|
|
24
|
+
cancelGuest(id: string, token: string, options?: CallOptions): Promise<ReturnRequestAnswer>;
|
|
25
25
|
}
|
|
26
26
|
export declare function createReturnApi(http: HttpClient): ReturnApi;
|
|
27
27
|
//# sourceMappingURL=api.d.ts.map
|
package/dist/returns/api.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
1
|
+
import { entity, opened } from '../answer.js';
|
|
2
|
+
// The calls of commerce-core that are about returns (`/store-customer-return-requests`), each asking one thing and giving back what the store answers, named (`{ returnRequests, pagination }`, `{ returnRequest }`: the store sends a page
|
|
3
|
+
// as `{ data, pagination }` and a request bare: opened here, once). A customer's need their JWT. A guest's need none: what proves who they are is a TOKEN, in the link the store emailed to the address of the order (`guest/verify`
|
|
4
|
+
// asks for the email; the site's page `/returns?token=…` is where the link leads, and reads the token from the address).
|
|
3
5
|
const PATH = '/store-customer-return-requests';
|
|
4
6
|
const bearer = (options) => (options?.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : {});
|
|
5
7
|
const part = (id) => encodeURIComponent(id);
|
|
6
8
|
const query = (q) => ({ page: q?.page, limit: q?.limit, sortBy: q?.sortBy, sortDirection: q?.sortDirection, lang: q?.lang });
|
|
7
9
|
export function createReturnApi(http) {
|
|
8
10
|
return {
|
|
9
|
-
list: (input, options) => http.request({ path: PATH, query: query(input), headers: bearer(options), signal: options?.signal }),
|
|
10
|
-
get: (id, options) => http.request({ path: `${PATH}/${part(id)}`, headers: bearer(options), signal: options?.signal }),
|
|
11
|
-
create: (input, options) => http.request({ method: 'POST', path: PATH, body: input, headers: bearer(options), signal: options?.signal }),
|
|
12
|
-
cancel: (id, options) => http.request({ method: 'DELETE', path: `${PATH}/${part(id)}`, headers: bearer(options), signal: options?.signal }),
|
|
11
|
+
list: async (input, options) => opened('returnRequests', await http.request({ path: PATH, query: query(input), headers: bearer(options), signal: options?.signal })),
|
|
12
|
+
get: async (id, options) => entity('returnRequest', await http.request({ path: `${PATH}/${part(id)}`, headers: bearer(options), signal: options?.signal })),
|
|
13
|
+
create: async (input, options) => entity('returnRequest', await http.request({ method: 'POST', path: PATH, body: input, headers: bearer(options), signal: options?.signal })),
|
|
14
|
+
cancel: async (id, options) => entity('returnRequest', await http.request({ method: 'DELETE', path: `${PATH}/${part(id)}`, headers: bearer(options), signal: options?.signal })),
|
|
13
15
|
verifyGuest: (input, options) => http.request({ method: 'POST', path: `${PATH}/guest/verify`, body: input, signal: options?.signal }),
|
|
14
|
-
guestSession: (token, options) => http.request({ path: `${PATH}/guest/session`, query: { token }, signal: options?.signal }),
|
|
15
|
-
createGuest: (input, options) => http.request({ method: 'POST', path: `${PATH}/guest`, body: input, signal: options?.signal }),
|
|
16
|
-
cancelGuest: (id, token, options) => http.request({ method: 'DELETE', path: `${PATH}/guest/${part(id)}`, query: { token }, signal: options?.signal }),
|
|
16
|
+
guestSession: async (token, options) => entity('returnSession', await http.request({ path: `${PATH}/guest/session`, query: { token }, signal: options?.signal })),
|
|
17
|
+
createGuest: async (input, options) => entity('returnRequest', await http.request({ method: 'POST', path: `${PATH}/guest`, body: input, signal: options?.signal })),
|
|
18
|
+
cancelGuest: async (id, token, options) => entity('returnRequest', await http.request({ method: 'DELETE', path: `${PATH}/guest/${part(id)}`, query: { token }, signal: options?.signal })),
|
|
17
19
|
};
|
|
18
20
|
}
|
|
19
21
|
//# sourceMappingURL=api.js.map
|
package/dist/returns/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createReturnApi } from './api.js';
|
|
2
2
|
export type { ReturnApi } from './api.js';
|
|
3
3
|
export { createReturns } from './returns.js';
|
|
4
|
-
export type { Returns } from './returns.js';
|
|
5
|
-
export type { CreateGuestReturnInput, CreateReturnInput, GuestOrderItem, GuestReturnSession, GuestVerifyAnswer, GuestVerifyInput, ReturnItem, ReturnItemInput, ReturnRequest, ReturnRequestPage, ReturnStatus, } from './types.js';
|
|
4
|
+
export type { Returns, ReturnsResources } from './returns.js';
|
|
5
|
+
export type { CreateGuestReturnInput, CreateReturnInput, GuestOrderItem, GuestReturnSession, GuestReturnSessionAnswer, GuestVerifyAnswer, GuestVerifyInput, ReturnItem, ReturnItemInput, ReturnRequest, ReturnRequestAnswer, ReturnRequestPage, ReturnStatus, } from './types.js';
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,23 +1,34 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CustomerResources } from '../customer-resources.js';
|
|
2
2
|
import type { Cache } from '../cache.js';
|
|
3
3
|
import { type Resource, type ResourceIdentity } from '../resource.js';
|
|
4
4
|
import type { ReturnApi } from './api.js';
|
|
5
|
-
import type { CreateGuestReturnInput, CreateReturnInput,
|
|
6
|
-
export interface
|
|
5
|
+
import type { CreateGuestReturnInput, CreateReturnInput, GuestReturnSessionAnswer, GuestVerifyAnswer, GuestVerifyInput, PageQuery, ReturnRequestAnswer, ReturnRequestPage } from './types.js';
|
|
6
|
+
export interface ReturnsResources {
|
|
7
7
|
list(query?: PageQuery): Resource<ReturnRequestPage> | null;
|
|
8
|
-
get(id: string): Resource<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
get(id: string): Resource<ReturnRequestAnswer> | null;
|
|
9
|
+
guest: {
|
|
10
|
+
/** The order the link's token opens: a resource, so a page shows it (and what is asked of the same token is asked once). */
|
|
11
|
+
session(token: string): Resource<GuestReturnSessionAnswer>;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface Returns {
|
|
15
|
+
/** A page of the customer's return requests: `{ returnRequests, pagination }`. */
|
|
16
|
+
list(query?: PageQuery): Promise<ReturnRequestPage>;
|
|
17
|
+
/** One request: `{ returnRequest }`. */
|
|
18
|
+
get(id: string): Promise<ReturnRequestAnswer>;
|
|
19
|
+
/** Asks to return some of the items of an order: `{ returnRequest }`. Throws what the store refuses (an order that is not the customer's: 404). */
|
|
20
|
+
create(input: CreateReturnInput): Promise<ReturnRequestAnswer>;
|
|
21
|
+
/** Cancels a request that is still pending (the store refuses any other: 400): `{ returnRequest }`. */
|
|
22
|
+
cancel(id: string): Promise<ReturnRequestAnswer>;
|
|
13
23
|
guest: {
|
|
14
24
|
/** Asks the store to email the link (it answers the same whether the order exists or not). */
|
|
15
25
|
requestLink(input: GuestVerifyInput): Promise<GuestVerifyAnswer>;
|
|
16
|
-
/** The order the link's token opens:
|
|
17
|
-
session(token: string):
|
|
18
|
-
create(input: CreateGuestReturnInput): Promise<
|
|
19
|
-
cancel(id: string, token: string): Promise<
|
|
26
|
+
/** The order the link's token opens: `{ returnSession }`. */
|
|
27
|
+
session(token: string): Promise<GuestReturnSessionAnswer>;
|
|
28
|
+
create(input: CreateGuestReturnInput): Promise<ReturnRequestAnswer>;
|
|
29
|
+
cancel(id: string, token: string): Promise<ReturnRequestAnswer>;
|
|
20
30
|
};
|
|
31
|
+
resource: ReturnsResources;
|
|
21
32
|
/** Forgets what was kept of the customer's requests, so the next question is asked again. */
|
|
22
33
|
refresh(): void;
|
|
23
34
|
}
|
package/dist/returns/returns.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
import { loadForCustomer } from '../customer-resources.js';
|
|
1
2
|
import { createResourceFactory } from '../resource.js';
|
|
2
3
|
export function createReturns(options) {
|
|
3
4
|
const { api, customer } = options;
|
|
4
5
|
const make = createResourceFactory(options.cache, options.identity);
|
|
5
6
|
const forgetMine = () => { customer.forget('returns.list'); customer.forget('returns.get'); };
|
|
6
|
-
|
|
7
|
+
const resource = {
|
|
7
8
|
list: (query) => customer.resource('returns.list', undefined, query ?? {}, (o) => api.list(query, o)),
|
|
8
9
|
get: (id) => customer.resource('returns.get', id, undefined, (o) => api.get(id, o)),
|
|
10
|
+
guest: { session: (token) => make('returns.guestSession', token, undefined, (o) => api.guestSession(token, o)) },
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
list: (query) => loadForCustomer(resource.list(query)),
|
|
14
|
+
get: (id) => loadForCustomer(resource.get(id)),
|
|
9
15
|
async create(input) {
|
|
10
16
|
const made = await api.create(input);
|
|
11
17
|
forgetMine();
|
|
@@ -21,10 +27,11 @@ export function createReturns(options) {
|
|
|
21
27
|
},
|
|
22
28
|
guest: {
|
|
23
29
|
requestLink: (input) => api.verifyGuest(input),
|
|
24
|
-
session: (token) =>
|
|
30
|
+
session: (token) => resource.guest.session(token).load(),
|
|
25
31
|
create: (input) => api.createGuest(input),
|
|
26
32
|
cancel: (id, token) => api.cancelGuest(id, token),
|
|
27
33
|
},
|
|
34
|
+
resource,
|
|
28
35
|
refresh: forgetMine,
|
|
29
36
|
};
|
|
30
37
|
}
|
package/dist/returns/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OrderAddress,
|
|
1
|
+
import type { OrderAddress, PageInfo, PageQuery } from '../orders/types.js';
|
|
2
2
|
export type ReturnStatus = 'pending' | 'accepted' | 'rejected' | 'refunded';
|
|
3
3
|
/** One item of a return: which item of the order (`orderItemId` of an `OrderItem`) and how many of it. */
|
|
4
4
|
export interface ReturnItemInput {
|
|
@@ -53,7 +53,15 @@ export interface ReturnRequest {
|
|
|
53
53
|
createdAt: string;
|
|
54
54
|
updatedAt: string;
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
/** A page of the customer's return requests. */
|
|
57
|
+
export interface ReturnRequestPage {
|
|
58
|
+
returnRequests: ReturnRequest[];
|
|
59
|
+
pagination: PageInfo;
|
|
60
|
+
}
|
|
61
|
+
/** One return request (what is asked, read or cancelled). */
|
|
62
|
+
export interface ReturnRequestAnswer {
|
|
63
|
+
returnRequest: ReturnRequest;
|
|
64
|
+
}
|
|
57
65
|
export type { PageQuery };
|
|
58
66
|
/** How a guest says which order: the address it was bought with, and the order's id OR its reference (the number the customer was given). */
|
|
59
67
|
export interface GuestVerifyInput {
|
|
@@ -96,4 +104,8 @@ export interface GuestReturnSession {
|
|
|
96
104
|
items: GuestOrderItem[];
|
|
97
105
|
};
|
|
98
106
|
}
|
|
107
|
+
/** What a guest's link opens, named: `{ returnSession }`. */
|
|
108
|
+
export interface GuestReturnSessionAnswer {
|
|
109
|
+
returnSession: GuestReturnSession;
|
|
110
|
+
}
|
|
99
111
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/reviews/api.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ import type { CallOptions } from '../catalog/types.js';
|
|
|
3
3
|
import type { AccessTokenOptions } from '../auth/api.js';
|
|
4
4
|
import type { MyReviewPage, MyReviewsQuery, ReviewAnswer, ReviewDeletedAnswer, ReviewInput, ReviewPatch } from './types.js';
|
|
5
5
|
export interface ReviewApi {
|
|
6
|
-
/** `GET /reviews/by-customer`: the customer's own reviews, a page at a time. */
|
|
7
|
-
|
|
6
|
+
/** `GET /reviews/by-customer`: the customer's own reviews, a page at a time: `{ reviews, pagination }` (the store sends the list as `data`). */
|
|
7
|
+
list(input?: MyReviewsQuery, options?: CallOptions & AccessTokenOptions): Promise<MyReviewPage>;
|
|
8
8
|
/**
|
|
9
9
|
* `POST /reviews/product-variants/:variantId`: writes the customer's review of a variant (`201`). Refused with `EMAIL_NOT_VERIFIED` (400: the address is not verified), `REVIEW_ALREADY_EXISTS` (409) and 404
|
|
10
10
|
* for a variant that is not the store's.
|
package/dist/reviews/api.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { opened } from '../answer.js';
|
|
1
2
|
// The calls of commerce-core that are about what a customer writes of what they bought (`/reviews`), each asking one thing and giving back what the store answers, unchanged (what it writes or removes comes in `data`).
|
|
2
3
|
// All need the JWT of the customer; a store can turn reviews off (403 "Reviews are disabled for this store"). Reading everybody's reviews of a product is the catalog's.
|
|
3
4
|
const bearer = (options) => (options?.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : {});
|
|
4
5
|
const part = (id) => encodeURIComponent(id);
|
|
5
6
|
export function createReviewApi(http) {
|
|
6
7
|
return {
|
|
7
|
-
|
|
8
|
+
list: async (input, options) => opened('reviews', await http.request({
|
|
8
9
|
path: '/reviews/by-customer', query: { page: input?.page, limit: input?.limit, sortBy: input?.sortBy, sortDirection: input?.sortDirection, rating: input?.rating, lang: input?.lang }, headers: bearer(options), signal: options?.signal,
|
|
9
|
-
}),
|
|
10
|
+
})),
|
|
10
11
|
create: (variantId, input, options) => http.request({ method: 'POST', path: `/reviews/product-variants/${part(variantId)}`, body: input, headers: bearer(options), signal: options?.signal }),
|
|
11
12
|
update: (id, patch, options) => http.request({ method: 'PATCH', path: `/reviews/${part(id)}`, body: patch, headers: bearer(options), signal: options?.signal }),
|
|
12
13
|
remove: (id, options) => http.request({ method: 'DELETE', path: `/reviews/${part(id)}`, headers: bearer(options), signal: options?.signal }),
|
package/dist/reviews/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export { createReviewApi } from './api.js';
|
|
2
2
|
export type { ReviewApi } from './api.js';
|
|
3
3
|
export { createReviews } from './reviews.js';
|
|
4
|
-
export type { Reviews } from './reviews.js';
|
|
4
|
+
export type { Reviews, ReviewsResources } from './reviews.js';
|
|
5
5
|
export { reviewAbility } from './rules.js';
|
|
6
6
|
export type { ReviewAbility } from './rules.js';
|
|
7
7
|
export { REVIEW_ERROR_CODES } from './types.js';
|
|
8
|
-
export type { MyReview, MyReviewPage, MyReviewsQuery, CustomerReview, ReviewAnswer, ReviewDeletedAnswer, ReviewInput, ReviewPatch } from './types.js';
|
|
8
|
+
export type { MyReview, MyReviewPage, MyReviewsQuery, CustomerReview, ReviewAnswer, ReviewDeletedAnswer, ReviewInput, ReviewPatch, ReviewResult } from './types.js';
|
|
9
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import type { AuthStore } from '../auth/store.js';
|
|
2
2
|
import type { CachedCatalog } from '../catalog/cached.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type CustomerResources } from '../customer-resources.js';
|
|
4
4
|
import type { Resource } from '../resource.js';
|
|
5
5
|
import type { ReviewApi } from './api.js';
|
|
6
|
-
import { type MyReviewPage, type MyReviewsQuery, type
|
|
6
|
+
import { type MyReviewPage, type MyReviewsQuery, type ReviewInput, type ReviewPatch, type ReviewResult } from './types.js';
|
|
7
|
+
export interface ReviewsResources {
|
|
8
|
+
list(query?: MyReviewsQuery): Resource<MyReviewPage> | null;
|
|
9
|
+
}
|
|
7
10
|
export interface Reviews {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
/** A page of the customer's own reviews: `{ reviews, pagination }`. */
|
|
12
|
+
list(query?: MyReviewsQuery): Promise<MyReviewPage>;
|
|
13
|
+
/** Writes the customer's review of a variant: `{ review }`. Throws what the store refuses (`EMAIL_NOT_VERIFIED`, `REVIEW_ALREADY_EXISTS`, a variant that is not the store's) and, for a guest, `LOGIN_REQUIRED`. */
|
|
14
|
+
create(variantId: string, input: ReviewInput): Promise<ReviewResult>;
|
|
15
|
+
/** Changes what is given of the customer's own review: `{ review }`. */
|
|
16
|
+
update(id: string, patch: ReviewPatch): Promise<ReviewResult>;
|
|
12
17
|
/** Removes the customer's own review; says which. */
|
|
13
18
|
remove(id: string): Promise<{
|
|
14
19
|
id: string;
|
|
15
20
|
}>;
|
|
21
|
+
resource: ReviewsResources;
|
|
16
22
|
}
|
|
17
23
|
export declare function createReviews(options: {
|
|
18
24
|
api: ReviewApi;
|
package/dist/reviews/reviews.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadForCustomer } from '../customer-resources.js';
|
|
1
2
|
import { CommerceError } from '../errors.js';
|
|
2
3
|
import { REVIEW_ERROR_CODES } from './types.js';
|
|
3
4
|
export function createReviews(options) {
|
|
@@ -6,24 +7,27 @@ export function createReviews(options) {
|
|
|
6
7
|
const login = () => new CommerceError({ kind: 'http', status: 401, code: REVIEW_ERROR_CODES.loginRequired, message: 'Log in to write a review.' });
|
|
7
8
|
/** What was kept that a review changes: the customer's own, and the catalog's reviews of its product (all of them when the product is not known). */
|
|
8
9
|
const forget = (productId) => {
|
|
9
|
-
customer.forget('reviews.
|
|
10
|
+
customer.forget('reviews.list');
|
|
10
11
|
catalog.invalidate(productId ? { call: 'products.getReviews', subject: productId } : { call: 'products.getReviews' });
|
|
11
12
|
};
|
|
13
|
+
const resource = {
|
|
14
|
+
list: (query) => customer.resource('reviews.list', undefined, query ?? {}, (o) => api.list(query, o)),
|
|
15
|
+
};
|
|
12
16
|
return {
|
|
13
|
-
|
|
17
|
+
list: (query) => loadForCustomer(resource.list(query)),
|
|
14
18
|
async create(variantId, input) {
|
|
15
19
|
if (!loggedIn())
|
|
16
20
|
throw login();
|
|
17
21
|
const { data } = await api.create(variantId, input);
|
|
18
22
|
forget(data.productId);
|
|
19
|
-
return data;
|
|
23
|
+
return { review: data };
|
|
20
24
|
},
|
|
21
25
|
async update(id, patch) {
|
|
22
26
|
if (!loggedIn())
|
|
23
27
|
throw login();
|
|
24
28
|
const { data } = await api.update(id, patch);
|
|
25
29
|
forget(data.productId);
|
|
26
|
-
return data;
|
|
30
|
+
return { review: data };
|
|
27
31
|
},
|
|
28
32
|
async remove(id) {
|
|
29
33
|
if (!loggedIn())
|
|
@@ -32,6 +36,7 @@ export function createReviews(options) {
|
|
|
32
36
|
forget();
|
|
33
37
|
return data;
|
|
34
38
|
},
|
|
39
|
+
resource,
|
|
35
40
|
};
|
|
36
41
|
}
|
|
37
42
|
//# sourceMappingURL=reviews.js.map
|
package/dist/reviews/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PageInfo, PageQuery } from '../orders/types.js';
|
|
2
2
|
export interface ReviewInput {
|
|
3
3
|
rating: number;
|
|
4
4
|
title: string;
|
|
@@ -31,11 +31,19 @@ export interface MyReviewsQuery extends PageQuery {
|
|
|
31
31
|
/** Only the reviews with this rating (1 to 5). */
|
|
32
32
|
rating?: number;
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
/** A page of the customer's own reviews. */
|
|
35
|
+
export interface MyReviewPage {
|
|
36
|
+
reviews: MyReview[];
|
|
37
|
+
pagination: PageInfo;
|
|
38
|
+
}
|
|
39
|
+
/** What the store answers to writing a review, as it sends it: the review, in `data` (the package gives it as `ReviewResult`). */
|
|
36
40
|
export interface ReviewAnswer {
|
|
37
41
|
data: CustomerReview;
|
|
38
42
|
}
|
|
43
|
+
/** What writing or changing a review gives back: the review, by name. */
|
|
44
|
+
export interface ReviewResult {
|
|
45
|
+
review: CustomerReview;
|
|
46
|
+
}
|
|
39
47
|
/** What it answers to removing one: the review that was removed (its `id` at least; the store sends the whole row). */
|
|
40
48
|
export interface ReviewDeletedAnswer {
|
|
41
49
|
data: {
|
|
@@ -6,7 +6,8 @@ import { type WishlistItem } from './types.js';
|
|
|
6
6
|
export interface WishlistState {
|
|
7
7
|
/** Whose wishlist this is. `unknown` until it is known whether a customer is logged in; a guest's is empty. */
|
|
8
8
|
owner: Owner;
|
|
9
|
-
|
|
9
|
+
/** The wishlist: what the customer has put in it. */
|
|
10
|
+
wishlist: WishlistItem[];
|
|
10
11
|
/** The variants that are in it: what a heart on a product asks. */
|
|
11
12
|
variantIds: string[];
|
|
12
13
|
count: number;
|
|
@@ -6,7 +6,7 @@ const idsOf = (items) => items.flatMap((i) => i.product.variants.map((v) => v.id
|
|
|
6
6
|
const isDisabled = (error) => error.status === 403 && (error.code === WISHLIST_ERROR_CODES.disabled || /disabled/i.test(error.message));
|
|
7
7
|
export function createWishlist(options) {
|
|
8
8
|
const { api, auth } = options;
|
|
9
|
-
const holder = createStateHolder({ owner: 'unknown',
|
|
9
|
+
const holder = createStateHolder({ owner: 'unknown', wishlist: [], variantIds: [], count: 0, loaded: false, enabled: null, busy: false, error: null });
|
|
10
10
|
const set = holder.set;
|
|
11
11
|
const queue = createSerialQueue((busy) => set({ busy }));
|
|
12
12
|
// Counts the times the wishlist changed hands: an answer that comes after that is about a wishlist that is not this one any more
|
|
@@ -17,22 +17,22 @@ export function createWishlist(options) {
|
|
|
17
17
|
const list = await api.list();
|
|
18
18
|
if (epoch !== mine)
|
|
19
19
|
return;
|
|
20
|
-
set({
|
|
20
|
+
set({ wishlist: list.data, variantIds: idsOf(list.data), count: list.data.length, loaded: true, enabled: true, error: null });
|
|
21
21
|
}
|
|
22
22
|
catch (error) {
|
|
23
23
|
if (epoch !== mine)
|
|
24
24
|
return;
|
|
25
25
|
const failure = toCommerceError(error);
|
|
26
|
-
set(isDisabled(failure) ? {
|
|
26
|
+
set(isDisabled(failure) ? { wishlist: [], variantIds: [], count: 0, loaded: true, enabled: false, error: null } : { error: failure });
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
function toGuest() {
|
|
30
30
|
epoch++;
|
|
31
|
-
set({ owner: 'guest',
|
|
31
|
+
set({ owner: 'guest', wishlist: [], variantIds: [], count: 0, loaded: true, error: null });
|
|
32
32
|
}
|
|
33
33
|
function toCustomer() {
|
|
34
34
|
epoch++;
|
|
35
|
-
set({ owner: 'customer',
|
|
35
|
+
set({ owner: 'customer', wishlist: [], variantIds: [], count: 0, loaded: false, error: null });
|
|
36
36
|
void queue.run(loadCustomer);
|
|
37
37
|
}
|
|
38
38
|
const following = followAuth(auth, { current: () => holder.get().owner, toGuest, toCustomer });
|