github evershopcommerce/evershop v2.3.0

pre-releaseone hour ago

A feature release on top of v2.2.2: outbound webhooks, a country-aware address system, a unified payment capture/void/refund flow with a rebuilt PayPal integration, redesigned transactional emails, a media library, and storefront building blocks for theme authors. It includes everything in v2.2.2 (a start-up fix and several security fixes).

This release contains breaking changes and 6 new database migrations, one of which renames address columns. Please read Breaking Changes and Upgrade Notes before upgrading. If you upgrade from v2.2.1 or earlier, also read the Upgrade Notes of v2.2.2 below.

Highlights

  • Webhooks — a new webhook module: send signed events to up to 10 HTTPS endpoints, with retries and a delivery log.
  • Address Format Registry — country-aware address forms, validation, storage and display, driven by one format record per country.
  • Unified payments — one capture / void / refund flow for every payment method, and a rebuilt PayPal integration (webhook, refunds, reconciliation).
  • Emails — a shared email layout, plus new refund and cancellation emails, in 17 languages.
  • Media library — a paginated, redesigned file browser with rename and paste-to-upload.
  • Theme building blocks — layouts.json, storefront pages split into movable blocks, shared widget typography hooks, theme assets and seed content.

Breaking Changes

Four changes that also affect you when upgrading from v2.2.1 shipped in v2.2.2 and are described there: raw SQL needs sql(), getContextValue(...) takes data literals only, Query.order(uuid) needs proven access, and API route middleware is authenticated.

  • Address columns were renamed (Address Format Registry). On customer_address, cart_address and order_address: full_name → recipient, address_1 → address_line_1, address_2 → address_line_2, city → locality, province → administrative_area, postcode → postal_code; seven nullable columns were added (organization, address_line_3, dependent_locality, sorting_code, given_name, family_name, extra). tax_rate.province / postcode became administrative_area / postal_code, and shipping_zone_province became shipping_zone_region. The migrations rename only (no row is rewritten), but v2.2.1 and v2.2.2 code do not work with the new schema: they still start, but every address reads back empty. The GraphQL Address types and the address REST payloads changed with them, and the types/customerAddress and lib/locale/countries modules were removed (use @evershop/evershop/lib/address: getCountries, getCountryName, isKnownCountry and the address types). Custom SQL, extensions and themes that read the old names must be updated. A script that reverses the address renames only is in the repository at scripts/address-formats/rollback-schema.sql.
  • Payment capture and refund routes were replaced by one contract. Removed: POST /api/stripe/paymentIntents/capture, POST /api/stripe/paymentIntents/refund, POST /api/cod/captures, POST /api/paypal/authorizations/capture, POST /api/paypal/authorizedTransactions, POST /api/paypal/captureTransactions and the PayPal refund route, together with the per-method admin buttons. Use POST /api/orders/:id/capture and POST /api/orders/:id/refunds (admin only); the order page shows generic Capture and Refund buttons. Payment methods now declare optional capture, void and refund handlers in registerPaymentMethod. Cash on Delivery orders get their own cod_* statuses, and a migration moves existing awaiting-payment COD orders from pending to cod_pending and records an offline authorize transaction for them. The order status no longer changes.
  • PayPal return page renamed: /paypal/proccessing is now /paypal/processing (no alias).
  • Storefront pages are now a shell plus blocks. The product, category, cart, checkout, account, order list, blog and search pages keep only the query, the provider and the Areas; every piece is its own page component that registers into a slot at its old position, so the default markup is unchanged. A theme that forked ProductView.tsx, CategoryView.tsx, ProductSingleForm.tsx, ShoppingCart.tsx, Checkout.tsx, MyAccount.tsx, OrderList.tsx, a blog page shell or SearchPage.tsx must delete the inline pieces from its copy, or the page renders them twice.
  • Public ?limit= is capped at system.max_collection_size (default 200, the largest admin grid option).
  • Fresh installs no longer create sample data. The install migration no longer creates the Men / Women / Kids categories or the Color and Size attributes with their options. Existing stores keep what they have. Themes supply their own catalogue through themes/<id>/seed/*.json, read only by evershop seed.
  • sanitize-html was replaced by xss for rich text. The allow-list is the same; serialization differs cosmetically (<br> instead of <br />), and style values containing url(javascript:) or expression() are now dropped. Code that imported sanitize-html through EverShop must declare it itself.
  • Prices and dates follow the active locale (the request locale, then the Store Setting language, then shop.language). Admin pages format in the admin language.

New Features

Webhooks (new webhook core module)

  • Admin → Settings → Webhooks: up to 10 webhooks, each with an HTTPS URL, a signing secret and a list of events (21 event topics, grouped in the form). Includes a test event, a delivery list with filters, a payload viewer and Retry.
  • Delivery is write-first: a webhook_delivery row is stored, sent, and retried from that row (after 1, 5, 30 and 120 minutes, 5 attempts). The row is both the retry queue and the admin log. Delivery is at-least-once; a 2xx response within 10 seconds counts as success. Failed deliveries are kept 30 days; completed ones up to webhookLogLimit (default 100) per webhook.
  • Each request is signed: X-EverShop-Signature: t=<unix>,v1=<HMAC-SHA256> over <timestamp>.<body>; X-EverShop-Delivery stays the same across retries. password is removed from payloads at any depth.
  • Only HTTPS and public addresses are allowed, checked at connect time. Set system.webhook.allowPrivateNetworks to allow local receivers.
  • Extensions can add topics with addProcessor('webhookTopics'). Translated into all 17 languages.
  • Event subscribers placed in subscribers/_all/ now receive every event, and every subscriber gets { name, uuid } as an optional second argument. Delivery of ordinary subscribers is unchanged (at-most-once).

Address Format Registry

  • One country format record (Google libaddressinput shape) now drives the storefront form, server validation, display, GraphQL, emails and every integration mapping. The checkout and the account address book use one schema-driven form with country-specific fields, region lists, telephone and postal patterns.
  • Admin: Settings → Customer (name format, telephone, company, address lines, required fields, default country) and Sell to countries on Settings → Shipping, with a confirmation that lists shipping zones that would be left without a country.
  • Shipment emails now print the delivery address.
  • Public API for extensions: @evershop/evershop/lib/address (patchAddressFormat, registerRegionProvider, registerAddressField, resolveAddressSchema, validateAddress, formatAddress, toIntegrationAddress). Registration is locked after bootstrap, like the other registries.
  • The package now ships a NOTICE file for the bundled address data (derived from Google's libaddressinput).

Payments

  • Unified operations. Capture, void and refund are owned by core (captureOrder, refundOrder) and work the same for every payment method. The order page buttons appear from canCapture / canRefund. New order_refunded and order_canceled events.
  • PayPal was rebuilt: payment is finalized in-process on the return page (the old HTTP call to the store's own URL failed behind a bot challenge or proxy), a verified webhook at POST /api/paypal/webhook (set the webhook ID in the PayPal settings or in system.paypal.webhookId), admin refunds with partial-refund tracking, paypal_pending for pending captures, idempotent transaction recording, current Orders v2 payloads with exact-amount breakdowns (including zero-decimal currencies), and a reconciliation cron (every 30 minutes, system.paypal.abandonedOrderTtlHours, default 6) that captures approved orders and cancels and restocks abandoned ones. When a buyer cancels at PayPal, the abandoned order is now cancelled and restocked before the cart is reactivated.
  • Stripe: the charge amount and currency come from the order instead of the browser, the webhook verifies them, locks the order row and handles payment_intent.payment_failed and charge.refunded.
  • Cash on Delivery is wired into the same contract with its own cod_* statuses.
  • Order status changes are clamped instead of throwing on terminal or reverted states; shipments can be cancelled on refunded orders and cannot be created on terminal ones.

Transactional emails

  • A shared layout (header and logo, content, footer; button, divider and items-table partials; a {{t}} translation helper) now renders the order confirmation, welcome, reset password, shipment created and shipment delivered emails.
  • New emails: refund and cancellation, each switchable with notification_emails.order_refunded / order_canceled (enabled, templatePath).
  • The email copy is translated into all 17 languages (machine-assisted; less common languages deserve a native-speaker review).

Media library and file browser

  • The file browser is paginated for every storage provider (local, S3, Azure Blob, Google Cloud Storage) with cursors, so a folder with thousands of files no longer loads at once. Folders always load completely; listings now include size and type.
  • Rename files (extension is preserved, an existing name is never overwritten), paste-to-upload, a details panel with the full URL, size and dimensions, loading skeletons, and a working scrollbar. A Media library entry joins the CMS menu.
  • The configured upload types (System Setting → File Uploads: PDF, video, audio) are honoured by the browser; before, only images worked.

Storefront and catalog

  • All products page at /products, editable in the page builder, with sorting, filters, pagination, a category tree facet and an attribute facet. Included in the sitemap and offered in the link picker. The category filter now matches the whole subtree.
  • Virtual collections ("newest", "on sale", a category's products) for widgets and the page builder, so a new store's shelves are not empty.
  • Product cards decide Add to Cart themselves: simple products get the button ("Sold out" when out of stock), products with variants get Select options, a link to the product page. The featured items, recommendation, collection spotlight and rich-text product widgets gain the button.
  • Contact form widget with stored submissions: submissions are saved before the email is attempted, with an admin grid (unread / read / spam) and delete. The email outcome is recorded.
  • Admin → landing pages: Replace homepage with a landing page, with an automatic disabled backup and a one-click restore.
  • New featured blogs fallback (newest posts when none are picked), a slideshow placeholder in the page builder, and the page builder shows a loading skeleton while a replaced image renders.

Theme development

  • themes/<id>/layouts.json moves any storefront page component to another Area or sort order without forking the file. Hot reload in development; admin pages are never affected.
  • Theme theme.json can now ship landing pages, upload assets through the store's configured storage provider (theme-asset:<path> tokens) and reference products or collections by a stable key (store-ref:<entity>/<by>/<key>).
  • Shared widget classes evershop-widget__eyebrow, __heading, __subtext and __item-heading let one rule restyle every widget; vertical spacing is consistent. The Section widget follows the page column through --page-max-width and --page-gutter and exposes data-padding.
  • evershop seed is re-runnable and theme-aware: widgets (--widgets), variant groups, category images and a theme's retired categories are handled idempotently, size can drive a variant like colour, and seeded images go through the configured storage provider. The footer copyright is now live (© <year> <store name>) when none is set.

Security

The fixes released in v2.2.2 are listed under that version. This release also fixes:

  • A PayPal capture action that could be called without authentication using only an order UUID; the public PayPal capture and authorize routes were removed.
  • The Stripe charge amount is now derived from the order, not from client data.
  • ?limit= on public pages could request the whole catalog in one response (denial of service).

Performance

  • The product listing COUNT query drops joins it does not need (about 50.7 ms → 26.5 ms on a 300,000-product catalog) and the category facet matches a subtree with one array parameter instead of a recursive subquery (about 79 ms → 2.5 ms). The store-wide attribute facet uses an EXISTS probe (about 48 ms → 2 ms on 1.2 million index rows).
  • Rich text sanitizing no longer bundles sanitize-html into the storefront: about 200 KB less minified (67 KB gzipped).
  • Image and static-asset requests no longer create a session or send a Set-Cookie, so CDNs can cache product images.

Bug Fixes

  • The order confirmation email no longer prefixes the base URL onto absolute (S3 and other remote) thumbnail URLs.
  • og:image URLs are correct for cloud storage.
  • Requests no longer hang when NODE_ENV is neither development nor production, and local extensions no longer vanish in that case.
  • A widget with settings its GraphQL type does not accept no longer takes the whole page down with a 500: unknown keys are dropped with a warning, an unusable widget is left out of the page, and saving such settings is refused.
  • Sorting on the product listing resets the page, and the direction toggle works on the default sort.
  • Display locale for prices and dates comes from the active locale, not only shop.language.
  • Page components are imported in Area sortOrder, so the CSS cascade follows it (headings no longer lose their Tailwind base styles on the cart and checkout pages).
  • Menu items are keyed by their stored id (no duplicate React keys); the mobile menu stays inside the viewport; the header renders its Areas in source order (correct keyboard tab order).
  • Product images default to object-fit: cover, so photos are cropped rather than stretched.
  • Banner and slideshow buttons and headlines are legible over photographs; the split feature's vertical alignment works; call-to-action buttons share one height; either slideshow arrows field can hide the controls.
  • The Section widget lines up with the page column and no longer causes sideways scrolling on Windows and Linux.
  • The gallery slider and the variant selector work with other bundlers and with relative product URLs.
  • The file browser opens above the page builder, and its clicks no longer close the settings drawer.
  • evershop build no longer logs a database connection error (no database exists at build time).
  • Seeding no longer dies on the second page, re-uploads category images, or creates a category a theme meant to retire.

Developer Experience

  • New npm run compile:dev compiles in place without deleting dist/, so a running dev server keeps working. compile:tsc now copies the same non-source files as compile.
  • Jest maps @components/* to the compiled components and stubs stylesheet imports, so page components can be unit tested.
  • New regression tests guard failures that compile and pass CI: a comment inside export const layout, a Node built-in reachable from a React component, and an API route that reads request.body without body parsing.
  • @evershop/postgres-query-builder 2.1.0 adds SelectQuery.getJoins() and pruneUnreferencedLeftJoins().

Dependencies

  • Added: xss; jsdom (dev, for DOM component tests).
  • Removed: sanitize-html and @types/sanitize-html.
  • The multer, sharp, axios, undici and @evershop/postgres-query-builder upgrades, and the removal of cypress, shipped in v2.2.2.

Upgrade Notes

  1. Back up your database. This release runs 6 new migrations: checkout 1.0.12, customer 1.0.5 and tax 1.0.1 (address columns, renames only), cod 1.0.0 (moves awaiting-payment COD orders to cod_pending), cms 1.5.0 (contact_submission) and webhook 1.0.0. They apply automatically on first start. After they run, do not start v2.2.1 or v2.2.2 against the database: it starts, but every address reads back empty. To go back, restore your backup. As an alternative, run scripts/address-formats/rollback-schema.sql (psql --single-transaction -f) while you redeploy the old release: it reverses the address renames and steps the checkout, customer and tax migration records back, but it does not undo the cod status change (orders that were awaiting payment stay cod_pending) or remove the new tables.
  2. Update @evershop/evershop, reinstall dependencies, run npm run build. Use Node.js 20.9 or newer (required since v2.2.2).
  3. Stripe: enable payment_intent.succeeded, payment_intent.amount_capturable_updated, payment_intent.payment_failed, payment_intent.canceled and charge.refunded on your webhook endpoint.
  4. PayPal: create a webhook in the PayPal dashboard pointing at /api/paypal/webhook and save its ID in the PayPal settings. Until then the endpoint answers 503 and the reconciliation cron is the only fallback. The old paypalWebhookSecret setting is gone.
  5. Review your shipping zones under Settings → Shipping. Provinces became regions, and Sell to countries limits which countries checkout accepts.
  6. If you use custom SQL, extensions or a theme: update address column names and the removed types/customerAddress and lib/locale/countries imports; replace calls to the removed payment routes with POST /api/orders/:id/capture and /refunds; delete the inline pieces from forked page shells (see Breaking Changes).
  7. Themes that depended on order-first to place the header's center Area: Areas now render left, center, right in the markup. Check your header.
  8. Rich text is sanitized by xss; check content that relied on <br /> output or on sanitize-html being installed.

Don't miss a new evershop release

NewReleases is sending notifications on new releases.