Skip to main content

@webround/commerce

The logic of a store, in TypeScript, with no interface and no framework (ESM, Node 22 or later).

Create the store​

import { createCommerce } from '@webround/commerce';

const commerce = createCommerce({
storeId: '<store id>',
commerceApiUrl: 'https://commerce-api.webround.com',
catalogApiUrl: 'https://catalog-api.webround.com',
checkoutApiUrl: 'https://checkout-api.webround.com',
});

createCommerce makes what a page needs of one store, together: auth, catalog, cart, checkout, account, addresses, orders, returns, wishlist, reviews and the cache they share. Without checkoutApiUrl the store works and the checkout fails where it is used, saying so. Other options: returnUrls (where the customer comes back to after paying), cartStorage (where a guest's cart is kept: localStorage by default), langCode and cartPricing.

Catalog​

commerce.catalog has the calls of the catalog API and gives back what it answers:

  • products.search(input): a page of products ({ data, pagination }).
  • products.getBySlug(variantSlug, input) and products.getById(id, input): a product with its variants. The slug belongs to a variant, not to a product.
  • products.getReviews(...) and products.getRelated(...).
  • storeData.get(...): the store's own data (tags, collections, filters, currencies, and the settings of reviews and wishlist), which is what controls are drawn from.

The cache keeps the answers for the whole visit: the same question asked from many places is one request, an error is never kept, and commerce.catalog.invalidate(...) forgets what you want asked again.

The shop​

The rules of a shop (what a customer chose, and how it becomes a search) are pure functions in ShopState: a collection, a text, tag values, one range for each filter, a sorting and a page. Every change returns a new state (setRange, toggleTagValue, setSort…) and goes back to page 1, except setPage. toSearch(state, storeData, context) turns it into the catalog's search and never throws. createShopStore keeps the state in memory, or in the address bar with createAddressShopStore, so a search is a link and "back" goes back through the choices. In the address, q is the text, tags the tag values, r.<filter>=min..max a range, sort=<field>:<asc|desc> the order, and page and limit.

Customers​

commerce.auth is the session: signup, login, verifyTwoFactor, logout, refresh. The status is unknown, signedOut, signingIn, challenge (the code of the authenticator app is needed) or signedIn.

  • The token (JWT) of the customer lasts an hour and is kept in memory. What survives a page load is the store's refresh cookie, which JavaScript never sees.
  • The token is renewed a minute before it ends, and again if a request comes back 401. Many requests refused together are one renewal.
  • A failed call throws what the store said: it belongs to the form that made it.
  • Two-factor authentication is part of it, and so are the customer's requests about their account (commerce.account): password reset, email verification, account deletion, two-factor setup and the newsletter.

Cart​

commerce.cart works in two ways with the same calls:

  • A guest has the lines in the browser (wrCart_<storeId> in localStorage), and the store prices them.
  • A customer has the store's cart: every action is a call to it. When a guest logs in, their lines are merged into the customer's cart, and forgotten in the browser only once the store has them.

Actions are add({ variantId, priceId, quantity }) (it adds to a variant that is already there), setQuantity(variantId, quantity), remove(variantId), clear() and refresh(). The state has what the cart is (lines, count), known at once, apart from what it costs (priced, and isStale when it is one change behind). A cart has at most 99 different variants and one currency and cadence; a refused action throws what the store said, and the cart goes back to what the store holds.

Checkout​

commerce.checkout uses the checkout API, with the same token as the customer.

  • quote() is the preview: what the cart costs with the store's rules and hooks. It changes nothing and is never cached: asking again cancels the one on its way. Lines without enough stock are not an error: they come apart in quote.unavailable, and items and totals count only what can be bought.
  • pay(returnUrls?) reserves the stock, makes the order and answers { url, orderId, reference }: you send the customer to url. It does not redirect by itself.
  • What the customer chose (the delivery of each variant, the addresses, the shipping method, the promotion codes, the email of a guest, the invoice) is in state; the lines are the cart's and are never kept here.
  • After paying, on the success page, commerce.checkout.complete() empties the cart and forgets the choices.

The store refuses on purpose with a code: CHECKOUT_BLOCKED (a validation hook), CHECKOUT_HOOK_FAILED and SHIPPING_METHOD_UNAVAILABLE. checkoutRefusalOf(error) reads it. If nothing is left to pay, pay throws NOTHING_TO_PAY without asking the store.

What a customer has​

  • commerce.addresses: a customer's address book, kept by the store; a guest's addresses live in memory for the page. remember(input) keeps an address the checkout just got.
  • commerce.orders: the customer's orders and subscriptions, a page at a time, as resources of the cache.
  • commerce.returns: a customer's return requests (create, cancel) and the guest flow: guest.requestLink({ customerEmail, orderId | reference }) makes the store email a link to your site's page /returns?token=…, where the guest chooses the items. Returns are only for paid orders.
  • commerce.wishlist: a switch (toggle(variantId)) that adds or removes. A guest has none: it throws LOGIN_REQUIRED without asking the store. A store can turn it off.
  • commerce.reviews: a customer writes one review for each variant, with a verified email address (EMAIL_NOT_VERIFIED otherwise). A store can turn reviews off.

Wishlist and reviews are on unless the store's settings say otherwise: storeData carries them, and your interface hides what is off.