This is the full developer documentation for Adobe Commerce Storefront on Edge Delivery Services # Adobe Commerce Storefront Documentation > Complete documentation for Adobe Commerce Storefront > Generated: 2026-08-05T20:48:05.723Z > Source: https://experienceleague.adobe.com/developer/commerce/storefront --- # Blocks and the repository This page shows where your Git repository sits between merchant-authored documents and shopper cart, checkout, or product flows. It also explains how content blocks differ from Commerce blocks and which boilerplate folders hold the wiring. `blocks/` and `scripts/initializers/` are not sample folders. They are the files you edit when a Commerce region on the page breaks or shows the wrong data. [How a page loads](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/how-a-page-loads/) shows the timeline from document to API call. This page focuses on ownership: authors work in documents, EDS converts tables into block `div`s, and your repository provides decorators, initializers, styles, and configurations so Commerce blocks load the correct drop-ins. ## Comparing blocks and drop-in components Each document table becomes a block `div` that EDS and your JavaScript identify. Your repo connects Commerce blocks to drop-ins through decorators, `scripts/initializers/`, styles, and storefront configuration, then pushes to GitHub to drive Edge Delivery builds. Authoring references: https://docs.da.live/, [Document Authoring](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/quick-start/document-authoring/) and [Universal Editor](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/quick-start/universal-editor/) Quick Starts, https://www.aem.live/docs/authoring-guide, https://www.aem.live/docs/. For packages and shared vocabulary, see [Drop-ins at a glance](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/drop-ins-at-a-glance/) and [Boilerplate getting started](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/getting-started/). | | Content blocks | Commerce blocks | Drop-in components | |---|----------------|-----------------|---------------------| | Role | Layout and marketing UI (cards, heroes, columns, headers, footers) | Interactive Commerce experiences (cart, checkout, account, PDP, …) | Packaged UI and logic that Commerce blocks load and initialize | | Where it comes from | Block Collection and custom blocks in `blocks/` | Commerce blocks in `blocks/`, mapped in the boilerplate | npm (Node's package manager. You use it to install drop-in packages — for example, `npm install @dropins/storefront-cart` — in your storefront repository.) packages such as `@dropins/storefront-cart` | | Authored as document tables | Yes | Yes | No — developers add packages and wire initializers in code | | Typical tie to Adobe Commerce | None — no Commerce GraphQL for most blocks | Yes — GraphQL, REST, and services through the boilerplate | Direct — Commerce API calls (GraphQL, REST, services) are built into each package | | Learn more | https://www.aem.live/developer/block-collection | [Key files and folders](#key-files-and-folders) · [Blocks reference](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/blocks-reference/) | [Drop-ins introduction](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/introduction/) | ### Commerce Storefront SDK Drop-in components are built on shared Commerce Storefront SDK (The Drop-in SDK used to build custom drop-ins and related integration logic.) patterns (initialization, rendering, slots, and extension hooks) so behavior and structure stay consistent across cart, checkout, product discovery, and the rest of the set. - [Commerce Storefront SDK](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/) — Reference for APIs, design components, and utilities used across drop-in components - [Drop-ins introduction](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/introduction/) — Full map of B2C and B2B drop-in components and how they install into the boilerplate ### Content blocks and Commerce blocks Both kinds are document tables EDS turns into HTML. The split is what runs next. Content blocks are layout and marketing (cards, columns, headers, footers from the https://www.aem.live/developer/block-collection). They contain no drop-ins or Commerce GraphQL. Commerce blocks load interactive cart, checkout, account, PDP, and similar flows via initializers and `@dropins/*` calling Commerce GraphQL and related APIs. Prefer plain JavaScript for Commerce blocks. React can make achieving a top Lighthouse score hard. See [Libraries](https://experienceleague.adobe.com/developer/commerce/storefront/troubleshooting/faq/#libraries) and the boilerplate blocks as patterns. ### Key files and folders The https://github.com/hlxsites/aem-boilerplate-commerce is your project's Git repository. It separates core AEM block delivery from Commerce-specific code (`scripts/commerce.js`, `scripts/initializers/`, and storefront configuration) so both sides stay maintainable independently. Here is what the top-level layout looks like when you clone the boilerplate. ``` aem-boilerplate-commerce/ ├── blocks/ │ ├── commerce-cart/ │ │ ├── commerce-cart.js # block decorator — loads and mounts the Cart drop-in │ │ └── commerce-cart.css │ └── commerce-checkout/ │ ├── commerce-checkout.js # block decorator — loads and mounts the Checkout drop-in │ └── commerce-checkout.css ├── scripts/ │ ├── scripts.js # page orchestration (eager, lazy, delayed phases) │ ├── commerce.js # Commerce-specific loading and configuration │ └── initializers/ │ ├── cart.js # sets the GraphQL endpoint and labels for the Cart drop-in │ └── checkout.js # sets the GraphQL endpoint and labels for the Checkout drop-in ├── styles/ └── config.json # storefront configuration (endpoints, locale, and so on) ``` The table below explains each key file or folder in detail. For the full https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/scripts and `blocks/`: | File/Folder | Role | |-------------|------| | `scripts/scripts.js` | Page load, block decoration, fonts, and orchestration of eager, lazy, and delayed loading phases. Imports from `scripts/aem.js` and `scripts/commerce.js`. Also the extension point for global DOM decorators, third-party plugins (such as experimentation tools), and any code that must run eagerly on page load. | | `scripts/commerce.js` | Commerce-specific loading, templates, page type detection, Adobe Client Data Layer (ACDL) initialization, and storefront configuration. ACDL is a JavaScript library that captures shopper behavior for analytics — you will see it again when you read about drop-in coordination and analytics. Centralizes all commerce features and keeps them distinct from core AEM logic. | | `scripts/initializers/` | One file per drop-in. Each initializer (A JavaScript module that configures a drop-in when imported, such as setting endpoints, registering dictionaries, and preparing runtime behavior.) sets the GraphQL endpoint, loads UI string translations, and registers the drop-in for rendering. All of that runs at import time, before any block decorates. See [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/) for the code pattern. | | `blocks/` | Commerce and content blocks. See [Content blocks and Commerce blocks](#content-blocks-and-commerce-blocks) above and [Getting started](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/getting-started/) for the repo layout. | Customize and connect: [Blocks reference](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/blocks-reference/), [Blocks configuration](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/configuration/), [Storefront configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/). ## What's next [Drop-ins at a glance](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/drop-ins-at-a-glance/) explains packages, browser runtime, and npm install. --- # Commerce services and backends Shoppers get pages from Edge Delivery Services. Drop-ins call Adobe Commerce for cart, checkout, and account. Optional hosted services accelerate catalog reads. The diagram shows the full stack. Without that picture, Catalog Service, Live Search, or the Storefront Compatibility package can feel like surprise add-ons. [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/) lists them for Commerce on Cloud or on-premises so you can plan installs in the right order. Start by configuring your Commerce connection in [Commerce configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/). Then check [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/) to confirm your setup is supported. ## Coming from Luma or a classic Commerce theme? If you know Luma or another Adobe Commerce theme, focus on one idea: who builds the page the shopper sees. In the classic model, Commerce builds storefront pages with PHP. In this model, Edge Delivery Services sends HTML and JavaScript, and drop-ins call Adobe Commerce APIs from the shopper's browser. You are not replacing the Commerce Admin, catalog, or order tools. You are choosing a different way to assemble the shopper-facing experience. If you still run Luma while you move content and marketing to Edge Delivery, use [Luma Bridge](https://experienceleague.adobe.com/developer/commerce/storefront/setup/discovery/luma-bridge/). It is a PHP module that shares the shopper's cart and sign-in session between Luma and EDS drop-ins so both storefronts stay in sync during the migration. For diagrams that include Luma Bridge and the Adobe Commerce Optimizer Connector, read [Backend topology](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/#backend-topology). Those topics cover phased migration. ## How the pieces connect Commerce blocks, drop-ins, the boilerplate, your backend, and shared Commerce Services all work together on the page. The storefront uses GraphQL (A query language that drop-in components use to request and update data from Adobe Commerce APIs. Catalog Service, Live Search, and the core Commerce API all expose GraphQL endpoints.) for both reads and writes. Commerce Services include Catalog Service (Adobe's fast, read-only GraphQL API for product data. Drop-ins call it instead of core Commerce GraphQL for product pages, search results, and category listings — up to ten times faster.), Live Search (Adobe's AI-powered search service. It returns results instantly as shoppers type and adjusts rankings and facets based on browsing and click signals in the current session.), Product Recommendations, and core GraphQL on your instance for carts, checkout, accounts, and other flows not covered by the hosted catalog layer. All backend options connect to these services. To compare them, see [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/). ```mermaid graph TB EDS["Edge Delivery Services Hosting & CDN"] AEMBlocks["Content blocks Cards, columns, headers"] CommerceBlocks["Commerce blocks Author in DA.live"] EDS --> AEMBlocks EDS --> CommerceBlocks CommerceBlocks --> B2CGroup["B2C Drop-ins Cart, Checkout, Account, and more"] CommerceBlocks --> B2BGroup["B2B Drop-ins Company, Quote, PO, and more"] B2CGroup --> Boilerplate["Commerce boilerplate Storefront configuration + API clients"] B2BGroup --> Boilerplate Boilerplate --> BackendChoice{Backend} BackendChoice --> CloudService["Adobe Commerce as a Cloud Service Fully managed"] BackendChoice --> Optimizer["Adobe Commerce Optimizer Fully managed SaaS"] BackendChoice --> PaaS["Commerce on Cloud or on-premises Your Adobe Commerce instance"] subgraph Services["Commerce Services"] direction TB CS["Catalog Service"] LS["Live Search"] PR["Product Recommendations"] GraphQL["Core GraphQL"] end CloudService -.-> Services Optimizer -.-> Services PaaS -.-> Services style Services fill:#FFF9C4,stroke:#827717,stroke-width:2px classDef storefront fill:#e8f5e9,stroke:#4caf50 classDef cloudService fill:#f3e5f5,stroke:#9c27b0 classDef optimizer fill:#fff3e0,stroke:#ff9800 classDef paas fill:#fce4ec,stroke:#e91e63 classDef commerceSvcBlock fill:#EDE7F6,stroke:#7E57C2 class EDS,AEMBlocks,CommerceBlocks,Boilerplate storefront class CloudService cloudService class Optimizer optimizer class PaaS paas class CS,LS,PR,GraphQL commerceSvcBlock ``` > **API Mesh (advanced)** https://developer.adobe.com/graphql-mesh-gateway/ is an option when your project needs to combine multiple backend sources behind a single GraphQL endpoint. Most new storefronts do not need it. You can add it later if the requirement arises. ## Commerce Services and integrations This section covers the services and packages that connect your storefront to Commerce data, hosted Adobe services, and other Adobe platforms. Most rows below describe calls that go from the storefront into Adobe: drop-in components call hosted services or core GraphQL to get or update Commerce data. Data Connection (An optional Commerce extension that sends storefront and order event data to Adobe Experience Platform for use in personalization, segmentation, and cross-channel campaigns.), later on this page, is different. It sends shopper event data from your storefront out to Adobe Experience Platform. Not all of these are required for every project. What you need depends on your backend and which drop-ins you use. For a checklist of prerequisites (especially on Commerce on Cloud or on-premises), use [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/#prerequisites-by-backend) and [PaaS: required packages and services](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/#paas-required-packages-and-services). ### Services Connector The Services Connector links your Commerce instance to the hosted catalog and search services in this section: Catalog Service, Live Search, and Product Recommendations. On Commerce on Cloud or on-premises, you configure that link once in the Commerce Admin using production and sandbox API keys from your Commerce license owner. All three services share the same key pair and data space after you save that configuration. On Adobe Commerce as a Cloud Service and Adobe Commerce Optimizer, Adobe sets up the hosted side of those services for you as part of the managed model in [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/). You do not repeat the same manual Commerce Services Connector steps that administrators on Commerce on Cloud or on-premises use in the Admin. The Experience League topic in the quick reference table below still explains how API keys and data spaces work if you need the full background for PaaS or for troubleshooting. ### Catalog Service Catalog Service gives your storefront fast, read-only access to product data through a dedicated GraphQL API. Instead of querying your Commerce application each time a shopper opens a product page or browses a category, drop-in components call this API, which Adobe designed specifically for storefront reads. It is built to return catalog reads faster than routing every read through the core Commerce GraphQL API alone. The Product Details drop-in requires Catalog Service, and the Product Discovery drop-in uses it for search results and category pages. ### Live Search Live Search replaces the default Commerce catalog search with an AI-powered experience. When a shopper types in the search field, Live Search returns results instantly and adjusts facets and ranking based on what shoppers are browsing and clicking in that session. It powers the Product Discovery drop-in, which gives you customizable search and product listing pages with filtering. For storefronts that do not use the classic PHP theme, Live Search needs storefront events so it can learn what shoppers click and view. You need to turn on event collection before the AI models have data to work with. See [Analytics](https://experienceleague.adobe.com/developer/commerce/storefront/setup/analytics/instrumentation/) for details. ### Product Recommendations Product Recommendations uses Adobe Sensei, Adobe's AI engine, to show shoppers products they are likely to want. Examples include units like "Customers who bought this also bought" and "Most viewed." You configure and place recommendation units in the Commerce Admin, and they can appear on product pages, the cart page, or anywhere else on your site. A storefront can run without Product Recommendations, but Adobe recommends turning it on when you want AI-driven merchandising blocks on the site. Without the classic PHP theme, Product Recommendations needs the same storefront event collection as Live Search before models have signal. See [Analytics](https://experienceleague.adobe.com/developer/commerce/storefront/setup/analytics/instrumentation/). ### Core GraphQL Core GraphQL is the GraphQL API on your Adobe Commerce backend (the address you set in the boilerplate for drop-ins). Cart, checkout, account, orders, and many other reads and writes still go here. Hosted services such as Catalog Service sit beside that API and speed up specific product reads. They do not remove core GraphQL from your architecture. Adobe Commerce as a Cloud Service can offer one combined GraphQL layer that merges core Commerce fields with the hosted service fields storefronts need. When you run Adobe Commerce yourself, whether on Adobe-managed cloud infrastructure or on your own servers (on-premises), you use the long-standing core and service schemas described in Adobe's web API documentation. For a full overview, see the https://developer.adobe.com/commerce/webapi/graphql/ on developer.adobe.com. ### Data Connection Data Connection sends shopper behavior and order data from your storefront to the Adobe Experience Platform, where other Adobe tools can use it for personalization and segmentation. For example, Adobe Journey Optimizer (a cross-channel marketing automation tool) can use that data to trigger an abandoned cart email. This service is optional for most storefront builds. You only need it if your project uses Adobe Experience Platform features like real-time audience segmentation or cross-channel personalization. According to the https://experienceleague.adobe.com/en/docs/commerce/data-connection/overview on Experience League, the Data Connection extension applies to Adobe Commerce on cloud infrastructure and on-premises projects. It does not apply to Adobe Commerce as a Cloud Service (the fully managed SaaS product), where integrations with Experience Platform follow a different model. The drawing below shows only this optional path so it does not compete with the main stack diagram above. Data Connection is a Commerce extension on your Adobe Commerce instance. It receives storefront activity from the shopper session (for example, through the Adobe Client Data Layer) and back office events from the Commerce application, then forwards compatible data to Experience Platform. For installation and configuration, see the https://experienceleague.adobe.com/en/docs/commerce/data-connection/overview on Experience League. ```mermaid flowchart TB Storefront["Storefront Edge Delivery + browser"] DC["Adobe Commerce Data Connection extension"] AEP["Adobe Experience Platform"] Storefront -->|"Storefront events"| DC DC -->|"Forwarded data"| AEP classDef storefront fill:#e8f5e9,stroke:#4caf50 classDef commerce fill:#fff3e0,stroke:#ff9800 classDef platform fill:#e3f2fd,stroke:#2196f3 class Storefront storefront class DC commerce class AEP platform ``` ### Storefront Compatibility package The Storefront Compatibility package extends the default Commerce GraphQL schema with additional support that drop-ins require. Without it on the Commerce deployment that serves those flows, the drop-ins cannot talk to your backend as they expect. See [Storefront Compatibility Package](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/storefront-compatibility/) for who installs it on each backend. On Adobe Commerce as a Cloud Service and Adobe Commerce Optimizer, Adobe manages this package for you as part of the fully managed model in [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/). On Commerce on Cloud or on-premises (PaaS) with Adobe Commerce Optimizer, install it with Composer on your Commerce server before you finish storefront settings in your GitHub project. ## Quick reference | Service | One-line job | Where to find it | |---|---|---| | Services Connector | On Commerce on Cloud or on-premises, connects Catalog Service, Live Search, and Product Recommendations using Admin API keys. On Adobe Commerce as a Cloud Service and Adobe Commerce Optimizer, Adobe sets up the hosted side for you (see [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/)). | https://experienceleague.adobe.com/en/docs/commerce-merchant-services/user-guides/integration-services/saas | | Catalog Service | Fast read-only catalog data for product pages, list pages, search, and navigation; required by the Product Details drop-in. | https://experienceleague.adobe.com/en/docs/commerce/catalog-service/guide-overview | | Live Search | AI-powered search that powers the Product Discovery drop-in. It needs storefront event collection when you do not use the classic PHP theme. | https://experienceleague.adobe.com/en/docs/commerce/live-search/overview | | Product Recommendations | Optional (recommended). Adobe Sensei units in the Commerce Admin. It needs storefront event collection when you do not use the classic PHP theme. | https://experienceleague.adobe.com/en/docs/commerce/product-recommendations/getting-started/headless /merchants/content-customizations/product-recommendations/ | | Core GraphQL | Required. GraphQL API on your Commerce instance for cart, checkout, account, orders, and other flows; hosted catalog services do not replace it. | https://developer.adobe.com/commerce/webapi/graphql/ | | Data Connection | Optional. Sends storefront and order event data to Adobe Experience Platform for personalization. Applies to Adobe Commerce on cloud infrastructure or on-premises, not Adobe Commerce as a Cloud Service. | https://experienceleague.adobe.com/en/docs/commerce/data-connection/overview | | Storefront Compatibility package (A PHP package you install on Commerce PaaS that extends the GraphQL schema so cart, checkout, account, and order drop-ins can communicate with your backend as expected.) | Required for drop-ins. Manual install on Commerce on Cloud or on-premises with Adobe Commerce Optimizer. Managed automatically on Adobe Commerce as a Cloud Service and Adobe Commerce Optimizer. | [Overview](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/storefront-compatibility/) · [Manual installation](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/storefront-compatibility/install/) | > **Adobe Commerce Services on Experience League** For a complete list of merchandising services for your Adobe Commerce storefront, see the https://experienceleague.adobe.com/en/docs/commerce-merchant-services/user-guides/home on Experience League. The table above links to the services you will use most often with this storefront. ## What's next [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/) explains how to confirm your backend type, required installations, and service connections before configuring your storefront. --- # Drop-ins at a glance A drop-in is a packaged piece of UI and logic for one Commerce job, such as cart, checkout, or sign-in. It loads Commerce data through APIs, often GraphQL. To use a drop-in, you need three things: the npm package, initializer settings in your repo, and a mount point on the page. For setup steps, see [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/). For a full package map, see [Drop-ins introduction](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/introduction/). ## One drop-in: what you provide and what it does In this model, your project provides the npm package, initializer configuration, and DOM mount point. The drop-in then renders the UI and calls Commerce APIs from the browser. For the full path from document to mount, see [How a page loads](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/how-a-page-loads/). ```mermaid %%{init: {'theme':'base', 'themeVariables': { 'edgeLabelBackground':'#ffffff'}}}%% flowchart TB subgraph provides["Your storefront project provides"] npm["npm dependency @dropins/storefront-*"] cfg["Initializer settings GraphQL endpoint, language labels"] dom["Mount target Commerce block region in the DOM"] end di["Drop-in component Packaged UI and logic for one Commerce job (cart, checkout, sign-in, ...)"] subgraph outcomes["At runtime"] ui["Shopper-facing UI Interactive areas for that job"] api["Commerce traffic GraphQL and related calls from this browser only"] end npm --> di cfg --> di dom --> di di --> ui di --> api style di fill:#dbeafe,stroke:#1d4ed8,stroke-width:4px style provides fill:#f8fafc,stroke:#64748b,stroke-width:1px style outcomes fill:#fffbeb,stroke:#d97706,stroke-width:1px style npm fill:#f1f5f9,stroke:#475569,stroke-width:2px style cfg fill:#f1f5f9,stroke:#475569,stroke-width:2px style dom fill:#f1f5f9,stroke:#475569,stroke-width:2px style ui fill:#fff7ed,stroke:#ea580c,stroke-width:2px style api fill:#fff7ed,stroke:#ea580c,stroke-width:2px ``` ## Where drop-ins run After load, drop-ins run in the browser. EDS delivers HTML, CSS, and JS. Commerce requests go straight from the drop-in to your endpoints, not through EDS in the middle. > **If data never loads in the cart or checkout** If your Commerce environment is only reachable on a VPN or a strict IP allow list, remember that the shopper's device must be able to reach the same endpoints you configured. A setup that works for your laptop behind VPN may still fail for a shopper on a normal network. ## How you add drop-ins to a project Install the drop-in via npm (Node's package manager. You use it to install drop-in packages — for example, `npm install @dropins/storefront-cart` — in your storefront repository.) (for example, `@dropins/storefront-cart`). Packages are published on npm. Adobe publishes source for many packages in public repositories under `github.com/adobe-commerce`. Commerce blocks and `scripts/initializers/` load each drop-in using your configured endpoint and labels. For setup steps, see [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/). For a reference implementation, see the https://github.com/hlxsites/aem-boilerplate-commerce. To go deeper, see [Extending drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/extending/). ## How much can you customize? More than you might expect. Each drop-in exposes several customization layers so you can change behavior without editing the package source. - CSS variables and stylesheet overrides control colors, spacing, typography, and layout. - Label overrides change UI text (button labels, errors, placeholders) without opening the package code. - Slots add custom content (for example, a banner or promo message) in named regions of the drop-in layout. - Extension hooks replace logic for specific actions, such as what runs when a shopper clicks `Add to cart`. Most projects stop at CSS and labels. Slots and hooks are there when configuration alone is not enough. See [Extending drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/extending/) for patterns and examples. ## B2C, B2B, and the package list B2C means business-to-consumer (a typical shopper storefront). B2B means business-to-business (accounts, quotes, purchase orders, and similar flows). The [Drop-ins introduction](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/introduction/) lists B2C and shared packages. If you build B2B experiences, you will use extra packages and pages under [Drop-ins B2B](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/) (company, quotes, purchase orders, requisition lists, and related topics). These names show the pattern for consumer-facing drop-ins. Treat the per-drop-in install pages as the source of truth if a name or version changes. B2B installs are covered in [Drop-ins B2B](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/). - `@dropins/storefront-account` - `@dropins/storefront-auth` - `@dropins/storefront-cart` - `@dropins/storefront-checkout` - `@dropins/storefront-order` - `@dropins/storefront-payment-services` - `@dropins/storefront-pdp` - `@dropins/storefront-recommendations` - `@dropins/storefront-wishlist` - `@dropins/storefront-personalization` - `@dropins/storefront-product-discovery` ## What's next [How drop-ins coordinate on a page](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/drop-ins-on-a-page/) explains how multiple drop-ins work together using a shared event bus. --- # How drop-ins coordinate on a page Multiple drop-ins on one page (for example, Cart and Auth) share a single event bus (A shared in-memory channel that lets drop-in components on the same page publish and subscribe to events without depending directly on each other.) (`@dropins/tools/event-bus.js`). They publish and listen for small messages so each package can react without importing the others directly. Read this page before you rely on [Events](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/events/) alone. Otherwise it is easy to mix up in-page bus messages with analytics data or calls to Commerce services. > **Same tab and document** The bus is an in-memory channel for one loaded HTML document in one tab. Code that imports `@dropins/tools/event-bus.js` in that same JavaScript context can emit and listen together. A full navigation to another URL, a new tab, or a separate cross-origin iframe loads a different context, so those environments do not share the same bus instance unless you build an explicit bridge (not part of the default Commerce boilerplate pattern). If you wondered how cart and checkout work when they are different pages: the next document does not inherit the previous page's bus. Commerce keeps the cart on the server, and storefront bootstrap code loads the cart again on the new page so events such as `cart/initialized` occur in that new context. The full walk-through, with Commerce boilerplate file links, is in [Multiple storefront routes](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/events/) on the [Events](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/events/) page. Patterns such as `getCartDataFromCache` are in [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/). API details are in the [Event Bus API reference](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/events/). Each colored box is a drop-in package on the same page. Solid arrows show events emitted to the bus; dashed arrows show events consumed from the bus. Pink is your custom code alongside those drop-ins. ```mermaid %%{init: {'theme':'base', 'themeVariables': { 'edgeLabelBackground':'#ffffff'}, 'flowchart': {'nodeSpacing': 72, 'rankSpacing': 96, 'curve': 'basis'}}}%% flowchart LR subgraph side [Other emitters] direction TB Auth[Auth drop-in] Order[Order drop-in] YourCode[Your code] end subgraph core [Cart, bus, checkout] direction LR Cart[Cart drop-in] EB[Event Bus] Checkout[Checkout drop-in] end Auth -->|"authenticated"| EB Order -->|"order/placed, cart/reset"| EB YourCode -->|"locale, authenticated"| EB Cart -->|"cart/updated, cart/data"| EB Checkout -->|"checkout/updated"| EB EB -.->|"authenticated"| Cart EB -.->|"order/placed"| Cart EB -.->|"authenticated"| Checkout EB -.->|"cart/updated, cart/data"| Checkout EB -.->|"cart/initialized"| Checkout linkStyle 0,1,2,3,4 stroke:#3b82f6,stroke-width:2px linkStyle 5,6,7,8,9 stroke:#6366f1,stroke-width:1.5px,stroke-dasharray:5 style EB fill:#fef3c7,stroke:#f59e0b,stroke-width:3px style Auth fill:#f3e8ff,stroke:#a855f7,stroke-width:2px style Cart fill:#dbeafe,stroke:#3b82f6,stroke-width:2px style Checkout fill:#e0e7ff,stroke:#6366f1,stroke-width:2px style Order fill:#e0e7ff,stroke:#6366f1,stroke-width:2px style YourCode fill:#fce7f3,stroke:#ec4899,stroke-width:2px style side fill:#fafafa,stroke:#cbd5e1,stroke-width:1px,stroke-dasharray:4 style core fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,stroke-dasharray:4 ``` - `events.on(name, handler, options)` — subscribe. Returns a subscription handle. - `events.emit(name, payload)` — publish an event - `subscription.off()` — unsubscribe (call on the handle returned from `events.on`) Each drop-in component documents which events it emits and listens to. See the [event bus API reference](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/events/) and the [drop-in events overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/events/) for details. > **Events vs. analytics** The event bus handles internal communication between drop-in components on the page. It is separate from behavioral data for Adobe Commerce Services (Live Search, Product Recommendations) sent through the Adobe Client Data Layer (a JavaScript library that captures shopper behavior from the storefront for analytics). See [Analytics](https://experienceleague.adobe.com/developer/commerce/storefront/setup/analytics/instrumentation/). ## What's next [Commerce services and backends](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/commerce-services-and-backends/) explains Edge Delivery, the boilerplate, backends, hosted catalog and search services, and prerequisites. --- # How a page loads The diagram shows six stages from a merchant-authored document to a Commerce API call in the browser. The steps below follow the same order. Use them to decide whether a preview problem comes from authoring, HTML delivery, your block code, or Commerce endpoints instead of assuming every empty UI is only a GraphQL issue. ```mermaid graph LR A["Author Creates document"] B["Edge Delivery Services Publishes page"] C["Browser Loads HTML"] D["Block decorator initializer (top level) · decorate(block) · mount UI"] E["Drop-in Renders UI"] F["Commerce API Data"] A -->|"Document"| B B -->|"HTML with divs"| C C -->|"Decorates blocks"| D D -->|"Renders"| E E -->|"Fetch"| F F -->|"Response"| E style A fill:#E3F2FD,stroke:#2196F3,stroke-width:2px style B fill:#F3E5F5,stroke:#9C27B0,stroke-width:2px style C fill:#FFF9C4,stroke:#FBC02D,stroke-width:2px style D fill:#E8F5E9,stroke:#4CAF50,stroke-width:2px style E fill:#FFE0B2,stroke:#FF9800,stroke-width:2px style F fill:#FFCDD2,stroke:#F44336,stroke-width:2px ``` 1. The merchant creates a document in DA.live, Google Docs, or SharePoint. In document-based authoring, a table in the document defines a block on the page — for example, a "Commerce Cart" table becomes the cart block. In the Universal Editor, the merchant edits blocks visually on the rendered page and no document tables are involved. 1. Edge Delivery Services converts the document into an HTML page. Each table becomes a `div` with a class name that matches the block, for example, ``. EDS serves this HTML from servers close to the shopper, which is what makes pages load fast. 1. The shopper's browser requests and loads that HTML. 1. The block decorator (The JavaScript module that runs for a block after the page loads. It imports the initializer, then calls provider.render() to mount the drop-in UI into the block region of the page.) runs for each block on the page. A block decorator is the JavaScript file in your repository for that block, for example, `blocks/commerce-cart/commerce-cart.js`. Edge Delivery Services finds each block's `div` in the HTML, loads the matching file from your repository, and runs its default export. That file imports the drop-in's initializer (A JavaScript module that configures a drop-in when imported, such as setting endpoints, registering dictionaries, and preparing runtime behavior.) at the top level of the file, outside `decorate(block)`, so the Commerce API endpoint and translated labels are configured before `decorate(block)` runs. Inside `decorate(block)`, your code mounts the shopper-facing UI, such as `provider.render()` to render the cart block UI. 1. The drop-in component renders the shopper-facing UI — the cart, checkout form, product detail page, or whichever Commerce experience that block delivers. 1. The drop-in sends requests to your Commerce API endpoints and renders the data it gets back. For example, the Cart drop-in calls Commerce GraphQL to load the shopper's cart items and totals. This call goes from the shopper's browser directly to Commerce. Edge Delivery Services is not in the middle of it. ## What's next [Blocks and the repository](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/blocks-and-repo/) compares content and Commerce blocks and points at the repo folders that wire them to drop-ins. --- # Storefront Architecture These short topics give you a working map of documents, delivery, blocks, drop-ins, events, and Commerce services. You will also see which layer to check first when a block will not render, a drop-in shows no data, or Commerce calls fail. ## How the pieces fit together Merchants create pages as documents. Edge Delivery Services (Adobe's hosting and delivery infrastructure that turns authored documents into fast HTML pages served from servers close to the shopper. You push code to GitHub; Edge Delivery Services builds and publishes automatically.) turns those documents into HTML. Your Git repository runs JavaScript in the shopper's browser to load the right Commerce UI for each block on the page. Drop-in components (NPM packages that provide core Commerce storefront features such as cart, checkout, product details, and account flows.) inside those blocks call Adobe Commerce APIs to load real product, cart, and account data. If you are new to this stack, think of it in three layers: an authoring layer (how merchants create pages), a delivery and logic layer (how pages load and run code), and a data layer (where live Commerce information comes from). The diagram below shows all three at once. ```mermaid %%{init: {'flowchart': {'rankSpacing': 45, 'nodeSpacing': 30}}}%% flowchart TB Doc["Document"] EDS["Edge Delivery Services"] Doc -->|"published by"| EDS EDS -->|"HTML blocks"| ContentPath["Content block"] EDS -->|"HTML blocks"| CommercePath["Commerce block"] CommercePath -->|"loads"| DropIn["Drop-in component"] DropIn -->|"calls"| API["Commerce APIs"] Repo["Commerce boilerplate"] CommercePath -.->|"connects to"| Repo Repo -.->|"configures"| DropIn classDef eds fill:#e8f5e9,stroke:#4caf50 classDef commerce fill:#e3f2fd,stroke:#2196f3 classDef api fill:#fff3e0,stroke:#ff9800 class Doc,EDS,ContentPath eds class CommercePath,DropIn,Repo commerce class API api ``` > **How this differs from a classic Commerce storefront** In a classic storefront, Adobe Commerce often builds shopper-facing pages with PHP. On this storefront, Edge Delivery Services delivers the HTML and JavaScript. Drop-in components call Commerce APIs from the shopper's browser when they need live cart, product, or account data. The Commerce Admin, catalog, and order tools stay on the server. You change how the storefront page is assembled, not how Commerce stores data. ## Key concepts ### Edge Delivery Services Edge Delivery Services (EDS) is Adobe's cloud delivery network for storefront pages. It turns authored documents into HTML and serves those pages from servers close to the shopper, so pages load fast. You do not manage servers or deployments. When you push code to GitHub, EDS builds and publishes automatically. ### Document Authoring and DA.live Document Authoring is the way merchants create and update storefront pages without writing code. Pages are written as documents in Google Docs, SharePoint, or directly in https://da.live (Document Author). Tables in those documents define the blocks on the page. Site Creator (App in Document Author (DA.live) that creates and initializes a storefront by setting up content, optional code, theme choice, and storefront configuration values.), the tool you use to set up a new storefront, also runs inside DA.live. ### Content blocks A content block is a named section of a page that displays layout and marketing content, such as heroes, card rows, columns, headers, or footers. Merchants create them as document tables. EDS converts each table into a `div` with a matching class name. Your repository holds the JavaScript and CSS that renders that `div` on the page. Content blocks do not call Adobe Commerce APIs. ### Commerce blocks A Commerce block is a named page region for interactive Commerce experiences such as cart, checkout, product detail, sign-in, and account. Merchants create Commerce blocks the same way as content blocks, using a table in a document. The difference is what runs in the browser. The block's JavaScript loads a drop-in component and initializes it with your Commerce endpoint and configuration. ### Commerce boilerplate The Commerce boilerplate is a starter template for your storefront repository. It starts from Adobe's https://github.com/hlxsites/aem-boilerplate-commerce and holds everything your storefront needs to run: block decorators (one JavaScript file per block under `blocks/`), initializer scripts that configure each drop-in (`scripts/initializers/`), styles, and your storefront configuration file. This is the code you own, customize, and push to GitHub. ### Drop-in components A drop-in component is an npm package (`@dropins/storefront-*`) that ships a ready-made interactive slice of the Commerce UI, such as cart, checkout, sign-in, or product details. In the diagram above, drop-ins sit between Commerce blocks and Adobe Commerce APIs. A Commerce block loads its drop-in first, then that drop-in calls the APIs for live data. You install drop-ins with npm, configure them in `scripts/initializers/`, and extend or style them when the defaults are not enough. ### Adobe Commerce APIs Adobe Commerce APIs are the endpoints that drop-in components use to read and write live Commerce data like product details, cart contents, orders, account information, and so on. They include GraphQL and REST. Adobe also provides hosted Commerce Services, including Catalog Service, Live Search, and Product Recommendations. These services return catalog data to the storefront faster than the core GraphQL APIs alone. ## Quick reference | Piece | One-line job | Where to find it | |---|---|---| | Document | Where merchants create and edit pages | Google Docs, SharePoint, DA.live, or https://experienceleague.adobe.com/developer/commerce/storefront/merchants/universal-editor/ (a visual editor for rendered pages) | | Edge Delivery Services | Turns documents into fast HTML pages | Adobe-hosted; see https://www.aem.live/docs/ for how it builds and deploys pages | | Content block (Edge Delivery Services blocks used for non-commerce page content and layout, such as cards, columns, headers, and footers.) | Displays text and media layout on a page | `blocks//.js` in your repo | | Commerce block (JavaScript blocks that integrate drop-in components into Edge Delivery Services pages to power storefront commerce experiences.) | Loads a drop-in for interactive Commerce UI | `blocks//.js` in your repo | | Commerce boilerplate (Pre-configured storefront with the components and services you need to get started.) | Starter code for your storefront GitHub repository that connects blocks to drop-ins | https://github.com/hlxsites/aem-boilerplate-commerce | | Drop-in component (NPM packages that provide core Commerce storefront features such as cart, checkout, product details, and account flows.) | Ready-made Commerce UI package | `@dropins/storefront-*` on npm | | Adobe Commerce APIs | Endpoints drop-ins call for live Commerce data | Your Commerce backend — Commerce PaaS, Adobe Commerce as a Cloud Service, or Adobe Commerce Optimizer | ## Go deeper Read in order the first time through. Each topic builds on the previous one. After you have run the boilerplate once, you can jump ahead for lookup. If you open [Events](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/events/) on day one with no context, it can feel out of order; the architecture pages above prepare you for that topic. - [1. How a page loads](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/how-a-page-loads/) — The step-by-step timeline from an authored document to a Commerce API call in the browser. - [2. Blocks and the repository](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/blocks-and-repo/) — How documents, blocks, your Git repo, and drop-ins connect, plus which boilerplate folders to open first. - [3. Drop-ins at a glance](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/drop-ins-at-a-glance/) — What a drop-in package contains, how you install one with npm, and where to go when you need to extend or customize. - [4. Drop-in coordination](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/drop-ins-on-a-page/) — How multiple drop-ins on the same page communicate through a shared event bus so cart, checkout, and auth stay in sync. - [5. Commerce services and backends](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/commerce-services-and-backends/) — The full system stack: hosted Commerce Services, core GraphQL, and how your backend type affects what you install. --- # Backend options The storefront supports a few backend types, each with its own prerequisites, so follow only the path that matches your project. Choosing the wrong type leads to misleading signals. For example, GitHub and DA.live look healthy, but previews or drop-ins fail. As a result, you might install packages your license does not require. For authoring storefront pages (Document Authoring, Universal Editor), see [Document Authoring Quick Start](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/quick-start/document-authoring/). This page covers the Commerce backend the storefront connects to. All drop-in components require an Adobe Commerce as a Cloud Service or Adobe Commerce Optimizer license. Adobe Commerce on Cloud and on-premises customers who add an Adobe Commerce Optimizer license get drop-in support as part of that license. There is no separate "B2B backend" type. Backends fall into the following categories: - Commerce on Cloud or on-premises — Existing Adobe Commerce on Cloud infrastructure or on-premises deployment without an Adobe Commerce as a Cloud Service or Adobe Commerce Optimizer license - Adobe Commerce as a Cloud Service — Cloud Service deployment whose license includes Storefront - Adobe Commerce Optimizer — New Adobe Commerce Optimizer deployment, or an existing PaaS deployment that added Adobe Commerce Optimizer > **Related documentation** This page covers Commerce backends only. For Adobe Commerce product help (Admin, deployments, merchandising services, integrations), start from https://experienceleague.adobe.com/en/docs/commerce on Experience League. For pages and blocks on DA.live, see https://docs.da.live/ or [Document Authoring Quick Start](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/quick-start/document-authoring/). ## Which backend do you have? | Feature | Adobe Commerce (on cloud, on-premises) | Adobe Commerce as a Cloud Service | Adobe Commerce Optimizer | |---------|---------------|-----------------------------------|--------------------------| | License includes Storefront | No | Yes | Yes | | Setup | Manual install required | Fully automated | Fully automated | | Version | v2.4.8+ (self-managed Commerce) | Continuous release (Adobe-managed) | Continuous release (Adobe-managed) | > **PaaS + Adobe Commerce Optimizer Connector** The Adobe Commerce Optimizer Connector syncs catalog and pricing data from Adobe Commerce on Cloud or on-premises into Adobe Commerce Optimizer. You install a PHP extension on the Adobe Commerce platform, and then use the Adobe Commerce Optimizer Studio to configure catalogs, product discovery, merchandising, and other storefront settings. See the https://experienceleague.adobe.com/en/docs/commerce/aco-optimizer-connector/overview for setup instructions. ## Backend topology This diagram focuses on the backend layer. It adds two optional pieces you use during a phased migration to Adobe Commerce as a Cloud Service or Adobe Commerce Optimizer: Luma Bridge (A PHP module on your Commerce instance that reads session cookies from EDS drop-ins, letting Luma pages share the same shopper cart and sign-in session during a phased migration to Edge Delivery Services.) and the Adobe Commerce Optimizer Connector. Luma is Adobe's classic server-side Commerce storefront theme. If you run a Luma (Adobe Commerce's classic server-side storefront theme, built with PHP. If you run a Luma storefront today, Luma Bridge can help share cart and sign-in sessions with EDS drop-ins while you migrate.) storefront and move to Edge Delivery Services, Luma Bridge enables Luma pages and EDS drop-ins to share the same cart and sign-in session. For the full stack (drop-ins, boilerplate, and how services connect), see [Commerce services and backends](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/commerce-services-and-backends/). ```mermaid graph TB subgraph Storefronts["Storefronts"] EDS["Edge Delivery Services Drop-ins, boilerplate"] Luma["Luma / PHP Cart, checkout, account PaaS only"] end subgraph Bridge["Session bridge (optional)"] LB["Luma Bridge PHP on Commerce · session only Same domain"] end subgraph Backends["Backends"] PaaS["Commerce on Cloud or on-premises Catalog, pricing, orders"] ACCS["Adobe Commerce as a Cloud Service Fully managed, license includes Storefront"] ACO["Adobe Commerce Optimizer Catalog Service, merchandising"] end Connector["ACO Connector PaaS-to-ACO sync Catalog, pricing"] EDS -->|"session cookies"| LB LB <--> Luma EDS --> PaaS EDS --> ACCS EDS --> ACO Luma --> PaaS PaaS -->|"Manual install"| Connector Connector --> ACO classDef storefront fill:#e8f5e9,stroke:#4caf50 classDef bridge fill:#fff3e0,stroke:#ff9800 classDef backend fill:#e3f2fd,stroke:#2196f3 classDef connector fill:#f3e5f5,stroke:#9c27b0 class EDS,Luma storefront class LB bridge class PaaS,ACCS,ACO backend class Connector connector ``` ## Prerequisites by backend Open only the tab for your backend and read it end to end. Commerce on Cloud or on-premises includes a manual checklist the two managed backends skip. ### Commerce Platform-as-a-Service (PaaS) #### Product license Adobe Commerce on Cloud or on-premises. The Magento Open Source edition (Adobe's open-source e-commerce platform, which is a separate product from Adobe Commerce) is not supported. A PaaS license alone does not include drop-in components. To use drop-ins on PaaS, you must also hold an Adobe Commerce Optimizer license (see [Adobe Commerce Optimizer](#adobe-commerce-optimizer)). #### Version v2.4.8 or later #### Required packages and services Manual installation is required. See [PaaS: required packages and services](#paas-required-packages-and-services) below for step-by-step instructions. > **Luma Bridge and Adobe Commerce Optimizer** Luma Bridge is a PHP module on Commerce (PaaS). It reads session cookies that EDS drop-ins set, so Luma pages can share cart and sign-in state with EDS. It works with or without Adobe Commerce Optimizer, and it does not connect directly inside drop-in code. If you use the Adobe Commerce Optimizer Connector, Luma Bridge can still share sessions while you migrate. See [Luma Bridge](https://experienceleague.adobe.com/developer/commerce/storefront/setup/discovery/luma-bridge/). ### Adobe Commerce as a Cloud Service #### Product license Adobe Commerce as a Cloud Service (license includes Storefront) #### Requirements Adobe manages all requirements automatically, so no manual installation is required. ### Adobe Commerce Optimizer #### Product license Adobe Commerce Optimizer (license includes Storefront) #### Requirements Adobe fully manages all requirements as a SaaS service, so no manual installation is required. > **Adobe Commerce Optimizer + PaaS** If you are on Commerce on Cloud or on-premises with an existing Luma storefront and add Adobe Commerce Optimizer, you can use [Luma Bridge](https://experienceleague.adobe.com/developer/commerce/storefront/setup/discovery/luma-bridge/) to share sessions between EDS and Luma's cart, checkout, and account pages while content and marketing move to Edge Delivery first. ## PaaS: required packages and services Install and configure the following on Commerce on Cloud or on-premises before you create your storefront. Adobe Commerce as a Cloud Service and Adobe Commerce Optimizer manage these automatically, so skip this section if you are not on Commerce on Cloud or on-premises. ### Minimum checklist before you create a storefront Before Site Creator (App in Document Author (DA.live) that creates and initializes a storefront by setting up content, optional code, theme choice, and storefront configuration values.), manual configuration, or local preview can rely on Commerce APIs, confirm these items on Commerce on Cloud and on-premises: 1. You are on Adobe Commerce v2.4.8 or later (Magento Open Source is not supported for this storefront path). 1. You installed the Storefront Compatibility package (A PHP package you install on Commerce PaaS that extends the GraphQL schema so cart, checkout, account, and order drop-ins can communicate with your backend as expected.), which fills gaps in the core Commerce GraphQL schema that drop-ins require. See [Storefront Compatibility Package](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/storefront-compatibility/) for who installs it, or [Manual installation](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/storefront-compatibility/install/) if you are on Commerce on Cloud or on-premises with Adobe Commerce Optimizer. 1. If you build a B2B storefront, install the Storefront Compatibility B2B Package (SCP-B2B) on the same Commerce instance, after Adobe Commerce B2B is configured there. 1. You connected the storefront services your project needs (at minimum, plan for Services Connector and Catalog Service, then add Live Search, Product Recommendations, and Data Connection per your rollout). Use the [service documentation table](#storefront-services) below for links. ### Storefront Compatibility package Your storefront repo still uses npm for JavaScript drop-ins, but these modules run as PHP on the Commerce server. See [Commerce services](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/commerce-services-and-backends/#commerce-services) to learn how Commerce services fit together. ### Storefront services Catalog Service, Services Connector, Live Search, and Product Recommendations are covered on Adobe Experience League and in [Commerce services](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/commerce-services-and-backends/#commerce-services). Data Connection is optional and comes from a separate product, Adobe Experience Platform. Headless storefronts must send the required https://developer.adobe.com/commerce/services/shared-services/storefront-events/ for Live Search and Product Recommendations. The Adobe Experience League topics in the table cover setup and headless data requirements. On Edge Delivery Services, implement storefront events with the Adobe Client Data Layer and /setup/analytics/instrumentation/. | Service | Documentation | |---------|---------------| | Data Connection | https://experienceleague.adobe.com/en/docs/commerce/data-connection/overview | | Services Connector | https://experienceleague.adobe.com/en/docs/commerce-merchant-services/user-guides/integration-services/saas | | Catalog Service | https://experienceleague.adobe.com/en/docs/commerce/catalog-service/guide-overview | | Live Search | https://experienceleague.adobe.com/en/docs/commerce/live-search/overview; https://experienceleague.adobe.com/en/docs/commerce/live-search/workspace#data-collection | | Product Recommendations | https://experienceleague.adobe.com/en/docs/commerce/product-recommendations/guide-overview; https://experienceleague.adobe.com/en/docs/commerce/product-recommendations/getting-started/headless · /merchants/content-customizations/product-recommendations/ | > **Multiple stores in one Commerce environment** If you are migrating only one store to Edge Delivery Services in a shared Commerce environment, contact Adobe Commerce Support to ensure that Live Search stays active without disabling Elasticsearch for your other stores. ## Next steps After confirming your prerequisites, create your storefront and connect it to Commerce. - [Create a storefront](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/create-storefront/) — Site Creator in DA.live first; manual template and Code Sync when required. - [Commerce configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) — Endpoints, headers, and config.json for your backend. --- # Before you start By the end of this page, you will know what to have ready before you create a storefront, whether you use Site Creator (App in Document Author (DA.live) that creates and initializes a storefront by setting up content, optional code, theme choice, and storefront configuration values.) or the manual GitHub path. If anything here is unfamiliar, follow the link in the table before continuing. This page assumes you are comfortable with JavaScript, HTML, CSS, and basic npm workflows. You do not need to know Adobe Commerce, PHP, or React to get started. ## What you need These tools help you build and deploy a storefront on Edge Delivery Services (Adobe's hosting and delivery infrastructure that turns authored documents into fast HTML pages served from servers close to the shopper. You push code to GitHub; Edge Delivery Services builds and publishes automatically.) (EDS). EDS is Adobe's hosting and delivery layer: it turns authored documents into fast HTML pages. You will need Adobe accounts, common developer tools, and access to your Commerce environment. | What | Why you need it | How to get it | |------|-----------------|---------------| | Adobe ID | Document Author (DA.live) and other Adobe experiences in this workflow expect you to sign in with an Adobe ID. | Open https://www.adobe.com/, then use the account menu in the header to sign in or create an Adobe ID. | | GitHub account | Site Creator creates or links a GitHub repository for your storefront code. | https://github.com/signup (personal or enterprise). | | Document Author (DA.live) access | Site Creator creates your starter content in DA.live. This applies to the default Document Authoring path. If your project uses AEM Sites as the content source, DA.live access is not required. | https://da.live and sign in with the Adobe ID from the first row. For workspace and authoring concepts, see https://docs.da.live/. If your organization uses enterprise access, ask your admin for an invitation. | | Adobe Commerce backend | Every drop-in component (NPM packages that provide core Commerce storefront features such as cart, checkout, product details, and account flows.) — a ready-made Commerce UI package for cart, checkout, account, or product pages — connects to an Adobe Commerce backend. You need to know which backend type you have before you can configure the storefront. | See [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/) to pick Commerce PaaS, Adobe Commerce as a Cloud Service, or Adobe Commerce Optimizer. | | Commerce Admin access and API values | Site Creator can read from your Commerce APIs when they are reachable. After the repo exists, [Storefront configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) asks for base URLs, GraphQL endpoints (Commerce's query language for reading and writing data), and credentials for a headless storefront (the storefront UI runs in the shopper's browser, not on the Commerce server). The exact fields depend on your backend. | Work with your Commerce administrator or solution partner to gather values before you paste them into tools. [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/) lists services and limits per platform. When you have API access and a repository, the https://da.live/app/adobe-commerce/storefront-tools/tools/config-generator/config-generator in DA.live can build a starter storefront configuration from your inputs. Use [Storefront configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) to learn what each field means. | | Node.js | You need Node.js before you run the Commerce boilerplate locally (`npm install`, `npm start`, and drop-in updates). Adobe lists which Node.js release to install in the Prerequisites section of https://experienceleague.adobe.com/developer/commerce/storefront/get-started/create-storefront/. Adobe updates that Prerequisites line when the recommendation changes. The Commerce boilerplate on GitHub does not declare a Node version in `package.json` (there is no `engines` field), so match whatever Prerequisites shows when you read it. | Open Prerequisites on https://experienceleague.adobe.com/developer/commerce/storefront/get-started/create-storefront/, install the Node.js release listed there from https://nodejs.org/en/download, then run `node -v` to confirm. | | npm | The Commerce boilerplate on GitHub uses npm (Node's package manager. You use it to install drop-in packages — for example, `npm install @dropins/storefront-cart` — in your storefront repository.) (Node's package manager) for installs and scripts (`npm install`, `npm start`, `npm run postinstall`). npm ships with Node.js. | Run `npm -v` in a terminal after you install Node.js. | | AEM CLI | The boilerplate `start` script runs `aem up`, so local preview uses the AEM command line client. AEM (Adobe Experience Manager) is the platform underlying Edge Delivery Services. Experience League lists the CLI with Node in the storefront prerequisites for https://experienceleague.adobe.com/developer/commerce/storefront/get-started/create-storefront/. | After Node.js and npm are installed, run `npm install -g @adobe/aem-cli`. See https://www.aem.live/developer/cli-reference. Run `aem --version` to confirm. | | Git | Cloning and pushing to your repository requires Git. | https://git-scm.com/downloads, or run `git --version` to check if it is already installed. | ## Extra steps for some teams The table above is the common baseline. The sections below only matter when that situation applies to you. If you are not yet sure which backend type you have, read [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/) after the Architecture section — it explains the three types (Commerce PaaS, Adobe Commerce as a Cloud Service, and Adobe Commerce Optimizer) and what each one requires. If your backend is Adobe Commerce as a Cloud Service or Adobe Commerce Optimizer, you can skip the Commerce PaaS section below. Adobe manages most storefront service requirements for those backends. ### Commerce PaaS backends If your backend is Commerce PaaS, you can still use Site Creator or the manual GitHub path to create the repository and starter content in Document Author. Those paths are not reserved for other backend types. The difference is your Commerce instance. On Commerce PaaS, finish the [PaaS: required packages and services](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/#paas-required-packages-and-services) checklist so storefront APIs and drop-ins match what the boilerplate expects. That checklist includes the Storefront Compatibility package, Services Connector, Catalog Service, and any other storefront services your rollout needs (for example, Live Search, Product Recommendations, or Data Connection). [Commerce services and backends](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/commerce-services-and-backends/) explains what each service does. Until that Commerce-side work is done, steps that read live data (previewing a product page or running the boilerplate locally) can fail or look incomplete. Install and configure the required pieces on Commerce first, or drop-ins will not work. Adobe Commerce as a Cloud Service and Adobe Commerce Optimizer handle these requirements for you. Using drop-in components requires a license that covers drop-ins: Adobe Commerce as a Cloud Service, Adobe Commerce Optimizer, or Commerce PaaS with Adobe Commerce Optimizer added. Read [Licensing requirements](https://experienceleague.adobe.com/developer/commerce/storefront/licensing/) for who can use drop-ins and how access works. If your backend type is Commerce PaaS as defined on [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/), confirm your situation against both topics before you invest in drop-in work. ### Site Creator, manual GitHub setup, and enterprise policies Site Creator walks you through the repository and starter content in the browser. If you skip it and start from the https://github.com/hlxsites/aem-boilerplate-commerce on GitHub, install the https://github.com/apps/aem-code-sync on that repository. Code Sync redeploys your storefront when you push to `main` and connects Edge Delivery Services to your repo. Follow [Step-by-step setup](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/create-storefront/#step-by-step) for the full GitHub and Code Sync sequence. Some organizations block repository creation or GitHub App installs until an administrator approves them. If you use the manual path, plan time for that approval, or use Site Creator when your organization allows it. ### Sidekick browser extension Sidekick (Browser extension that helps creators edit, preview, and publish content from a content folder, and helps developers open source documents from published pages.) is the browser extension authors use to preview, publish, and open source documents from a storefront page. You do not need it before you create your first site — Site Creator sets up Sidekick project wiring automatically. Authors install the extension from the Chrome Web Store when they are ready to use the toolbar. For setup details and the Edge browser path, see https://www.aem.live/docs/sidekick on AEM Docs. For a walkthrough that fits this page, see the optional Sidekick step in [Create a storefront](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/create-storefront/#optional-use-sidekick-for-content-editing). ## What you do not need right now The table above lists Node.js, npm, and the AEM CLI because you will need them once you clone the repository and run the boilerplate locally. You do not need them yet. You do not need a code editor, a cloned repository, or a local server to create a storefront. Site Creator and DA.live run entirely in the browser. Install and use those tools when you clone the repository and run the boilerplate on your machine. Follow [Create a storefront](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/create-storefront/) for the full sequence (including the https://www.aem.live/developer/cli-reference for local preview). Then use [Run it locally](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/getting-started/) for day-to-day commands after your site exists. ## Documentation hubs These links open related documentation sites outside this storefront collection. You do not read them end to end before you start. Open them when a step names Edge Delivery, Document Authoring, or the Commerce Admin and you want the official reference next to this page. - Edge Delivery Services (blocks, CDN, redirects, authoring concepts): https://www.aem.live/docs/. - Document Authoring (DA.live workspace, tools, admin API): https://docs.da.live/. - Adobe Commerce merchant documentation (Admin configuration, catalog, B2B, upgrades): https://experienceleague.adobe.com/en/docs/commerce. - Adobe Commerce developer documentation (APIs, App Builder, extensibility hub): https://developer.adobe.com/commerce/docs/. ## What's next When every row in the table above is confirmed, you are ready to move on. You should be able to sign in with your Adobe ID, verify your Node.js version, and know which Commerce backend your project uses. 1. [Storefront Architecture](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/) — learn about pages, blocks, drop-ins, events, and services before you lock backend work or create a site. 1. [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/) — confirm backend type and prerequisites. 1. [Create a storefront](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/create-storefront/) — use Site Creator in DA.live to create your repository and starter content. --- # Browser compatibility Supported browsers depend on your boilerplate suite. Open `package.json` and find the `@dropins/tools` line, then match it to a row below before you spend time on layout testing. That way you do not report a storefront bug for a browser this suite never supported. ## Which boilerplate suite do I have? Match `@dropins/tools` in `package.json` to the corresponding suite below. - March 2026 suite: `@dropins/tools@~1.8.0` - February 2026 suite: `@dropins/tools@~1.7.0` - January 2026 suite: `@dropins/tools@~1.6.0` - October 2025 suite: `@dropins/tools@~1.5.0` - August 2025 suite: `@dropins/tools@~1.4.0` - June 2025 suite: `@dropins/tools@1.3.0` - April 2025 suite: `@dropins/tools@0.42.0` - December 2024 suite: `@dropins/tools@0.38.0` ## Browser support by suite ### Desktop browsers | Browser | `@dropins/tools` ~1.5.0 and later (October 2025-March 2026 suites) | `@dropins/tools` ~1.4.0 and earlier (August 2025 suite and older) | |---------|-------------------------------------------------------------------|---------------------------------------------------------------------| | Chrome | 105 - 141 | 98 - 136 | | Edge | 105 - 141 | 98 - 136 | | Safari | 16 - 18.4 | 16 - 18.4 | | Firefox | 110 - 143 | 108 - 138 | | Opera | 92 - 122 | 85 - 118 | ### Mobile browsers | Platform | Browser | `@dropins/tools` ~1.5.0+ (Oct 2025-Mar 2026) | `@dropins/tools` ~1.4.0 and earlier | |----------|------------------|---------------------------------------------|-------------------------------------| | Android | Chrome | 141 | 136 | | Android | Edge | 141 | 136 | | Android | Firefox | 143 | 138 | | Android | Samsung Internet | 28 | 26 | | Android | UC Browser | Not Supported | Not Supported | | iOS 16+ | Safari | Default supported version | Default supported version | | iOS 16+ | Chrome | Default supported version | Default supported version | ## Known issues For the following browsers, the product listing page loads in a single product column and does not support responsiveness: - Chrome: 100 - 104 - Firefox: 108 and 109 - Opera: 91 or less ## Test URL You can test browser compatibility using the Commerce boilerplate test site: https://www.aemshop.net/ ## Browser testing recommendations When you test your storefront, try the following: - Run checks on the oldest supported version of each major browser you care about. - Run checks on the newest version of each major browser. - Test on real phones and tablets as well as desktop. - Resize the window and confirm layouts still make sense at common widths. - On product listing pages, confirm multi-column grids render as you expect. --- # Create a storefront By the end of this page, you will have a new GitHub repository from the Commerce boilerplate, the Code Sync app on that repo, a storefront configuration for your Commerce backend, starter content in Document Authoring on DA.live, and a local clone. Use the [Which backend do you have?](#which-backend-do-you-have) table and finish prerequisites before the tasks below. When Commerce data or services are missing, the fix is usually the backend checklist, not a broken repository. For `npm install`, `npm start`, and local preview, use [Boilerplate getting started](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/getting-started/). If accounts and tools are not ready, read [Before you start](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/before-you-start/) first. You can return later for optional steps such as Sidekick (Browser extension that helps creators edit, preview, and publish content from a content folder, and helps developers open source documents from published pages.) or the Universal Editor. ## Site Creator or this walkthrough This page walks through repository creation, Code Sync, configuration, content, and local setup in order. When your organization allows it, use https://da.live/app/adobe-commerce/storefront-tools/tools/site-creator/site-creator in https://da.live/ so one flow can create the GitHub repo, add starter content, and fill most storefront configuration. If you cannot use Site Creator, follow GitHub, Code Sync, and the DA.live config generator as separate tasks below. ### Which should you use? Use Site Creator first when you want the fastest path and your organization allows it. Stay on this page when you need to see how the pieces connect, or when Site Creator is not an option. Typical cases include the following: - You are new to the stack and want to see what each step does before you run it in a live project. - Your organization limits who can create repositories or install GitHub Apps, so you follow an approved process step by step. - You are fixing a store that is only partly configured, or you must run steps in a fixed order for automation. - You already have a repository and you use Site Creator only to connect content. You may still run some tasks by hand, and the sections below explain what each part is for. ## Big picture This tutorial uses Edge Delivery Services (Adobe's hosting and delivery infrastructure that turns authored documents into fast HTML pages served from servers close to the shopper. You push code to GitHub; Edge Delivery Services builds and publishes automatically.) (EDS) to host the storefront, the Commerce boilerplate for code, and Document Authoring (DA.live) for page content. You create a new EDS storefront from the https://github.com/hlxsites/aem-boilerplate-commerce. After you connect your own backend, the same repo holds local preview, demo content, and a path to production. The diagram caption and the numbered list below describe the same flow, so you can skim either one first. ![Steps to create and configure your starter storefront.](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/images/CreateStorefrontSteps.png) *Steps to create and configure your starter storefront.* 1. Create the site repository — Generate a storefront repository from the https://github.com/hlxsites/aem-boilerplate-commerce. 1. Add the Code Sync app — The app redeploys your storefront when you push to the `main` branch. It connects Edge Delivery Services to your repository so code and content stay aligned, and it sets where your site's content is registered in the Edge Delivery configuration. 1. Link the repository to Commerce data — Add a storefront `config.json` (or equivalent) in the repo with values for your backend. 1. Add content — In https://da.live/, use the https://da.live/app/adobe-commerce/storefront-tools/tools/site-creator/site-creator app to create or attach a content folder. 1. (Optional) Universal Editor — Edit content in context on the rendered page. 1. (Optional) Sidekick — Preview, publish, and open source documents from the live site. 1. Set up a local environment — Clone the repo, install dependencies, and run the boilerplate on your computer. 1. Secure the storefront — Tighten access to content, the repository, and the site before production or a wide audience. > **Document Authoring content source** This walkthrough uses Document Authoring (The tooling for creating storefront pages as documents in Google Docs, SharePoint, or DA.live without writing code. Tables in documents define the blocks that appear on each page.) on https://da.live/, the default for new EDS storefronts. For AEM Sites content (for example, Commerce drop-ins on an existing AEM as a Cloud Service site), content setup and Universal Editor steps differ. See the https://github.com/adobe-rnd/aem-boilerplate-xcom and https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/edge-delivery/overview. ## Which backend do you have? | Scenario | Description | Prerequisites | |----------|-------------|---------------| | [Commerce PaaS](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/#commerce-platform-as-a-service-paas) | Existing Adobe Commerce on cloud or on-premises without an Adobe Commerce as a Cloud Service or Adobe Commerce Optimizer license (see [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/)) | Manual installation: Storefront Compatibility package, Services Connector, Catalog Service, and other storefront services your project needs. See [PaaS: required packages and services](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/#paas-required-packages-and-services). | | [Adobe Commerce as a Cloud Service](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/#adobe-commerce-as-a-cloud-service) | Fully managed Commerce SaaS. License includes Storefront. | Skip the Commerce PaaS storefront install checklist. Adobe runs the Commerce backend and storefront services for your license. See [Adobe Commerce as a Cloud Service](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/#adobe-commerce-as-a-cloud-service) for details. | | [Adobe Commerce Optimizer](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/#adobe-commerce-optimizer) | Fully managed Optimizer SaaS for catalog and merchandising. License includes Storefront. | Skip the Commerce PaaS storefront install checklist. Adobe runs the Optimizer services your license includes. See [Adobe Commerce Optimizer](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/#adobe-commerce-optimizer) for details. | Before you continue, confirm prerequisites on [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/) for whichever scenario in the table above matches your Commerce backend. On Commerce PaaS, finish [PaaS: required packages and services](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/#paas-required-packages-and-services) on the Commerce instance first. On Adobe Commerce as a Cloud Service or Adobe Commerce Optimizer, skip the Commerce PaaS storefront install checklist because your licensed environment already includes the services the storefront expects. The steps below apply to every backend unless a note says otherwise. Backend-specific detail starts at [Link your repository to Commerce data](#link-your-repository-to-commerce-data). On Adobe Commerce as a Cloud Service or Adobe Commerce Optimizer, run [Link your repository to Commerce data](#link-your-repository-to-commerce-data) only after [Commerce services and integrations](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/commerce-services-and-backends/#commerce-services) shows catalog data in sync. If you are unsure, use [Data export validation](https://experienceleague.adobe.com/developer/commerce/storefront/setup/discovery/data-export-validation/) first. If you already have an Edge Delivery site for this Commerce project, skip creating a brand-new site and use the https://da.live/docs/operations/import for code and content. ## Example site The CitiSignal demo site was built from the same boilerplate you will set up to develop your own storefront. You can open the full demo site at https://main--citisignal-one--adobedevxsc.aem.live/. [![CitiSignal demo storefront homepage showing navigation, hero banner, and product categories.](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/images/citisignal-demo.png)](https://main--citisignal-one--adobedevxsc.aem.live/us/en/) ## Manual setup without Site Creator The [Site Creator or this walkthrough](#site-creator-or-this-walkthrough) section explains when to use this long-form path. The tasks below assume you create the GitHub repository from the https://github.com/hlxsites/aem-boilerplate-commerce, install the https://github.com/apps/aem-code-sync, and use Site Creator for content (including attaching an existing repository), as in [Before you start](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/before-you-start/). If your organization only allows Site Creator for brand-new sites, work with your administrator, then pick up at the Add content task with the repo Site Creator already created. ## Step-by-step The tasks below follow the same order most teams use: GitHub repository, Code Sync, Commerce data, Document Authoring content, optional tooling, local clone, then security. Run them in order unless a note tells you to wait or skip. Open [Storefront Architecture](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/) or [Before you start](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/before-you-start/) when a label or step does not make sense. ### 1. Create the site repository This task requires a GitHub account with access to the organization or account where you want to create the new repository. All three backends use the same boilerplate template. ### Personal GitHub account Create the repo under your personal account. When you select Owner, choose your username. Install the Code Sync app on the repository in the next step. ### GitHub Enterprise (organization) Create the repo under your organization. When you select Owner, choose the organization. You may need organization admin approval to install the Code Sync app on a repository. Ensure you have permission to create repositories and install GitHub Apps in the organization. > **Trouble with organization access?** If you have trouble with your organization access, you can create your repo from a personal account first, then migrate that repo to a https://www.aem.live/docs/repoless Edge Delivery Services site after you learn more about EDS and repoless. ![Create your storefront repo](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/images/create-repo.webp) *Create your storefront repo.* > **Sign in to GitHub first** Sign in to your GitHub account before you open the repository's **Use this template** control. If you are not signed in, that control does not appear. 1. Navigate to https://github.com/hlxsites/aem-boilerplate-commerce. 1. Select the **Use this template** button. 1. Select **Create a new repository** when GitHub shows that option. The repository creation form opens. 1. Complete the form with the following details: - Repository template: `hlxsites/aem-boilerplate-commerce` (default). - Include all branches: Do not include all branches (default). - Owner: Your organization or account (required). - Repository name: A unique name for your new repo (required). GitHub allows letters, numbers, hyphens, and underscores. For Commerce on Edge Delivery Services, use lowercase letters, numbers, and hyphens only so preview and live URLs match the usual `main--your-repo--your-org` pattern. - Description: A brief description of your repo (optional). - Public or Private: We recommend public (default). 1. Select the Create repository button and watch GitHub create your new storefront repo. 1. After a few seconds, you should be redirected to the home page of your new repo. > **Managed backends** Adobe Commerce as a Cloud Service and Adobe Commerce Optimizer use the same repo creation steps above. The only difference is in the next step when you configure the storefront to point to your managed environment. ### 2. Add the Code Sync app Install the Code Sync app on your repository (it cannot be installed globally on a user or org). It redeploys your storefront site whenever you push or merge changes to the `main` branch. That behavior is the same for every backend in the table above. ![Add AEM Code Sync to your repository.](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/images/code-sync.webp) *Add AEM Code Sync to your repository.* > **Slightly different UIs** A gray background on an organization or account indicates that at least one repository within it has the Code Sync app installed. In such cases, you are redirected to the Code Sync configuration page for that org or account, where you choose which repositories have the app. There are no differences in the steps, but the UI differs slightly from the one shown in the diagram (Install versus Save buttons), which shows the Code Sync page when installing on a repository for the first time. ### Personal GitHub account Select your username when choosing where to install. You will see your personal repositories in the selector. ### GitHub Enterprise (organization) Select your organization. You may need admin approval to install the Code Sync app on repositories. If your organization uses SAML SSO, you may need to authorize the app for SAML SSO access. 1. Navigate to the https://github.com/apps/aem-code-sync. 1. Select the **Configure** button (top right). GitHub opens the page where you choose which repository receives the app. 1. Select the organization or account that owns the repo you just created. 1. In the form, choose **Only select repositories**. 1. Open the **Select repositories** selector and choose your repo from the list. 1. Select **Install** (or **Save**; see the note above) to install Code Sync on your repository. 1. You should see a success screen if the installation completed without errors. Your repo is now connected to the Edge Delivery Services code bus. 1. (_Optional_) If you return to the https://github.com/apps/aem-code-sync/installations/select_target, your repo's organization or account will now be gray with **Configure** added. Select your organization or account again to access the Code Sync configuration page, where you can see which repositories have Code Sync installed and add it to more repositories. ### 3. Link your repository to Commerce data To connect your Commerce backend to the storefront repository, use the https://da.live/app/adobe-commerce/storefront-tools/tools/config-generator/config-generator. It generates a [storefront configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) file for your site. The file differs by backend type. Follow only the tab for your backend in the config generator and in the reference. Add or update the storefront configuration for your project: 1. Open the https://da.live/app/adobe-commerce/storefront-tools/tools/config-generator/config-generator. 1. Select your backend type so the tool generates the correct storefront configuration structure. 1. Enter the values for your project. See the [Storefront configuration reference](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) for field descriptions (select the tab for your backend). 1. Save the generated JSON as `config.json` at the repository root. The storefront runtime requests `/config.json` from your site. 1. The template includes `demo-config.json` as a sample, not a committed `config.json`. After your root `config.json` matches your backend, remove `demo-config.json` or leave it unused so one file stays authoritative. 1. Commit and push the new or updated `config.json` to the repo. ### 4. Add content In this task, you create and initialize the content side of your storefront in the Document Authoring environment. The steps are the same for all backends. > **Demo Content** If you do not want demo content, you can create a file in https://da.live/ replacing `{org}` and `{site}` with your own. This reserves the org and site for you to add your own content. 1. Open the https://da.live/app/adobe-commerce/storefront-tools/tools/site-creator/site-creator in DA.live. 1. Select **Use existing repository**. 1. Copy the GitHub owner and site values into the input fields. 1. Select **Create site**. 1. Follow the prompts until Site Creator finishes copying starter content into your content folder. 1. If prerequisites were incomplete, you may need to preview or publish the content manually. ![Initialize your storefront content](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/images/site-creator.png) *Initialize your storefront content* ### 5. (Optional) Use Universal Editor for content editing Use this path when authors need in-context editing beyond what Site Creator sets up by default. For workflows and documentation links, see [Using the Universal Editor](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/quick-start/universal-editor/). ### 6. (Optional) Use Sidekick for content editing > **Sidekick setup** Site Creator wires your project for Sidekick so the toolbar can recognize preview and live URLs. Authors install the browser extension from the Chrome Web Store, or follow Edge steps on https://www.aem.live/docs/sidekick, when they are ready. That page covers install steps, site config, preview, and publish. For prerequisites that line up with this page, see [Before you start](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/before-you-start/#sidekick-browser-extension). ### 7. Set up local environment Clone the repository to your computer so you can work on the code and run a local preview. On GitHub, open your storefront repository, select the Code button, and copy the clone URL (HTTPS or SSH). In a terminal, run `git clone` with that URL, then `cd` into the folder GitHub created. For example, if the clone URL is `https://github.com/my-org/my-storefront.git`, you would run: ```bash frame="none" git clone https://github.com/my-org/my-storefront.git cd my-storefront ``` Use your own organization, repository, and folder names from GitHub. The commands for `npm install`, `npm start`, and opening the preview in a browser all live on [Running locally](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/getting-started/#running-locally) in Boilerplate getting started so this page stays shorter. That page also explains what to do if a command fails or port `3000` is already in use. ### 8. (Optional) Secure your storefront The storefront works at this point, but the site is not protected by default. We recommend securing your content, repository, and site access before you deploy to production or share the site with external users. See [Security and access](https://experienceleague.adobe.com/developer/commerce/storefront/setup/launch/#security-and-access) in the Launch checklist for step-by-step instructions. > **What you have now** You have a Commerce storefront project on GitHub, starter content in Document Authoring, and a local clone of the repository. Next, use [Boilerplate getting started](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/getting-started/) to run and explore the code, then customize the boilerplate and drop-in components (NPM packages that provide core Commerce storefront features such as cart, checkout, product details, and account flows.) to match your project. ## What's next - [Boilerplate getting started](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/getting-started/) — Install paths, `npm` scripts, and a typical local loop after your repository exists. - [Boilerplate configuration](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/configuration/) — Storefront configuration, `headers.xlsx`, and paths for preview versus production. - [Introduction to drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/introduction/) and [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/) — Initializer flow, slots, and styling when you are ready to change Commerce UI. - [Launch checklist](https://experienceleague.adobe.com/developer/commerce/storefront/setup/launch/) — Security, performance, and go-live items when you move past learning. --- # Storefront developer guide ## What is Adobe Commerce Storefront? Adobe Commerce Storefront lets you build a fast online store on Adobe Commerce, with full control over your brand's layout and shopping experience. Edge Delivery Services (Adobe's hosting and delivery infrastructure that turns authored documents into fast HTML pages served from servers close to the shopper. You push code to GitHub; Edge Delivery Services builds and publishes automatically.), Adobe's global delivery network, turns merchant-authored documents into HTML and serves pages from servers near shoppers around the world. Merchants keep editing in a document-style workflow while you own the storefront code, so content changes and code changes stay out of each other's way. Your job is to connect Commerce data to the page. You start from the Adobe Commerce boilerplate (Pre-configured storefront with the components and services you need to get started.), a Git repository that already contains a working storefront. The boilerplate already includes drop-in components (NPM packages that provide core Commerce storefront features such as cart, checkout, product details, and account flows.) for Commerce experiences such as cart, checkout, product details, search, and accounts. You configure and customize them to match your storefront requirements. Drop-ins call the Adobe Commerce GraphQL (A query language that drop-in components use to request and update data from Adobe Commerce APIs. Catalog Service, Live Search, and the core Commerce API all expose GraphQL endpoints.) API from the shopper's browser, so cart and catalog data stay current without any server-side code from you. You customize how drop-ins look and behave instead of writing every Commerce screen from scratch. With accounts and a backend in place, you can have a working storefront preview in less than a day. > **About drop-in licensing** Drop-in components are included with an Adobe Commerce as a Cloud Service or Adobe Commerce Optimizer license. If you use Adobe Commerce on Cloud, you need a separate Adobe Commerce Optimizer license to use drop-ins. Read [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/) to match your backend type to prerequisites, and [Licensing requirements](https://experienceleague.adobe.com/developer/commerce/storefront/licensing/) to confirm what your license covers. ## New here? Start here You can start with [Create a storefront](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/create-storefront/) if your team already has accounts and a chosen backend. If not, the numbered path below helps you sort out Commerce access, choose the right backend, and confirm the services that drop-ins need. 1. [Before you start](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/before-you-start/) — Gather your tools, accounts, and Commerce access before you open Site Creator (App in Document Author (DA.live) that creates and initializes a storefront by setting up content, optional code, theme choice, and storefront configuration values.). 1. [Storefront Architecture](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/) — Five short topics: how pages load, what blocks and drop-ins are, how drop-ins coordinate, and how Commerce services connect. Get a clear picture of how the pieces fit together before you start coding. 1. [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/) — Identify your Commerce backend (Adobe Commerce on Cloud, Adobe Commerce as a Cloud Service, or Adobe Commerce Optimizer) and confirm what you need before you create a site. 1. [Storefront Compatibility Package](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/storefront-compatibility/) — Confirm the compatibility package is on your Commerce backend, or install it on Commerce on Cloud or on-premises before drop-ins can use your APIs. 1. [Create a storefront](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/create-storefront/) — Provision your repo and preview URL with Site Creator in DA.live (The tooling for creating storefront pages as documents in Google Docs, SharePoint, or DA.live without writing code. Tables in documents define the blocks that appear on each page.). This is where your first working preview comes from. 1. [Boilerplate getting started](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/getting-started/) — Run the Commerce boilerplate on your machine. After it finishes, you'll have a local storefront with Commerce blocks already connected. 1. [Boilerplate configuration](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/configuration/) — Set up your storefront configuration, headers, and paths to match your production environment. 1. [Introduction to drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/introduction/) and [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/) — Learn what drop-in components are, how the initializer import pattern works, and what slots, styling, and labels do. Read them before you customize any Commerce UI. 1. [Storefront configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) — Connect the storefront to Commerce endpoints and services. 1. [Get to know SEO](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/) — Learn how Edge Delivery Services SEO and generative engine optimization (GEO) apply to Commerce storefront indexing, metadata, and launch checks. 1. [Launch Checklist](https://experienceleague.adobe.com/developer/commerce/storefront/setup/launch/) — Finish these steps before you treat the site as production-ready. If you already know your task, use the sidebar for the full catalog. Start with Storefront developer guide for the full numbered path, then follow the onboarding steps through Create a storefront. If you are building a B2B storefront, this path still applies. B2B-specific drop-ins and configuration are in the B2B Drop-Ins section in the sidebar after you complete these steps. > **When something goes wrong** Start with [Storefront Architecture](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/). It shows whether the problem is in content authoring, Edge Delivery Services, or Commerce, so you know where to look instead of searching only under `blocks/`. ## Reference and tutorials When you need technical details — API methods, props (configuration options), slots (customization points), events, or version notes — for any of Adobe's shipped drop-ins, open the matching drop-in page under B2C Drop-Ins or B2B Drop-Ins in the sidebar. Most storefront projects extend those shipped drop-ins (styling, slots, labels, and behavior). That work lives in [the Commerce Drop-Ins overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/introduction/) and each drop-in's reference pages. The Drop-In SDK section is for a smaller set of cases: use it when you write new drop-in packages from scratch or need to work directly with the framework that initializes and manages drop-ins — not for typical customization of Adobe's packages. After you complete the numbered path, see the Tutorials section for step-by-step pages covering federated search, Luma storefront migration (Luma Bridge), cart, checkout, and user account flows. --- # Performance best practices If your storefront feels slow or your mobile scores look low, start here. You'll walk through `delayed.js`, CSS load order, and `head.html` next to https://www.aem.live/developer/keeping-it-100, the Edge Delivery guide many teams keep open while they tune loading. After you change how things load, run [Lighthouse audits](#run-lighthouse-audits) for a fresh readout. Before launch, review [Performance and monitoring](https://experienceleague.adobe.com/developer/commerce/storefront/setup/launch/#performance-and-monitoring) on the launch checklist. When you already have the project from [Create a storefront](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/create-storefront/), leave the repo open in your editor so you can open files such as `scripts/delayed.js` and `styles/styles.css` when this page points to them. The Commerce boilerplate uses the same Edge Delivery Services (Adobe's hosting and delivery infrastructure that turns authored documents into fast HTML pages served from servers close to the shopper. You push code to GitHub; Edge Delivery Services builds and publishes automatically.) pattern as other Edge storefronts: eager, lazy, and delayed loading, fonts, and LCP (Largest Contentful Paint, how fast the main content shows). - [Start with Keeping it 100 on AEM.live](https://www.aem.live/developer/keeping-it-100) — The canonical Keeping it 100 page covers eager, lazy, and delayed loading, Largest Contentful Paint (LCP), fonts, and mistakes such as overusing preload and early hints. Read it before you edit head.html or split CSS. ## Run Lighthouse audits PageSpeed Insights runs Lighthouse on Google's hardware instead of on your computer, so you can compare one test run to the next more fairly than if you only tested locally. It reports Web Vitals, Google's standard scores for how fast and stable the page feels. The fix list follows Google's performance guidance. When you change load order or assets, compare those suggestions with https://www.aem.live/developer/keeping-it-100 so resource hints, fonts, and load phases still match what Edge Delivery Services expects. - [PageSpeed Insights](https://pagespeed.web.dev/) — Enter your storefront production URL (typically *.aem.live) for accurate results. That hostname sits on CDNs close to your customers, on the edge. ### Step-by-step The following steps run a PageSpeed Insights audit on a URL you choose. 1. Go to the https://pagespeed.web.dev/. 1. Paste your storefront URL into the PageSpeed Insights input field. 1. If you use the default `main` branch pattern on Edge Delivery, typical preview and production hosts look like this (replace `{repo}` and `{owner}` with your GitHub repository name and owner): - Preview: `https://main--{repo}--{owner}.aem.page/` - Production: `https://main--{repo}--{owner}.aem.live/` 1. Click the **Analyze** button to run the audit. 1. You'll see full Web Vitals reports for mobile and desktop. Scores are often high on tuned Edge Delivery storefronts, but they vary with page content, third-party scripts, and test conditions, so treat the report as a snapshot, not a guarantee of 100 on every run. ## Performance in the Commerce boilerplate ### Delayed phase https://www.aem.live/developer/keeping-it-100 describes a delayed phase for third-party tags, marketing tooling, extended analytics, consent, chat, and similar scripts. Load them through `delayed.js` so they do not compete with LCP or the rest of the experience. In the Commerce boilerplate, implement that path in https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/scripts/delayed.js. Keep it off the eager path. For Adobe Experience Platform and related patterns, see [Adobe Experience Platform analytics](https://experienceleague.adobe.com/developer/commerce/storefront/setup/analytics/adobe-experience-platform/). ### Eager vs lazy styles Keeping it 100 treats styles in two phases so LCP stays predictable. #### Eager phase The eager phase covers the markup, CSS, and JavaScript that must load first so the main content can appear quickly and your LCP score can settle. Stay within the network and payload limits the Keeping it 100 page describes. > **Preload and early hints** Too many preload tags, early hints, and preconnect entries can steal bandwidth from what the visitor needs first and hurt mobile LCP. Keeping it 100 explains how to stay inside safe limits instead of adding every hint you can think of. #### Lazy phase The lazy phase is for styles (and related assets) that can load after the main content and LCP finish so they do not slow the first screen. In the Commerce boilerplate, eager, site-wide tokens and styles usually start in https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/styles/styles.css. For deferred styles (for example, `lazy-styles.css`), use the folder-and-import patterns in [Branding and styles](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/branding/) instead of merging them into the eager file. After your split matches that guidance, align `head.html` and load order with Keeping it 100 so you are not maintaining a second rule set. Keeping it 100 also explains why preloading every web font often backfires. Keep the boilerplate font fallbacks unless you have a clear measurement that says to change them. > **Related storefront docs** For tips on catalog pages (images, APIs, loading order), see the [FAQ](https://experienceleague.adobe.com/developer/commerce/storefront/troubleshooting/faq/#how-can-i-improve-the-performance-of-my-catalog-pages). --- # Adobe Experience Platform ## Overview Adobe Experience Platform (AEP) is a comprehensive suite of services that enables you to collect, unify, and analyze customer data from multiple touchpoints. By integrating your Adobe Commerce storefront with AEP, you can gain deeper insights into customer behavior and create more personalized experiences. ![Adobe Experience Platform architecture.](https://experienceleague.adobe.com/developer/commerce/storefront/setup/analytics/aep-architecture-1024.png) *Adobe Experience Platform architecture showing data flow from various sources to unified customer profiles.* This integration allows your storefront to send commerce events (product views, purchases, cart actions) directly to the Experience Platform Edge Network, where they can be processed, stored, and used for real-time personalization and analytics. For more information about Adobe Experience Platform capabilities, see the https://experienceleague.adobe.com/docs/experience-platform/landing/home.html. ## Prerequisites Before configuring your integration with Adobe Experience Platform, ensure you have the following: ### Required Identifiers * **IMS Organization ID**: Your Adobe organization identifier (format: `1234567890ABCDEF7F000101@AdobeOrg`) * **Datastream ID**: A configured datastream for routing data (format: `12345678-1234-1234-1234-123456789012`) ### How to Find Your Identifiers **IMS Organization ID:** To locate your IMS Organization ID, refer to the https://experienceleague.adobe.com/docs/core-services/interface/administration/organizations.html. You can typically find this in: - Adobe Admin Console - Developer Console - Any Adobe Experience Cloud application under Account Settings **Datastream ID:** Your datastream must be configured to route data to Adobe Experience Platform. For detailed instructions on creating and configuring a datastream, see the https://experienceleague.adobe.com/docs/experience-platform/datastreams/overview.html. ## Configuration To enable data flow from your storefront to the Experience Platform Edge Network, you need to add your AEP credentials to your storefront configuration. ### Method 1: Configuration File (Recommended) Add your AEP credentials to the `analytics` section of your /setup/configuration/commerce-configuration/: ```json title="config.json" { "public": { "default": { "analytics": { "aep-ims-org-id": "1234567890ABCDEF7F000101@AdobeOrg", "aep-datastream-id": "12345678-1234-1234-1234-123456789012", "base-currency-code": "USD", "environment": "Testing", ... } } } } ``` When both `aep-ims-org-id` and `aep-datastream-id` are configured, the storefront automatically: - Enables event forwarding to Adobe Experience Platform - Configures the AEP context with your credentials - Begins sending commerce events to the Experience Platform Edge Network ### Method 2: Direct Script Configuration (Alternative) Alternatively, you can configure AEP directly in your `scripts/delayed.js` file: ```js window.adobeDataLayer.push( { aepContext: { imsOrgId: '1234567890ABCDEF7F000101@AdobeOrg', datastreamId: '12345678-1234-1234-1234-123456789012' } }, { eventForwardingContext: { aep: true } } ); ``` > **Recommended approach** Using the configuration file (Method 1) is recommended because it: - Centralizes all configuration in one place - Allows environment-specific settings without code changes - Simplifies deployment and configuration management ### Configuration Parameters | Parameter | Description | Required | Example | |-----------|-------------|----------|---------| | `aep-ims-org-id` | Your Adobe IMS Organization ID | Yes | `"1234567890ABCDEF7F000101@AdobeOrg"` | | `aep-datastream-id` | Your configured datastream ID for routing data to AEP | Yes | `"12345678-1234-1234-1234-123456789012"` | ### What This Configuration Does - **`aep-ims-org-id`**: Identifies your Adobe organization for routing events to the correct Experience Platform environment - **`aep-datastream-id`**: Specifies the datastream configuration that determines how events are processed and where they are sent - **Automatic event forwarding**: When both values are present, the storefront automatically enables `eventForwardingContext.aep` and configures the `aepContext` ## Storefront events Once configured, your storefront will automatically send the following types of events to Adobe Experience Platform: - **Shopping events**: Cart updates and views (`addToCart`, `removeFromCart`, `shoppingCartView`), page views (`pageView`, `productPageView`), checkout (`startCheckout`, `completeCheckout`) and more. - **Customer profile events**: Customer login (`signIn`), customer logout (`signOut`), create account (`createAccount`), edit account (`editAccount`). - **Search events**: Search query (`searchRequestSent`) and search results (`searchResponseReceived`). > **Search events** If `LiveSearch` is not installed and configured, these search events are not sent. For a complete list of storefront events, see the https://developer.adobe.com/commerce/services/shared-services/storefront-events/. > **Debugging events** To debug events in your storefront, use the AEP Debugger Events view. See the https://experienceleague.adobe.com/docs/experience-platform/debugger/home.html for instructions. These events are processed in real-time and can be used for: - Customer journey analysis - Real-time personalization - Audience segmentation - Attribution modeling ## Validation ### Testing Your Integration ### 1. Check browser console After implementing the configuration, open your browser's developer tools and verify that: - No JavaScript errors appear - Adobe Data Layer events are being fired - Network requests to Adobe Experience Platform Edge Network are successful ### 2. Monitor data ingestion Use Adobe Experience Platform's monitoring tools to confirm data is being received: - Navigate to your AEP workspace - Check the **Monitoring** section for incoming data - Verify events appear in your configured datasets ### Validate event structure Ensure events contain the expected commerce data fields and customer identifiers. ### Troubleshooting If data is not flowing as expected: - **Verify credentials**: Double-check your IMS Organization ID and Datastream ID - **Check datastream configuration**: Ensure your datastream is properly configured to route to Adobe Experience Platform - **Review browser network tab**: Look for failed requests to Adobe Experience Platform endpoints - **Validate Adobe Data Layer**: Confirm the Adobe Data Layer is properly initialized before your AEP configuration For detailed validation procedures, refer to the https://experienceleague.adobe.com/docs/platform-learn/getting-started-for-data-architects-and-data-engineers/ingest-batch-data.html. ## Next Steps After successful integration: 1. **Configure schemas**: Set up XDM schemas in Adobe Experience Platform to structure your commerce data 1. **Create audiences**: Build customer segments based on commerce behavior 1. **Set up Real-Time CDP**: Use collected data for personalization and marketing activation 1. **Monitor performance**: Regularly review data quality and ingestion metrics For a complete implementation example, see the https://experienceleague.adobe.com/docs/core-services/interface/administration/organizations.html and https://github.com/hlxsites/aem-boilerplate-commerce. --- # Analytics instrumentation ## Overview Analytics instrumentation is the process of wiring your storefront so user interaction data reaches Adobe Commerce services (for example, Live Search and Product Recommendations) in the shape those services expect. This topic covers the Adobe Client Data Layer (ACDL), `config.json` analytics settings, and validation—not Adobe Analytics reporting alone. ### Why event collection matters User interaction events collected through your storefront implementation enable: - **Adobe Sensei features**: Intelligent merchandising and search result optimization in Live Search - **Product recommendations**: Personalized product suggestions based on user behavior - **Performance analytics**: Detailed dashboards showing search performance, conversion rates, and user engagement - **Business intelligence**: Data-driven insights for inventory management and marketing strategies > **Required for Core Features** Live Search and Product Recommendations events are not sent to AEP. For these features to function correctly, you must collect and send user interaction events to Adobe Commerce. Without proper instrumentation, these features will not work as expected. ## Adobe Client Data Layer (ACDL) The https://github.com/adobe/adobe-client-data-layer is a standardized JavaScript framework that simplifies data collection on your storefront. It provides a unified approach to capturing, storing, and transmitting user interaction data. ### Key Capabilities The ACDL enables your storefront to: - **Collect interaction data**: Track user behaviors like product views, searches, cart actions, and purchases - **Standardize data format**: Ensure consistent data structure across all events - **Manage event timing**: Control when and how data is sent to analytics services - **Support multiple integrations**: Work seamlessly with Adobe Experience Platform, Analytics, and other tools ### Core API Functions | Function | Description | Use Case | |----------|-------------|----------| | `push()` | Add data or trigger events | Send product view, cart addition events | | `getState()` | Retrieve current data layer state | Access user session or cart information | | `addEventListener()` | Register event listeners | React to specific user actions | | `getHistory()` | View event history | Debug or audit data collection | > **Built-in Support** The Adobe Commerce boilerplate includes ACDL by default, so you don't need to install it separately. Drop-in components automatically send events to the data layer. ## Configuration ### Store Configuration To enable proper event collection, you need to configure your store's analytics settings. This configuration tells the instrumentation system about your store's identity and structure. The specific values depend on your Commerce environment type. Refer to the https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/ for complete details on how to configure your store. ### Required Configuration Parameters The analytics configuration structure varies based on your Commerce backend type: ### Adobe Commerce (PaaS) / ACCS For Adobe Commerce PaaS and Adobe Commerce as a Cloud Service environments, use standard Commerce store and website identifiers from your Commerce environment. You can obtain these values using a `storeConfig` query. ```json title="config.json" { "analytics": { "aep-ims-org-id": "{{IMS_ORG_ID}}", "aep-datastream-id": "{{DATASTREAM_ID}}", "base-currency-code": "{{CURRENCY_CODE}}", "environment": "{{ENVIRONMENT_TYPE}}", "environment-id": "{{ENVIRONMENT_ID}}", "store-code": "{{STORE_CODE}}", "store-id": {{STORE_ID}}, "store-name": "{{STORE_NAME}}", "store-url": "{{STORE_URL}}", "store-view-code": "{{STORE_VIEW_CODE}}", "store-view-id": {{STORE_VIEW_ID}}, "store-view-name": "{{STORE_VIEW_NAME}}", "website-code": "{{WEBSITE_CODE}}", "website-id": {{WEBSITE_ID}}, "website-name": "{{WEBSITE_NAME}}" } } ``` **Configuration Properties:** | Parameter | Description | Example | Required | |-----------|-------------|---------|----------| | `aep-ims-org-id` | Adobe IMS Organization ID for Experience Platform integration | `"1234567890ABCDEF7F000101@AdobeOrg"` | No (required for AEP) | | `aep-datastream-id` | Datastream ID for routing data to Adobe Experience Platform | `"12345678-1234-1234-1234-123456789012"` | No (required for AEP) | | `base-currency-code` | The base currency code for the store | `"USD"`, `"EUR"` | Yes | | `environment` | Environment type | `"Testing"`, `"Production"` | Yes | | `environment-id` | Unique identifier for the Commerce environment | `"f38a0de0-764b-41fa-bd2c-5bc2f3c7b39a"` | Yes | | `store-code` | Code identifier for the store from your Commerce environment | `"main_website_store"` | Yes | | `store-id` | Numeric ID for the store | `1`, `2` | Yes | | `store-name` | Display name for the store | `"Main Website Store"` | Yes | | `store-url` | Base URL for the store | `"https://example.com"` | Yes | | `store-view-code` | Code identifier for the store view | `"default"` | Yes | | `store-view-id` | Numeric ID for the store view | `1`, `2` | Yes | | `store-view-name` | Display name for the store view | `"Default Store View"` | Yes | | `website-code` | Code identifier for the website from your Commerce environment | `"base"` | Yes | | `website-id` | Numeric ID for the website | `1`, `2` | Yes | | `website-name` | Display name for the website | `"Main Website"` | Yes | > **Adobe Experience Platform integration** To enable automatic event forwarding to Adobe Experience Platform, include both `aep-ims-org-id` and `aep-datastream-id` in your analytics configuration. When both values are present, events will automatically be sent to AEP. See the [Adobe Experience Platform integration guide](https://experienceleague.adobe.com/developer/commerce/storefront/setup/analytics/adobe-experience-platform/) for detailed setup instructions. ### Adobe Commerce Optimizer (ACO) For Adobe Commerce Optimizer environments, use a simplified analytics configuration structure: ```json title="config.json" { "analytics": { "aep-ims-org-id": "{{IMS_ORG_ID}}", "aep-datastream-id": "{{DATASTREAM_ID}}", "base-currency-code": "{{CURRENCY_CODE}}", "environment": "{{ENVIRONMENT_TYPE}}", "environment-id": "{{TENANT_ID}}", "locale": "{{LOCALE}}", "store-url": "{{STORE_URL}}", "store-view-currency-code": "{{CURRENCY_CODE}}", "storefront-template": "{{TEMPLATE_TYPE}}", "view-id": "{{CATALOG_VIEW_ID}}" } } ``` **Configuration Properties:** | Parameter | Description | Example | Required | |-----------|-------------|---------|----------| | `aep-ims-org-id` | Adobe IMS Organization ID for Experience Platform integration | `"1234567890ABCDEF7F000101@AdobeOrg"` | No (required for AEP) | | `aep-datastream-id` | Datastream ID for routing data to Adobe Experience Platform | `"12345678-1234-1234-1234-123456789012"` | No (required for AEP) | | `base-currency-code` | The base currency code for the store | `"USD"`, `"EUR"` | Yes | | `environment` | Environment type | `"Testing"`, `"Production"` | Yes | | `environment-id` | The tenant ID for the Adobe Commerce Optimizer instance | `"8idEEDDiVwjCEJAyB5kjfi"` | Yes | | `locale` | Catalog source locale (language or geography) | `"en-US"` | Yes | | `store-url` | Base URL for the store | `"https://example.com"` | Yes | | `store-view-currency-code` | Currency code for the store view | `"USD"`, `"EUR"` | Yes | | `storefront-template` | Storefront template type | `"Other"` | No | | `view-id` | The unique ID assigned to the catalog view | `"0d3eebf7-b5fb-4904-9ccf-f35fcc61862b"` | Yes | > **environment** The `environment` parameter is used to determine the type of environment your store is running in. This is important because it affects how data is collected and processed. In your storefront configuration, set the value to the JSON string `"Testing"` while you develop and `"Production"` when you deploy. ### Data Services Configuration For the instrumentation to work with Adobe Commerce's data services, you'll need additional configuration parameters. The easiest way to obtain these is through the `magento/module-data-services-graphql` module, which exposes the necessary GraphQL endpoints. #### Required for Data Services - **Catalog Service credentials**: For product data synchronization - **SaaS environment ID**: Links your storefront to Adobe Commerce SaaS services - **API keys**: Authenticate with Adobe Commerce backend services ## Event Collection and Validation ### Automatic Event Collection The Commerce boilerplate includes the https://github.com/adobe/commerce-events/tree/main/packages/storefront-events-collector, which automatically: 1. **Listens for ACDL events**: Monitors the data layer for new events 1. **Validates event structure**: Ensures events conform to required schemas 1. **Batches and sends data**: Efficiently transmits events to Adobe Commerce 1. **Handles errors**: Manages network issues and retry logic ### Event Types Collected Your instrumentation will automatically track: - **Shopping events**: Cart updates and views (`addToCart`, `removeFromCart`, `shoppingCartView`), page views (`pageView`, `productPageView`), checkout (`startCheckout`, `completeCheckout`) and more. - **Customer profile events**: Customer login (`signIn`), customer logout (`signOut`), create account (`createAccount`), edit account (`editAccount`). - **Search events**: Search query (`searchRequestSent`) and search results (`searchResponseReceived`). > **Search events** If `LiveSearch` is not installed and configured, these search events are not sent. > **Debugging events** To debug events in your storefront, use the AEP Debugger Events view. See the https://experienceleague.adobe.com/docs/experience-platform/debugger/home.html for instructions. ### Event Schema Compliance All events must comply with the schema defined by the https://github.com/adobe/commerce-events/tree/main/packages/storefront-events-sdk. This ensures compatibility with Adobe Commerce services and analytics tools. ## Validation and Testing The following sections describe how to validate and test your event implementation. ### Automated Validation You can validate your event implementation using the https://github.com/adobe/adobe-client-data-layer/pull/156. This tool checks: - **Event structure**: Verifies required fields are present - **Data types**: Ensures values match expected formats - **Schema compliance**: Confirms events follow Storefront Event SDK specifications > **Performance Recommendation** For optimal performance, Adobe recommends writing events directly to ACDL rather than using the Storefront Events SDK wrapper. Drop-in components handle this automatically, but custom implementations should follow this practice. ### Manual Testing Steps 1. **Open browser developer tools** and navigate to the Console tab 1. **Check for ACDL**: Verify `window.adobeDataLayer` exists and contains events 1. **Monitor network requests**: Look for successful data transmission to Adobe services 1. **Validate event data**: Inspect event payloads for completeness and accuracy 1. **Confirm that event data is collected**: To confirm that data is being collected from your Commerce store, use the Adobe Experience Platform debugger to examine your Commerce site. > **Debugging events** The AEP Debugger provides an Events view that you can use to examine the events being sent from your Commerce site. See the https://experienceleague.adobe.com/docs/experience-platform/debugger/home.html for instructions. ### Common Validation Issues - **Missing configuration**: Ensure all required analytics parameters are set - **Incorrect store IDs**: Verify store and website IDs match your Adobe Commerce setup - **Network connectivity**: Check that your storefront can reach Adobe Commerce endpoints - **Event timing**: Confirm events fire at the correct moments in the user journey ## Troubleshooting Configuration Issues **Problem**: Events not being sent **Solution**: 1. Verify your `config.json` contains all required analytics parameters 1. Check that store IDs match your Adobe Commerce backend configuration 1. Ensure the Storefront Events Collector is loading properly **Problem**: Invalid event data **Solution**: 1. Use the ACDL validator to check event structure 1. Verify custom events follow the Storefront Event SDK schema 1. Check for JavaScript errors that might corrupt event data ## Troubleshooting Integration Issues **Problem**: Live Search not receiving data **Solution**: 1. Confirm your SaaS environment ID is correctly configured 2. Verify API credentials are valid and have necessary permissions 3. Check that product catalog is properly synchronized For additional troubleshooting, refer to the https://experienceleague.adobe.com/en/docs/commerce/product-recommendations/admin/workspace#data-collection. ## Best Practices ### Implementation Guidelines - **Test thoroughly**: Validate events in development before deploying to production - **Monitor regularly**: Set up alerts for data collection failures - **Follow schemas**: Always comply with Storefront Event SDK specifications - **Optimize performance**: Batch events when possible to reduce network overhead ### Data Quality - **Validate user inputs**: Sanitize data before adding to events - **Handle edge cases**: Account for scenarios like network failures or missing data - **Maintain consistency**: Use standardized naming and formatting across all events - **Respect privacy**: Ensure compliance with data protection regulations --- # AEM Assets integration The AEM Assets integration displays product images managed in AEM Assets instead of traditional Commerce-hosted images. The integration delivers enhanced image management capabilities: advanced optimization, cropping, and delivery through Adobe's Content Delivery Network (CDN). Learn more in the https://experienceleague.adobe.com/en/docs/commerce/aem-assets-integration/overview. ## Boilerplate update required Only one update is required: set `"commerce-assets-enabled": true` in your [Storefront configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/). ```json title="config.json" ins={'+':4} { "public": { "default": { "commerce-assets-enabled": true } } } ``` The Commerce drop-ins automatically detect the `commerce-assets-enabled` configuration and adjust image handling accordingly. See the https://www.aemshop.net/config.json. ## Expected behaviors The following table describes what happens in different configuration scenarios. The **Image source** refers to where you store your images: either "Commerce" for traditional Commerce-hosted images or "AEM Assets" for images managed in AEM Assets. The **`commerce-assets-enabled setting`** column indicates whether this configuration is set to `true` or `false`. ```text [ ['Image source', 'commerce-assets-enabled setting', 'Expected behavior'], ['Commerce-hosted images', 'true', 'Images display correctly. The AEM Assets integration code passes through Commerce-hosted images without modification.'], ['AEM Assets images', 'true', 'Images display correctly with proper AEM Assets CDN optimization parameters applied. This is the intended configuration for AEM Assets integration.'], ['Commerce-hosted images', 'false', 'Images display correctly using standard Commerce image handling without AEM Assets optimization.'], ['AEM Assets images', 'false', 'Images may not display correctly. AEM Assets images require specific optimization parameters that may conflict with standard Commerce image handling, potentially resulting in 400 errors or broken images.'], ] ``` ## How it works in the boilerplate The Commerce drop-ins automatically detect the `commerce-assets-enabled` configuration and adjust image handling accordingly. Here's how the boilerplate integrates this configuration: ### Import the AEM Assets utility The Commerce blocks in the boilerplate import the `tryRenderAemAssetsImage` helper from the drop-ins tools package. ```javascript showLineNumbers=false ``` ### Render images via drop-in slots The Commerce blocks use the `tryRenderAemAssetsImage` function inside its drop-in image slots, as shown below. ```javascript showLineNumbers=false {"Container:":4-5} {"Container Image Slot:":6-8} {"AEM Assets integration:":10-17} export default async function decorate(block) { await dropinRenderer.render(DropinComponent, { slots: { DropinImageSlot: (ctx) => { const { data, defaultImageProps } = ctx; tryRenderAemAssetsImage(ctx, { imageProps: defaultImageProps, params: { width: defaultImageProps.width, height: defaultImageProps.height, }, }); }, }, })(block); } ``` ## Real-world examples from the boilerplate Based on the Commerce blocks in the boilerplate, AEM Assets integration uses the `tryRenderAemAssetsImage` function from `@dropins/tools/lib/aem/assets.js` as follows. ### Product List SearchResults container ```javascript showLineNumbers=false {"Container:":2-3} {"Container Image Slot:":4-6} {"AEM Assets integration:":11-20} provider.render(SearchResults, { slots: { ProductImage: (ctx) => { const { product, defaultImageProps } = ctx; const anchorWrapper = document.createElement('a'); anchorWrapper.href = rootLink(`/products/${product.urlKey}/${product.sku}`); tryRenderAemAssetsImage(ctx, { alias: product.sku, imageProps: defaultImageProps, wrapper: anchorWrapper, params: { width: defaultImageProps.width, height: defaultImageProps.height, }, }); }, }, }); ``` ### Checkout OrderProductList container ```javascript showLineNumbers=false {"Container:":2-3} {"Container Image Slot:":4-6} {"AEM Assets integration:":8-16} OrderProvider.render(OrderProductList, { slots: { CartSummaryItemImage: (ctx) => { const { data, defaultImageProps } = ctx; tryRenderAemAssetsImage(ctx, { alias: data.product.sku, imageProps: defaultImageProps, params: { width: defaultImageProps.width, height: defaultImageProps.height, }, }); }, ... }, })($orderProductList); ``` ## Using drop-ins outside the boilerplate (optional) If you use the boilerplate, AEM Assets works out of the box. Without the boilerplate, you need to implement the minimal config and slot usage below. See the [Commerce drop-ins overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/introduction/) for how slots and initializers work. To enable AEM Assets with standalone drop-ins, you'll need to implement the configuration system that drop-ins expect. See [Storefront configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) for full configuration details. ### Minimal configuration Set `commerce-assets-enabled: true` in a `public.default` config (JSON or config service). Align endpoints/headers to your environment as needed. > **Reference implementation** See the https://github.com/hlxsites/aem-boilerplate-commerce for complete examples. ### Minimal integration steps 1. Install `@dropins/tools`. 1. Add the `assets.js` file from the boilerplate to your project. 1. Import `tryRenderAemAssetsImage` into your drop-in components. 1. In each image slot, call `tryRenderAemAssetsImage(ctx, { alias: , imageProps, params: { width, height } })`. Fallback to Commerce-hosted images is automatic from functions in `assets.js`—no extra code is required. 1. Use the product SKU (or your configured alias) for `alias` so the correct asset is matched in AEM. 1. When linking images, pass a `wrapper` element (for example, an anchor) in the options. 1. Set the `width`/`height` params to the slot’s intended render size (e.g., from `defaultImageProps`). 1. For more details, see [Commerce drop-ins overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/introduction/). **Minimal implementation results**: Uses AEM Assets when available; falls back to Commerce; applies correct CDN params automatically. > **AEM Assets API helpers** Outside the boilerplate, use these helpers from `@dropins/tools/lib/aem/assets.js` to detect enablement, generate optimized URLs, and render slots: - `isAemAssetsEnabled()` - `getDefaultAemAssetsOptimizationParams()` - `isAemAssetsUrl(url)` - `generateAemAssetsOptimizedUrl(assetUrl, alias, params)` - `tryGenerateAemAssetsOptimizedUrl(assetUrl, alias, params)` - `makeAemAssetsImageSlot(config)` - `tryRenderAemAssetsImage(ctx, config)` ## Troubleshooting ### Images not displaying If product images are not displaying correctly: 1. **Check your configuration:** Ensure `commerce-assets-enabled` is set to `true` if you're using AEM Assets. 2. **Verify image URLs:** AEM Assets images typically include specific URL patterns that indicate they're served from AEM. 3. **Check browser console:** Look for 400 errors that might indicate incompatible optimization parameters. 4. **Test with mixed sources:** Try with both AEM Assets and Commerce-hosted images to isolate the issue. 5. **Confirm slot integration:** If not using the boilerplate, ensure each drop-in image slot calls `tryRenderAemAssetsImage` with a valid `alias` (SKU). 6. **Validate size params:** Ensure `params.width`/`params.height` match the slot's intended render size (for example, from `defaultImageProps`). ### Configuration not taking effect 1. **Clear cache:** Ensure your configuration changes are not cached. 2. **Check config location:** Verify your https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/ is properly formatted. 3. **Validate JSON:** Use a JSON validator to ensure your configuration file is valid. --- # AEM Commerce Prerender The AEM Commerce Prerender solution lets your Edge Delivery Services (Adobe's hosting and delivery infrastructure that turns authored documents into fast HTML pages served from servers close to the shopper. You push code to GitHub; Edge Delivery Services builds and publishes automatically.) Commerce Storefront serve complete product page HTML to search engines and AI crawlers _before_ JavaScript runs. This can improve SEO rankings and make your product catalog readable by large language models (LLMs) and other AI systems. ## How it works 1. Deploy the App Builder (Adobe's serverless platform for building and deploying cloud-native apps. The AEM Commerce Prerender app runs on App Builder — it polls your catalog on a schedule, generates product page HTML, and publishes it to Edge Delivery Services.) app first, because your storefront integration depends on the HTML it generates. The app runs in Adobe's cloud on a schedule, polling your product catalog and generating HTML for each product. 1. The app publishes that HTML to Edge Delivery Services as overlay (A secondary BYOM content source layered over the primary one; EDS serves overlay HTML for a URL when available, falling back to the primary source otherwise.) content, where it becomes part of the initial page HTML served to visitors. 1. Your `product-details` block adds interactive features to the prerendered HTML using the product detail page (PDP) drop-in (NPM packages that provide core Commerce storefront features such as cart, checkout, product details, and account flows.). > **Prerequisite: Bring Your Own Markup** The prerender solution uses Edge Delivery Services' https://www.aem.live/developer/byom API to publish HTML content. The app generates one HTML file per product and publishes it as overlay content, so Edge Delivery Services serves complete product HTML at your product page URLs. ## Why use prerender? By default, Adobe Commerce on Edge Delivery Services renders product information with client-side JavaScript, which limits discoverability for search engines and AI systems that don't run JavaScript. Edge Delivery Services has no built-in server-side rendering, so the prerender solution is the closest equivalent. Prerendering generates complete HTML — including structured data — and publishes it via the Bring Your Own Markup (BYOM) API before any page is requested. Structured data is machine-readable product information embedded in the HTML, so search engines can read product details without running JavaScript. - **Enhanced SEO**: Complete HTML for search engines improves indexing and rankings. - **AI-readable catalog**: AI systems can read structured markup to understand and recommend your products. - **Content appears immediately**: Prerendered product content is visible while interactive drop-ins finish loading. - **Faster first load**: Pages start displaying content before JavaScript finishes running. - **Automated updates**: Monitors the product catalog and updates markup when products change. ## Implementation roadmap To implement the prerender solution, you'll work in two repositories. Follow these steps in order: ```mermaid %%{init: {'theme':'base', 'themeVariables': {'fontSize':'16px'}}}%% flowchart LR Start([Start Implementation]):::start Step1["1. Check Prerequisites Verify access, tools, and storefront requirements"]:::step Step2["2. Understand the Architecture Learn how the two repositories work together"]:::step Step3["3. Install & Deploy App Builder App Repository: aem-commerce-prerender Clone, configure, and deploy to App Builder"]:::repo1 Step4["4. Integrate with Your Storefront Repository: Your Commerce Boilerplate Modify the product-details block to use prerendered markup"]:::repo2 Step5["5. Customize (Optional) Adjust templates, schemas, and queries"]:::optional End([✓ Implementation Complete]):::complete Start --> Step1 Step1 --> Step2 Step2 --> Step3 Step3 --> Step4 Step4 --> Step5 Step5 --> End classDef start fill:#e3f2fd,stroke:#1976d2,stroke-width:3px,color:#000 classDef step fill:#f5f5f5,stroke:#666,stroke-width:2px,color:#000 classDef repo1 fill:#fff3e0,stroke:#f57c00,stroke-width:3px,color:#000 classDef repo2 fill:#e8f5e9,stroke:#388e3c,stroke-width:3px,color:#000 classDef optional fill:#fce4ec,stroke:#c2185b,stroke-width:2px,color:#000,stroke-dasharray: 5 5 classDef complete fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px,color:#000 linkStyle default stroke-width:3px,stroke:#666 ``` *Complete implementation workflow showing the step-by-step process for setting up prerendered product pages in your storefront.* ## Prerequisites Before implementing the prerender solution, verify you have: ### Required access and permissions - **Adobe Developer Console access** with "Developer" role for your organization - **App Builder workspace** (Stage and Production workspaces recommended) - **AEM Admin API access** for your Edge Delivery Services project (see https://www.aem.live/docs/admin.html documentation) - **Adobe Commerce instance** with Catalog Service (Adobe's fast, read-only GraphQL API for product data. Drop-ins call it instead of core Commerce GraphQL for product pages, search results, and category listings — up to ten times faster.) configured and operational *(AEM Live tools note — see the full documentation site for the complete callout.)* ### Storefront requirements Your Edge Delivery Services Commerce Storefront must be: - Built on the https://github.com/hlxsites/aem-boilerplate-commerce - Configured with valid [storefront configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) connecting to your Adobe Commerce backend - Using product URLs with consistent format (typically `/products/{urlKey}/{sku}`) ### Technical requirements - **Node.js** (current LTS version) installed locally - **Adobe I/O CLI** (`@adobe/aio-cli`) installed globally - **Git** for cloning the prerender repository - Familiarity with JavaScript, Handlebars templates, and GraphQL ## Detailed workflow The three steps in "How it works" describe the high-level flow. The steps below show how the prerender system carries out that process on a schedule in the background: 1. The store admin manages products in Adobe Commerce. 2. The prerender system detects product changes. 3. The prerender system renders pages with semantic markup. 4. The prerender system publishes the pages via the AEM Admin API. 5. The prerender system updates the sitemap via the AEM Admin API. 6. Shoppers access the prerendered product details pages. The diagram below shows how data moves through each stage, from the store admin's product update to the shopper's product page. ```mermaid flowchart LR A["👨‍💼 Store Admin"]:::actor B["Product"]:::product subgraph subGraph0["Prerender system"] direction TB C["Product changed"]:::prerender D["Render page"]:::prerender C --> D end E["Store HTML in App Builder blob store"]:::action F["AEM Admin API configures overlay & updates sitemap"]:::action G["Product Details Page With embedded JSON-LD Metadata and Semantic Markup"]:::output H["🛒 Store Customer"]:::actor A --> B B --> subGraph0 subGraph0 --> E E -->|AEM Admin API| F F --> G G --> H classDef actor fill:none,stroke:none,color:#000000 classDef product fill:#e3f2fd,stroke:#1976d2,stroke-width:2px classDef prerender fill:#fff3e0,stroke:#f57c00,stroke-width:2px,stroke-dasharray: 5 5 classDef action fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px classDef output fill:#c8e6c9,stroke:#388e3c,stroke-width:2px ``` *High-level workflow showing how product data flows from Store Admin through the prerender system to deliver Product Details Pages to Store Customers.* ### Architecture components The solution consists of these components: - **App Builder Actions**: Handle product fetching, change detection, and markup generation. - **Scheduled Triggers**: Invoke actions on a configurable schedule to keep content synchronized. - **Catalog Service Integration**: Queries product data from your Adobe Commerce backend. - **AEM Admin API**: Publishes generated markup to your Edge Delivery Services project. - **Storage Layer**: Maintains product state and generated markup in App Builder storage. - **Management UI**: Provides a web interface for monitoring, configuration, and troubleshooting. The diagram below shows how these components connect and work together to generate and serve prerendered product pages. ```mermaid flowchart TB subgraph subGraph0["App Builder Workspace"] direction TB A["Product Change Detector (publishes products upon change)"]:::appBuilder B["Product List Scraper (fetch and store list)"]:::appBuilder C["PDP Renderer (generates/stores pages)"]:::appBuilder D[("Blob Store")]:::storage A --> D B --> D C --> D end E["AEM Admin API configures overlay link to App Builder blob store"]:::api F["Edge Delivery Services serves HTML from overlay at product URLs"]:::helix subgraph subGraph1["Product Page"] direction TB G["Embedded markup with product data and attributes, injected ahead of time"]:::embedded H["Description"]:::prerendered I["Product image carousel"]:::prerendered J["Price Stock qty"]:::dynamic K["More Customer-defined fields"]:::dynamic end subgraph subGraph2["Consumers"] direction TB L["SEO / AI systems"] M["AI Scrapers (read markup, no js)"] N["Googlebot (reads JSON-LD, Rendered page)"] O["Other crawlers (read markup, no js)"] end P["Catalog service (high frequency data)"]:::api Q["API for price retrieval"]:::api D -->|HTML stored in blob store| E E -->|Overlay configured| F F -->|Edge Delivery serves the final page| subGraph1 subGraph1 --> subGraph2 G --> H G --> I H --> subGraph2 I --> subGraph2 P -->|Provides data| J Q -->|Provides data| J P -->|Provides data| K classDef appBuilder fill:#e3f2fd,stroke:#2962FF,color:#2962FF classDef storage fill:#fff3e0,stroke:#f57c00 classDef helix fill:#e8f5e9,stroke:#388e3c classDef embedded fill:#fff9c4,stroke:#f57f17 classDef prerendered fill:#c8e6c9,stroke:#388e3c classDef dynamic fill:#ffcdd2,stroke:#c62828 classDef api fill:#fce4ec,stroke:#c2185b ``` *Complete architecture showing App Builder actions, content management, and how prerendered and dynamic content are delivered to consumers.* > **Catalog Size Considerations** For catalogs exceeding 50,000 products, see [Edge Delivery Services limits](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/platform-limits/#indexing-and-sitemap-size) for sitemap splitting strategies and scaling considerations. ## Product lifecycle management The prerender solution publishes everything Catalog Service returns, without filtering by product status, visibility, or stock level. A product gets prerendered when Catalog Service includes it in search results and it has a valid URL key. ### Draft product preview The prerender system does not have a draft mode. It renders every product that Catalog Service returns, regardless of whether that product is enabled, visible, or in stock. Whether a draft or inactive product appears in the prerendered site therefore depends entirely on your Commerce and Catalog Service configuration — not on any filtering in the prerender system. You can also preview the product template itself — the BYOM template document that provides the page structure for all product pages. Open it in Document Authoring (DA), Universal Editor (UE), or another supported editor, with a `defaultSku` configured to review the layout and content before any products go live. ### Scheduled publishing Products become live on your storefront only when they have been prerendered and published to Edge Delivery Services. The prerender system runs two coordinated polling actions: - **`fetch-all-products`** — runs every 60 minutes to discover new products in the catalog - **`check-product-changes`** — runs every 5 minutes to detect changes, render updated markup, and publish product pages To schedule when a product goes live, control when it becomes visible in Catalog Service. When the product appears in Catalog Service search results, the `fetch-all-products` action discovers it within 60 minutes and the `check-product-changes` action publishes it within 5 minutes of discovery. See the https://github.com/adobe-rnd/aem-commerce-prerender for details on configuring polling intervals in `app.config.yaml`. ### Offline product removal When a product is removed from Catalog Service — because it was disabled, deleted, or its visibility changed — the `mark-up-clean-up` action removes it from the published storefront. This action runs every 60 minutes and works by comparing the list of currently published product pages against current Catalog Service results: any product in the published index that is no longer returned by Catalog Service is unpublished and deleted. For faster, event-driven removal, you can build custom instrumentation that observes product changes in Commerce and calls the Edge Delivery Services unpublish API directly. The https://www.aem.live/docs/admin.html#tag/publish/operation/unpublishResource accepts a resource path and immediately removes it from your live site. ## Installation and configuration The https://github.com/adobe-rnd/aem-commerce-prerender has complete installation instructions. The steps below cover the key configuration values for your deployment. 1. **Clone the repository** and install dependencies: ```bash git clone https://github.com/adobe-rnd/aem-commerce-prerender.git cd aem-commerce-prerender npm install ``` 2. **Run the setup wizard** to generate your `.env` configuration: ```bash npm run setup ``` The wizard prompts for your App Builder workspace selection and Adobe I/O authentication. 3. **Configure environment variables** in the generated `.env` file with your storefront-specific values: | Variable | Description | Example | |----------|-------------|---------| | `ORG` | Your GitHub organization or username | `adobe` | | `SITE` | Your AEM site/repository name | `your-storefront` | | `CONTENT_URL` | Your AEM content URL (auto-populated by setup wizard) | `https://main--your-storefront--adobe.aem.live` | | `STORE_URL` | Your Commerce store URL (auto-populated by setup wizard) | `https://main--your-storefront--adobe.aem.live` | | `PRODUCTS_TEMPLATE` | URL for the product template page (auto-populated by setup wizard) | `https://main--your-storefront--adobe.aem.live/products/default` | | `PRODUCT_PAGE_URL_FORMAT` | URL pattern for product pages (auto-populated by setup wizard). Supports tokens: `{locale}`, `{urlKey}`, `{sku}` | `/{locale}/products/{urlKey}` | | `LOCALES` | Comma-separated list of locales (for example, `en-us,en-gb,fr-fr`) or empty for non-localized sites | `en-us,fr-fr` | | `AEM_ADMIN_API_AUTH_TOKEN` | Long-lived authentication token for AEM Admin API (valid for 1 year). The setup wizard exchanges your temporary token for this automatically. | `your-admin-token-here` | > **Token expiration** `AEM_ADMIN_API_AUTH_TOKEN` expires after one year. When it expires, product markup stops updating. Set a calendar reminder to rotate the token before expiration and redeploy with `npm run deploy`. See the Troubleshooting section for how to generate a new token. > **Commerce configuration** The prerender app reads Commerce configuration (endpoints, headers, and store context) from your storefront configuration, not from environment variables. Ensure your storefront configuration includes Commerce endpoints and headers as documented in the /setup/configuration/commerce-configuration/ documentation. For Adobe Commerce Optimizer, configure the appropriate headers (`AC-View-ID`, `AC-Source-Locale`, `AC-Price-Book-ID`) in your storefront configuration. 4. **Obtain required credentials**: - **AEM Admin API token**: To obtain your token: 1. Log in to the https://admin.hlx.page/login (select the link from your preferred Identity Provider, where links suffixed by "_sa" let you pick a specific account instead of the one currently logged in) 2. Once redirected to the JSON response, open your browser's Developer Tools (F12) 3. Go to the Application tab (Chrome) or Storage tab (Firefox) 4. Under Cookies, find and copy the value of the `auth_token` cookie 5. Paste that token in the setup wizard textarea (the setup wizard exchanges your temporary token for a long-lived token automatically) > **Keep this token secret** `AEM_ADMIN_API_AUTH_TOKEN` is a long-lived credential. Treat it like a password: never commit it to source control, and confirm your `.env` file is listed in `.gitignore` before you deploy. - **App Builder credentials**: To configure the credentials: 1. Go to the https://developer.adobe.com/console 2. Open your project and navigate to the workspace you want to use (Stage or Production) 3. Click **Download All** in the top-right to download the App Builder configuration file (a `.json` file) 4. Run `aio app use ` in the root directory of your prerender project to activate the credentials - **Commerce credentials** (Adobe Commerce as a Cloud Service): Access your Commerce Admin → System → Services → https://experienceleague.adobe.com/en/docs/commerce-merchant-services/user-guides/integration-services/saas for environment and API configuration - **Commerce credentials** (Adobe Commerce Optimizer): Configure your Commerce backend connection according to your deployment type. See the /setup/configuration/commerce-configuration/ documentation for details on configuring Optimizer headers and endpoints. 5. **Deploy to App Builder**: ```bash npm run deploy ``` This deploys all actions and schedules to your selected workspace (Stage or Production). 6. **Test the deployment**: - Manually invoke actions using the Management UI or via CLI commands: ```bash # Fetch all products from Catalog Service and store them aio rt action invoke aem-commerce-ssg/fetch-all-products # Check for product changes and generate markup aio rt action invoke aem-commerce-ssg/check-product-changes # Clean up and unpublish deleted products aio rt action invoke aem-commerce-ssg/mark-up-clean-up ``` - Verify product markup generation for sample SKUs - Check that pages publish successfully to your AEM project 7. **Enable automated synchronization**: - Use the https://prerender.aem-storefront.com to start the change detector - Configure polling intervals in the `app.config.yaml` file (default: checks for changes every 5 minutes) - Monitor the product index to verify continuous updates > **app.config.yaml productDependencies** If your `app.config.yaml` includes a `productDependencies` array, ensure each entry includes the required `maxVersion` property. The Adobe I/O CLI validates this during `aio app build`. See the Troubleshooting section below for details if you encounter build errors. > **App Builder costs** The prerender app runs as a paid App Builder application. Actions, scheduled triggers, and blob storage all consume App Builder quota. For large catalogs or frequent polling intervals, review the https://developer.adobe.com/app-builder/docs/overview/pricing/ documentation before deploying to production. ### Management UI After deployment, access the web-based https://prerender.aem-storefront.com to monitor and control your prerender deployment. Configure the UI with your App Builder workspace credentials before deployment-specific data appears. The Management UI provides: - **Monitoring capabilities** to view published products and track the product index - **Change detector controls** to start, stop, and configure scheduled product polling - **Markup preview** to view generated HTML for product pages before publication - **Logs and activations** for debugging App Builder action executions - **Storage management** to trigger manual product operations and manage App Builder file storage - **Configuration settings** to review environment variables and deployment parameters You configure access and authentication during the initial deployment. See the https://github.com/adobe-rnd/aem-commerce-prerender for details. ## Storefront integration With the App Builder app deployed, you've completed the first repository. Now modify your Commerce boilerplate — the second repository — so product pages can read and enhance the prerendered HTML the app publishes. ### The two-repository architecture The prerender solution involves two separate codebases that work together: 1. **The aem-commerce-prerender repository** (App Builder app) - This is the repository you cloned, configured, and deployed in the previous section - Runs as a serverless application in Adobe App Builder - Generates prerendered HTML and publishes it to your Edge Delivery Services site - Operates independently as a background process 2. **Your storefront repository** (Commerce boilerplate) - Your customer-facing website code - Built on the https://github.com/hlxsites/aem-boilerplate-commerce - Needs code changes to read and use the prerendered HTML - You'll make the integration changes in this repository. #### How they connect The prerender app and your storefront meet at each product page URL. The prerender app saves generated HTML in App Builder storage. The AEM Admin API tells Edge Delivery Services where to find those files so they can be served as an overlay: ready-made HTML at the same address as your product pages (for example, `/products/acme-widget/sku123`). When a shopper or search crawler opens that URL, Edge Delivery Services returns the prerendered HTML first, so product names, images, and descriptions appear without waiting for JavaScript. After the page loads, your storefront JavaScript runs and replaces that static HTML with the interactive product detail page (PDP) drop-in. Shoppers then see live prices, stock, and add-to-cart. Use lowercase SKUs in your product URLs because the drop-in reads the SKU from the `` tag (which preserves the original casing) rather than from the URL. See [URL format and SKU handling](#url-format-and-sku-handling) below. ```mermaid flowchart LR subgraph repo1[" "] direction TB A0["Repository 1 aem-commerce-prerender (App Builder Application)"] A1@{ label: "👨‍💻 Developer Actions" } A2["Clone repo Configure .env Deploy to App Builder"] end subgraph repo2[" "] direction TB D0["Repository 2 Your Storefront (Commerce Boilerplate)"] D1@{ label: "👨‍💻 Developer Actions" } D2["Modify product-details block to read prerendered HTML and enhance with PDP drop-in"] end subgraph repos["Developer Repositories"] direction TB repo1 repo2 end subgraph appbuilder["App Builder Workspace"] direction TB B0["Runs Independently"] B1["Detect product changes"] B2["Generate prerendered HTML with semantic markup"] B3["Store HTML in App Builder blob store"] B4["AEM Admin API configures overlay link to blob store"] end subgraph eds["Edge Delivery Services"] direction TB C0["Content Delivery Layer"] C1["Serves prerendered HTML from overlay at product URLs /products/{urlKey}/{sku}"] end subgraph delivery["Page Delivery Flow"] direction TB E1@{ label: "🤖 Crawler Request" } E2@{ label: "👤 User Request" } E3["Edge Delivery serves prerendered HTML"] E4["Crawler reads static HTML"] E5["Browser executes storefront JavaScript"] E6["PDP drop-in replaces HTML with interactive UI"] end A0 -.-> A1 A1 --> A2 D0 -.-> D1 D1 --> D2 B0 L_B0_B1_0@--> B1 B1 L_B1_B2_0@--> B2 B2 L_B2_B3_0@--> B3 B3 L_B3_B4_0@--> B4 C0 -.-> C1 E1 --> E3 E2 --> E3 E3 --> E4 & E5 E5 --> E6 repo1 L_repo1_appbuilder_0@-. npm run deploy .-> appbuilder repo2 == Deployed to ==> eds B4 L_B4_eds_0@-. Overlay configured .-> eds eds L_eds_delivery_0@-. Serves content .-> delivery A1@{ shape: rect} D1@{ shape: rect} E1@{ shape: rect} E2@{ shape: rect} A0:::title A1:::icon A2:::action D0:::title D1:::icon D2:::action B0:::note B1:::process B2:::process B3:::process B4:::process C0:::note C1:::storage E1:::icon E2:::icon E3:::delivery E4:::crawlerEnd E5:::delivery E6:::delivery classDef title fill:#f5f5f5,stroke:#666,stroke-width:1px,color:#333 classDef note fill:#fff,stroke:#999,stroke-width:1px,color:#666,font-style:italic classDef icon fill:none,stroke:none,color:#000000 classDef action fill:#fff3e0,stroke:#f57c00,stroke-width:2px classDef process fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px classDef storage fill:#e8f5e9,stroke:#388e3c,stroke-width:2px classDef delivery fill:#fff9c4,stroke:#f57f17,stroke-width:2px classDef crawlerEnd fill:#c8e6c9,stroke:#388e3c,stroke-width:2px style repo1 fill:#f3e5f5,stroke:#7b1fa2,stroke-width:1px style repo2 fill:#fff3e0,stroke:#f57c00,stroke-width:1px style appbuilder fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style eds fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style delivery fill:#fce4ec,stroke:#c2185b,stroke-width:2px style repos fill:#e3f2fd,stroke:#1976d2,stroke-width:2px L_B0_B1_0@{ animation: slow } L_B1_B2_0@{ animation: slow } L_B2_B3_0@{ animation: slow } L_B3_B4_0@{ animation: slow } L_repo1_appbuilder_0@{ animation: slow } L_B4_eds_0@{ animation: slow } L_eds_delivery_0@{ animation: slow } ``` *Two-repository architecture showing how the App Builder prerender app publishes HTML that your storefront code consumes and enhances for visitors.* ### How prerendered HTML reaches your storefront The prerendered HTML flows from the App Builder app to your storefront through App Builder storage and Edge Delivery Services overlay configuration: #### Publishing mechanism 1. The App Builder app generates complete HTML files with prerendered product markup. 2. The App Builder app saves those HTML files to its own file storage. 3. The AEM Admin API configures the link to the App Builder file storage as an overlay in your Edge Delivery Services configuration. 4. The AEM Admin API updates your sitemap to include the new and updated product pages. 5. Edge Delivery Services serves the prerendered HTML from the overlay when product page URLs are requested. The Commerce Prerender app uses App Builder file storage and Edge Delivery Services overlay configuration. It does _not_ use events to trigger updates. Each time the App Builder app detects a product change and generates HTML, it saves the HTML to App Builder storage and configures the overlay link via the AEM Admin API. When the overlay is configured, the prerendered HTML is served at the product URLs automatically. The storefront needs no additional fetching or configuration. ### How the storefront serves prerendered HTML When a visitor requests a product page URL (for example, `/products/acme-widget/sku123`), Edge Delivery Services serves the prerendered HTML as the actual page. The HTML already contains the `product-details` block populated with prerendered content. Your storefront doesn't fetch it separately. #### The delivery sequence 1. **Request arrives** → Edge Delivery Services receives the URL request 2. **HTML served** → Edge Delivery Services returns the prerendered HTML file (published by the App Builder app) 3. **Page loads** → The browser displays the page with prerendered content (crawlers stop here) 4. **JavaScript executes** → Your storefront code runs 5. **Enhancement** → The `product-details` block's `decorate()` function replaces the static HTML with the interactive PDP drop-in, which fetches live prices, stock, and add-to-cart from Commerce APIs This process is called progressive enhancement (An approach where a page starts as complete, readable HTML and JavaScript upgrades it to an interactive experience when it runs. Shoppers with JavaScript get live prices, stock, and add-to-cart. Crawlers and shoppers without JavaScript still see all the product information from the prerendered HTML.): the prerendered HTML is the complete page that everyone sees first. The storefront code reads the page's existing HTML, extracts the SKU from the meta tag, and upgrades the page to a fully interactive experience for shoppers with JavaScript. Crawlers and shoppers without JavaScript always see complete product information from the prerendered HTML. ### Modifying your `product-details` block To integrate prerendered markup with the PDP drop-in, you'll need to modify the `product-details` block in your https://github.com/hlxsites/aem-boilerplate-commerce. #### Location The https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/product-details lives in your boilerplate at: ``` blocks/product-details/product-details.js ``` #### Integration pattern By the time `decorate()` runs, Edge Delivery Services has already served the prerendered HTML to the browser, so crawlers have read that content before any JavaScript executes. When `decorate()` runs, it replaces the static block content with the interactive PDP layout. Your `decorate()` function should: 1. **Retrieve the SKU**: Read the product SKU from the `` tag. The prerender system stores the original-casing SKU here, so the drop-in gets the correct value even after URL lowercasing. 2. **Mount the PDP drop-in**: Call `mountImmediately` to initialize the PDP with the retrieved SKU. 3. **Replace with PDP layout**: Clear the block content and render the interactive PDP containers. > **Simplified code example** This snippet is a minimal teaching version: it reads the SKU, runs `mountImmediately(initialize, { sku })`, clears the block, and mounts `ProductHeader` only. In the Commerce boilerplate, PDP is initialized from `scripts/initializers/pdp.js`, which calls `mountImmediately` with `langDefinitions`, `models`, fetched placeholders, and the other options documented on [Product Details initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/initialization/). The real `product-details` block builds a layout fragment and renders every region (for example, `ProductGallery`, `ProductHeader`, `ProductPrice`, and `ProductOptions`). For production patterns, see the https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/product-details in the Commerce boilerplate and the [containers overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/containers/). ```js // File location: blocks/product-details/product-details.js // Import paths are relative to blocks/product-details/product-details.js export default async function decorate(block) { // Edge Delivery Services served the prerendered HTML before this function ran. // Crawlers have already read that content. decorate() now replaces it with the interactive layout. // Step 1: Retrieve the SKU from the meta tag (with URL fallback), then initialize PDP // getProductSku() reads the SKU from the tag and falls back to URL parsing. // In template preview mode (Universal Editor or Document Authoring), it reads a defaultSku value from the block config instead. const sku = getProductSku(); await initializers.mountImmediately(initialize, { sku }); // Step 2: Replace prerendered HTML with interactive composed containers // Production code builds a layout fragment and renders every region. This sample mounts ProductHeader only. block.innerHTML = ''; await productRenderer.render(ProductHeader, {})(block); // ... other rendering code } ``` #### URL format and SKU handling The prerender system generates markup files that match your storefront product URL structure. By default, this uses the format: ```plaintext /products/{urlKey}/{sku} ``` The `PRODUCT_PAGE_URL_FORMAT` can be customized in your https://github.com/adobe-rnd/aem-commerce-prerender?tab=readme-ov-file#url-naming-and-sanitization. > **SKU lowercase requirement** Starting with the October 2025 Adobe Commerce Storefront release, all SKUs in product URLs are automatically converted to lowercase to ensure URL consistency and proper resolution. When the prerendered markup is generated, the actual SKU (with original casing) is stored in a meta tag: ```html ``` This allows your PDP drop-in to retrieve the correct SKU regardless of URL transformations, which is why the code example above retrieves the SKU from the meta tag rather than parsing it from the URL. For complete implementation examples, refer to the https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/product-details in the Commerce boilerplate. ## Customization The prerender solution provides these customization options: | Customization | Purpose and Configuration | |---------------|---------------------------| | **Templates** | Control HTML markup structure and styling. By default, markup is handled on the client side at `{store_url}/product/default`. If the default template is not available, you can edit the template at https://github.com/adobe-rnd/aem-commerce-prerender/blob/main/actions/pdp-renderer/templates/product-details.hbs | | **Structured Data** | Customize JSON-LD schemas in https://github.com/adobe-rnd/aem-commerce-prerender/blob/main/actions/pdp-renderer/ldJson.js to optimize product rich snippets for search engines | | **GraphQL Queries** | Extend product data by modifying GraphQL queries in https://github.com/adobe-rnd/aem-commerce-prerender/blob/main/actions/queries.js. The rendering logic that uses these queries is implemented in the `generateProductHtml` method in https://github.com/adobe-rnd/aem-commerce-prerender/blob/main/actions/pdp-renderer/render.js | | **URL Patterns** | Match your storefront URL structure by configuring `PRODUCT_PAGE_URL_FORMAT` in the `.env` file | | **Rendering Logic** | Transform product data before markup generation by implementing custom logic in App Builder actions | For implementation details, see the https://github.com/adobe-rnd/aem-commerce-prerender, particularly the `actions/` directory and repository root for templates and configuration files. ## Troubleshooting For troubleshooting during development and operations, consult the https://github.com/adobe-rnd/aem-commerce-prerender/blob/main/docs/RUNBOOK.md. Common issues include: - **Authentication failures**: Verify that your AEM Admin API token is valid (tokens expire after 1 year). To check the token expiration, decode the token using https://www.jwt.io/ or check your https://www.aem.live/docs/admin.html#setting-up-authentication-for-admin-api. - Generate a new token if expired (see https://www.aem.live/docs/admin.html#tag/orgConfig/operation/createOrgApiKey) and update `AEM_ADMIN_API_AUTH_TOKEN` in your `.env` file - Redeploy with `npm run deploy` after updating credentials - **Product not rendering**: Check that the product exists in Catalog Service and that the SKU matches. - Query the Catalog Service directly using its https://developer.adobe.com/commerce/services/graphql/catalog-service/products/ to verify product data - Use the Management UI to view the product index and confirm that the SKU is listed - Run `aio rt activation list` to check action logs. For more details, see the https://github.com/adobe-rnd/aem-commerce-prerender/blob/main/docs/RUNBOOK.md - **Markup not updating**: Review the Change Detector logs to ensure scheduled triggers are running. - Access the https://prerender.aem-storefront.com to verify the change detector status (should show "Running") - Run `aio rt activation list` to check activation logs. For more details, see the https://github.com/adobe-rnd/aem-commerce-prerender/blob/main/docs/RUNBOOK.md - Manually trigger the change detector from the Management UI to test functionality - **Build error: "Missing or invalid keys in app.config.yaml"**: This error occurs when `productDependencies` entries are missing the required `maxVersion` property. - **Error message**: `must have required property 'maxVersion'` in `/productDependencies/0` - **Solution**: Ensure each entry in the `productDependencies` array includes `maxVersion`. Example: ```yaml productDependencies: - code: minVersion: maxVersion: # Required field ``` - Update your `app.config.yaml` file and redeploy with `npm run deploy` - **Deployment errors**: If you encounter 403 errors when running `npm run deploy`, the `aio` CLI may be authenticated to the wrong organization. Run `aio login --force` to re-authenticate and choose the correct organization. ## Additional resources - **App Builder Documentation**: https://developer.adobe.com/app-builder/docs/overview/ - **Catalog Service Guide**: https://experienceleague.adobe.com/en/docs/commerce/catalog-service/installation - **Edge Delivery Services**: https://www.aem.live/docs/publishing-from-authoring - **AEM Admin API**: https://www.aem.live/docs/admin.html (The prerender app automatically uses this API to publish content; useful for understanding the publishing mechanism and troubleshooting authentication issues) ## Support For technical issues or questions during implementation, consult the https://github.com/adobe-rnd/aem-commerce-prerender/blob/main/docs/RUNBOOK.md or contact Adobe Commerce support. - [Merchant documentation](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/content-customizations/prerendered-product-pages/) — Explain prerendering benefits to store administrators --- # Storefront configuration In this section, you'll learn how Commerce blocks in your storefront connect to a Commerce backend using values from either the https://www.aem.live/docs/admin.html#schema/PublicConfig for your site or a config.json file in your code repo. The configuration is organized into several key sections: - **Endpoints** - GraphQL endpoints for Commerce and Catalog Services - **Headers** - HTTP headers required for API authentication and store context - **Analytics** - Store and environment metadata for tracking and reporting - **Plugins** - Configuration for various Edge Delivery Services plugins and features - **Multistore** - Settings for multiple store views and internationalization When implementing your own project, you must update the configuration values with: - The Adobe Commerce and Catalog Service GraphQL endpoint that you configured as part of the [content delivery network (CDN) setup](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/content-delivery-network/). - The header values specific to your Adobe Commerce Catalog Services environment. If you [set up your storefront](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/) using the boilerplate code, refer to the config generator tool to generate a `config.json` file for your Commerce backend. This file will contain the environment values for a Commerce backend including GraphQL endpoints and headers. If you set up your site using the https://da.live/app/adobe-commerce/storefront-tools/tools/site-creator/site-creator, the `config.json` file is created and added to the GitHub repository created for the boilerplate code. You can review and update the default values in the `config.json` file after the process completes. :::note **Need help configuring your Commerce backend?** Use the https://da.live/app/adobe-commerce/storefront-tools/tools/config-generator/config-generator to automatically generate the appropriate configuration structure for your Adobe Commerce, Adobe Commerce as a Cloud Service, or Adobe Commerce Optimizer backend. The tool will detect your backend type and generate the correct configuration, though you should validate and confirm the results. ::: This page covers your storefront configuration (The JSON configuration object used by storefront code to resolve endpoints, headers, analytics, and plugin behavior.), when to replace sample default values (Starter values in a sample configuration that must be replaced with environment-specific values for your project.), and how helper APIs such as getConfigValue function (Helper function that reads a configuration value by dot-notation path from storefront config data.) and getHeaders function (Helper function that returns a header map for a given storefront scope based on configured header entries.) read resolved values. ## Configuration Caching The storefront configuration is cached at multiple levels to optimize performance: - **CDN Caching**: The configuration file is cached by the EDS CDN with a `max-age=7200` (2 hours) cache control header. This means configuration changes can take up to 2 hours to be reflected on the CDN after deployment. - **Browser Caching**: When the configuration is loaded in the browser, it is stored in https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage, which persists until the browser session is cleared. **For developers**: After making configuration changes, it can take up to two hours for the CDN cache to expire. Clear your browser's session storage to see updates. Verify the current cache configuration by accessing `/config.json` directly and checking the `last-modified` header. ## Configuration Sections The following sections detail each part of your configuration for Adobe Commerce. Each section includes placeholders (marked with `{{PLACEHOLDER}}`) that you must replace with your specific Commerce environment details. You can find your configuration in your code repo at `/config.json`, or in the config service for your site. If you do not have a configuration yet, you can: - Copy and customize the demo config for the boilerplate: https://main--aem-boilerplate-commerce--hlxsites.aem.live/config.json. - Or use the https://da.live/app/adobe-commerce/storefront-tools/tools/config-generator/config-generator to generate a configuration with your backend values (may contain placeholders to fill in). Be sure to update all values to match your Commerce backend before deploying. ### Endpoints The endpoints properties define the GraphQL API endpoints for your Commerce backend. ```json { "commerce-core-endpoint": "{{COMMERCE_CORE_ENDPOINT}}", "commerce-endpoint": "{{COMMERCE_ENDPOINT}}" } ``` **Configuration Properties:** - **`commerce-core-endpoint`** (read/write) - Core GraphQL endpoint for queries and mutations. This endpoint handles all write operations and some read operations. See https://developer.adobe.com/commerce/webapi/reference/graphql/latest/ for details. - **`commerce-endpoint`** (read-only) - Services GraphQL endpoint optimized for read-only operations with Catalog Service, Live Search, and Product Recommendations. It is also optimized for performance for product data retrieval. For details, see the following documentation: - For Adobe Commerce as a Cloud Service, see the https://experienceleague.adobe.com/en/docs/commerce/catalog-service/installation#access-the-service. - For Adobe Commerce Optimizer, see the https://developer.adobe.com/commerce/services/optimizer/merchandising-services/using-the-api/#base-url. ### Headers The headers section defines HTTP headers required for API authentication and store context. Headers are organized by scope to apply different authentication and context settings for different request types. The specific headers required depend on your Commerce environment type. ### Adobe Commerce (PaaS) Adobe Commerce PaaS environments use standard Adobe Commerce header naming conventions and require API keys for SaaS services. ```json { "headers": { "all": { "Store": "{{STORE_VIEW_CODE}}" }, "cs": { "Magento-Store-Code": "{{STORE_CODE}}", "Magento-Store-View-Code": "{{STORE_VIEW_CODE}}", "Magento-Website-Code": "{{WEBSITE_CODE}}", "x-api-key": "{{API_KEY}}", "Magento-Environment-Id": "{{ENVIRONMENT_ID}}" } } } ``` **Header Scopes:** - **`all`** - Headers applied to all GraphQL requests - **`Store`** - Store view code for Core GraphQL requests. See https://developer.adobe.com/commerce/webapi/graphql/usage/headers/ for details. - **`cs`** - Headers for Catalog Service requests - **`Magento-Store-Code`** - Store to connect to. See https://experienceleague.adobe.com/en/docs/commerce-operations/configuration-guide/multi-sites/ms-admin#step-3-create-stores for details. - **`Magento-Store-View-Code`** - Store view for Catalog Service requests. See https://experienceleague.adobe.com/en/docs/commerce-admin/stores-sales/site-store/store-views for details. - **`Magento-Website-Code`** - Website to connect to. See https://experienceleague.adobe.com/en/docs/commerce-operations/configuration-guide/multi-sites/ms-admin#step-2-create-websites for details. - **`x-api-key`** - API key for SaaS services (Catalog Service, Live Search, Product Recommendations). See https://experienceleague.adobe.com/en/docs/commerce/user-guides/integration-services/saas for details. - **`Magento-Environment-Id`** - Connects the storefront to the cloud instance serving it. See https://experienceleague.adobe.com/en/docs/commerce-cloud-service/user-guide/project/overview#environment-overview for details. ### Adobe Commerce as a Cloud Service (ACCS) Adobe Commerce as a Cloud Service uses standard Adobe Commerce header naming conventions but does not require API keys or environment IDs in the default configuration. ```json { "headers": { "all": { "Store": "{{STORE_VIEW_CODE}}" }, "cs": { "Magento-Store-Code": "{{STORE_CODE}}", "Magento-Store-View-Code": "{{STORE_VIEW_CODE}}", "Magento-Website-Code": "{{WEBSITE_CODE}}" } } } ``` **Header Scopes:** - **`all`** - Headers applied to all GraphQL requests - **`Store`** - Store view code for Core GraphQL requests. See https://developer.adobe.com/commerce/webapi/graphql/usage/headers/ for details. - **`cs`** - Headers for Catalog Service requests - **`Magento-Store-Code`** - Store to connect to. See https://experienceleague.adobe.com/en/docs/commerce-operations/configuration-guide/multi-sites/ms-admin#step-3-create-stores for details. - **`Magento-Store-View-Code`** - Store view for Catalog Service requests. See https://experienceleague.adobe.com/en/docs/commerce-admin/stores-sales/site-store/store-views for details. - **`Magento-Website-Code`** - Website to connect to. See https://experienceleague.adobe.com/en/docs/commerce-operations/configuration-guide/multi-sites/ms-admin#step-2-create-websites for details. ### Adobe Commerce Optimizer Adobe Commerce Optimizer uses different header naming conventions and requires Commerce Optimizer-specific headers as defined in the https://developer.adobe.com/commerce/services/optimizer/merchandising-services/using-the-api/#headers. ```json { "headers": { "cs": { "AC-View-ID": "{{CATALOG_VIEW_ID}}", "AC-Price-Book-ID": "{{PRICE_BOOK_ID}}", "AC-Policy-{{*}}": "{{ATTRIBUTE_VALUE}}" } } } ``` **Header Scopes:** - **`cs`** - Headers applied to all GraphQL requests - **`AC-View-ID`** - (Required) The unique ID assigned to the catalog view that products will be sold through. You can find this in the https://experienceleague.adobe.com/en/docs/commerce/optimizer/setup/catalog-view. - **`AC-Price-Book-ID`** - (Optional) Specifies the Price Book ID to use for pricing. Each catalog view defines a default Price Book. If a customer has an assigned Price Book, it is used when it falls within the allowed Price Books for the catalog view. Otherwise, the default Price Book for the catalog view is used. See /setup/configuration/price-book-setup/ for implementation details. - **`AC-Policy-{{*}}`** - (Optional) Policy trigger for filtering products by attribute. The header should match the "name" of the trigger. For example, `AC-Policy-Brand:Cruz` for brand filtering using "Cruz". You can specify multiple policy headers per request. :::note[Troubleshooting Optimizer Headers] **Warning:** There is currently a bug (being resolved) that may require adding the `AC-Environment-ID` header in some Optimizer configurations. If you have all the requisite Optimizer headers configured correctly and are still experiencing problems, you may need to add this header: ```json { "headers": { "cs": { "AC-Environment-Id": "{{TENANT_ID}}", // ... your other Optimizer headers } } } ``` ::: ### Analytics The analytics section contains store and environment metadata for tracking and reporting. The specific values depend on your Commerce environment type. ### Adobe Commerce (PaaS) / SaaS For Adobe Commerce PaaS and Adobe Commerce as a Cloud Service environments, use standard Commerce store and website identifiers from your Commerce Environment. You can obtain these values using a `storeConfig` query. ```json { "analytics": { "aep-ims-org-id": "{{IMS_ORG_ID}}", "aep-datastream-id": "{{DATASTREAM_ID}}", "base-currency-code": "{{CURRENCY_CODE}}", "environment": "{{ENVIRONMENT_TYPE}}", "environment-id": "{{ENVIRONMENT_ID}}", "store-code": "{{STORE_CODE}}", "store-id": {{STORE_ID}}, "store-name": "{{STORE_NAME}}", "store-url": "{{STORE_URL}}", "store-view-code": "{{STORE_VIEW_CODE}}", "store-view-id": {{STORE_VIEW_ID}}, "store-view-name": "{{STORE_VIEW_NAME}}", "website-code": "{{WEBSITE_CODE}}", "website-id": {{WEBSITE_ID}}, "website-name": "{{WEBSITE_NAME}}" } } ``` **Configuration Properties:** - **`aep-ims-org-id`** - (Optional) Adobe IMS Organization ID for Experience Platform integration (for example, `"1234567890ABCDEF7F000101@AdobeOrg"`) - **`aep-datastream-id`** - (Optional) Datastream ID for routing data to Adobe Experience Platform (for example, `"12345678-1234-1234-1234-123456789012"`) - **`base-currency-code`** - The base currency code for the store (for example, "USD", "EUR") - **`environment`** - Environment type ("Production", "Testing") - **`environment-id`** - Unique identifier for the Commerce environment - **`store-url`** - Base URL for the store - **`store-code`** - Code identifier for the store from your Commerce environment - **`store-id`** - Numeric ID for the store - **`store-name`** - Display name for the store - **`store-view-code`** - Code identifier for the store view - **`store-view-id`** - Numeric ID for the store view - **`store-view-name`** - Display name for the store view - **`website-code`** - Code identifier for the website from your Commerce environment - **`website-id`** - Numeric ID for the website - **`website-name`** - Display name for the website > **Adobe Experience Platform integration** To enable automatic event forwarding to Adobe Experience Platform, include both `aep-ims-org-id` and `aep-datastream-id` in your analytics configuration. When both values are present, events will automatically be sent to AEP. See the [Adobe Experience Platform integration guide](https://experienceleague.adobe.com/developer/commerce/storefront/setup/analytics/adobe-experience-platform/) for detailed setup instructions. ### Adobe Commerce Optimizer For Adobe Commerce Optimizer environments, use a simplified analytics configuration structure: ```json { "analytics": { "aep-ims-org-id": "{{IMS_ORG_ID}}", "aep-datastream-id": "{{DATASTREAM_ID}}", "base-currency-code": "{{CURRENCY_CODE}}", "environment": "{{ENVIRONMENT_TYPE}}", "environment-id": "{{YOUR_TENANT_ID}}", "store-url": "{{STORE_URL}}", "store-view-currency-code": "{{CURRENCY_CODE}}", "storefront-template": "{{TEMPLATE_TYPE}}", "view-id": "{{CATALOG_VIEW_ID}}" } } ``` **Configuration Properties:** - **`aep-ims-org-id`** - (Optional) Adobe IMS Organization ID for Experience Platform integration (for example, `"1234567890ABCDEF7F000101@AdobeOrg"`) - **`aep-datastream-id`** - (Optional) Datastream ID for routing data to Adobe Experience Platform (for example, `"12345678-1234-1234-1234-123456789012"`) - **`base-currency-code`** - The base currency code for the store (for example, "USD", "EUR") - **`environment`** - Environment type ("Production", "Testing") - **`environment-id`** - The tenant ID for the Adobe Commerce Optimizer instance - **`store-url`** - Base URL for the store - **`store-view-currency-code`** - Currency code for the store view (for example, "USD", "EUR") - **`storefront-template`** - (Optional) Storefront template type (for example, "Other") - **`view-id`** - The unique ID assigned to the catalog view from the Adobe Commerce Optimizer UI ### Plugins The plugins section configures various Commerce plugins and features. ```json { "plugins": { "picker": { "rootCategory": "{{ROOT_CATEGORY_ID}}" } } } ``` **Configuration Properties:** - **`picker.rootCategory`** - Root category ID for the product picker. This determines which category serves as the starting point when browsing products in the Commerce interface. ### Multistore The multistore section manages configurations for multiple store views and internationalization. This allows different configurations for different locales or store views. Each non-"default" configuration will be merged with the default at runtime and used for the corresponding locale or store view. For example, the below would serve as a basis for configuration for the site at aemshop.net/fr. ```json { "/fr/": { "headers": { "all": { "Store": "{{FRENCH_STORE_VIEW_CODE}}" }, "cs": { "Magento-Store-Code": "{{FRENCH_STORE_CODE}}", "Magento-Website-Code": "{{FRENCH_WEBSITE_CODE}}", "Magento-Store-View-Code": "{{FRENCH_STORE_VIEW_CODE}}" } } } } ``` **Configuration Properties:** - **Path-based configuration** - Use URL paths (like `/fr/`, `/de/`, etc.) to define locale-specific or store-specific overrides - **Header overrides** - Override default headers for specific store views - **Store context** - Define different store, website, and store view codes for each locale ### Additional Configuration Options Some configuration options can be set to enable or disable certain boilerplate features. ```json { "commerce-assets-enabled": boolean, "commerce-b2b-enabled": boolean, "commerce-companies-enabled": boolean, } ``` - **`commerce-assets-enabled`** - Boolean flag to enable or disable AEM Assets within the storefront - **`commerce-b2b-enabled`** - Boolean flag to enable or disable B2B-related global and shared logic within the storefront. When enabled, this flag is passed to the Auth initializer as `customerPermissionRoles: true` to enable customer role permissions API. This flag controls B2B functionality at the authentication and authorization level, enabling the drop-in to automatically fetch B2B customer role permissions on initialization and re-fetch when authentication state changes. See the [User Auth initialization documentation](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/user-auth/initialization/) for details. - **`commerce-companies-enabled`** - Boolean flag to enable or disable Commerce B2B company features within the storefront. This flag controls company-specific features such as company management, company structure, and company-related blocks. > **B2B Configuration Flags** **Understanding the difference:** - **`commerce-b2b-enabled`** - Controls B2B functionality at the authentication and authorization level. When set to `true`, this flag enables the customer role permissions API in the Auth drop-in (via the `customerPermissionRoles` parameter), which automatically fetches B2B customer role permissions including purchase order and company management capabilities. This is required for B2B features to work properly. See the [User Auth v3.0.0 changelog](https://experienceleague.adobe.com/developer/commerce/storefront/releases/changelog/) for implementation details. - **`commerce-companies-enabled`** - Controls company-specific features and blocks. This flag enables or disables company management features, company structure, and company-related commerce blocks. **Relationship:** While these flags serve different purposes, `commerce-b2b-enabled` is typically required for B2B features to function correctly, as it enables the underlying authentication and permission system. `commerce-companies-enabled` can be used independently to control company-specific features, but company features will only work properly when `commerce-b2b-enabled` is also set to `true`. For a complete B2B storefront with company features, set both flags to `true`: ```json { "commerce-b2b-enabled": true, "commerce-companies-enabled": true } ``` **Implementation:** To use `commerce-b2b-enabled` in your storefront, read it from config in your Auth initializer and pass it to the Auth drop-in: ```javascript title="scripts/initializers/user-auth.js" const isB2BEnabled = await getConfigValue('commerce-b2b-enabled'); await initializers.mountImmediately(initialize, { customerPermissionRoles: isB2BEnabled, // ... other options }); ``` This enables the customer role permissions API when B2B features are enabled in your configuration. ## Configuration Examples The following examples show real `config.json` files from different Adobe Commerce environments. These examples demonstrate the actual structure and values used in production environments. ### Adobe Commerce (PaaS) This example shows the configuration for an Adobe Commerce PaaS environment, which includes additional features like multistore support and asset management. This configuration is used on this site: https://www.aemshop.net/ ```json title="aemshop.net config.json example" showLineNumbers { "public": { "default": { "commerce-core-endpoint": "https://www.aemshop.net/graphql", "commerce-endpoint": "https://www.aemshop.net/cs-graphql", "headers": { "all": { "Store": "default" }, "cs": { "Magento-Store-Code": "main_website_store", "Magento-Store-View-Code": "default", "Magento-Website-Code": "base", "x-api-key": "4dfa19c9fe6f4cccade55cc5b3da94f7", "Magento-Environment-Id": "f38a0de0-764b-41fa-bd2c-5bc2f3c7b39a" } }, "analytics": { "aep-ims-org-id": null, "aep-datastream-id": null, "base-currency-code": "USD", "environment": "Production", "environment-id": "f38a0de0-764b-41fa-bd2c-5bc2f3c7b39a", "store-code": "main_website_store", "store-id": 1, "store-name": "Main Website Store", "store-url": "https://www.aemshop.net", "store-view-code": "default", "store-view-id": 1, "store-view-name": "Default Store View", "website-code": "base", "website-id": 1, "website-name": "Main Website" }, "plugins": { "picker": { "rootCategory": "2" } }, "commerce-assets-enabled": false }, "/fr/": { "headers": { "all": { "Store": "fr" }, "cs": { "Magento-Store-Code": "fr_store", "Magento-Website-Code": "fr_website", "Magento-Store-View-Code": "fr" } } } } } ``` ### Adobe Commerce as a Cloud Service (ACCS) This example shows the configuration for Adobe Commerce as a Cloud Service (ACCS). For ACCS you only need to set **`commerce-endpoint`** to your ACCS GraphQL URL; the same endpoint is used for catalog and core. This configuration is used on this site: https://main--boilerplate-accs--adobe-commerce.aem.live/ ```json title="ACCS config.json" showLineNumbers { "public": { "default": { "commerce-endpoint": "https://na1-sandbox.api.commerce.adobe.com/LwndYQs37CvkUQk9WEmNkz/graphql", "headers": { "all": { "Store": "default" }, "cs": { "Magento-Store-Code": "main_website_store", "Magento-Store-View-Code": "default", "Magento-Website-Code": "base" } }, "analytics": { "base-currency-code": "USD", "environment": "Testing", "environment-id": "LwndYQs37CvkUQk9WEmNkz", "store-code": "main_website_store", "store-id": 1, "store-name": "ACCS Store", "store-url": "https://main--boilerplate-accs--adobe-commerce.aem.live", "store-view-code": "default", "store-view-id": 1, "store-view-name": "Default Store View", "website-code": "base", "website-id": 1, "website-name": "Main Website" }, "plugins": { "picker": { "rootCategory": "2" } } } } } ``` ### Adobe Commerce Optimizer (ACO) This example shows the configuration for Adobe Commerce Optimizer (ACO), which uses different header naming conventions and includes ACO-specific settings. For ACO, set commerce-endpoint (ACO GraphQL URL for catalog) at a minimum. Set commerce-core-endpoint (your core endpoint—PaaS or your own Adobe Commerce hosted environment) to enable transactional or other core-related functionality. Include "adobe-commerce-optimizer": true. This example is from https://main--boilerplate-aco--adobe-commerce.aem.live/ and does not include a core endpoint. ```json title="ACO config.json" showLineNumbers { "public": { "default": { "adobe-commerce-optimizer": true, "commerce-endpoint": "https://na1-sandbox.api.commerce.adobe.com/8idEEDDiVwjCEJAyB5kjfi/graphql", "headers": { "cs": { "ac-view-id": "0d3eebf7-b5fb-4904-9ccf-f35fcc61862b", "ac-price-book-id": "west_coast_inc" } }, "analytics": { "base-currency-code": "USD", "environment": "Testing", "environment-id": "8idEEDDiVwjCEJAyB5kjfi", "store-url": "https://main--boilerplate-aco--adobe-commerce.aem.live/", "store-view-currency-code": "USD", "storefront-template": "Other", "view-id": "0d3eebf7-b5fb-4904-9ccf-f35fcc61862b" }, "plugins": { "picker": { "rootCategory": "2" } } } } } ``` ## Local Development Understanding how storefront configuration resolution works is essential for effective local development and branch-based testing. This section explains how the browser retrieves configuration and how to manage it across different environments. ### How Configuration Resolution Works When your storefront loads in the browser, the following sequence occurs: 1. **Browser requests** `https://[site]/config.json` 2. **EDS CDN checks** if a `config.json` file exists in the code repository root 3. **If found** - The `config.json` file from the repository is served 4. **If not found** - The CDN falls back to the `public` config from the EDS site config (Config Service) :::caution[Do not commit config.json to main] A `config.json` file in your repository **always** takes precedence over the Config Service. This means if `config.json` is committed to your `main` branch, your Config Service values will be silently ignored in production. `config.json` is intended for **local development and branch-based testing only**. For production, use the Config Service and ensure `config.json` is absent from the `main` branch. ::: ### Getting a starter configuration To get a configuration for your Commerce storefront, use one of these approaches: 1. **Config Service (recommended for production)**: Use the Config Service to store and manage your production configuration. See the https://www.aem.live/docs/config-service-setup to get started. Your configuration is stored at `https://admin.hlx.page/config/{ORG}/sites/{SITE}/public.json` and served automatically — no `config.json` in the repository is needed. 2. **Generate a local starter config**: Use the https://da.live/app/adobe-commerce/storefront-tools/tools/config-generator/config-generator to automatically generate a configuration for your Commerce backend. Use this as the basis for your Config Service upload or for local development. Note that the generated config may contain placeholders that you'll need to replace with actual values. 3. **Copy the demo configuration**: Download and customize the live configuration for the boilerplate from https://main--aem-boilerplate-commerce--hlxsites.aem.live/config.json. This provides a working example with all required fields — useful as a reference for local development. **Important**: Always update the configuration values to match your specific Commerce backend. For production, push your configuration to the Config Service rather than committing `config.json` to your `main` branch. ### Local Development Configuration To work with your own Commerce backend locally: 1. **Get your configuration from the config generator tool:** - Visit the https://da.live/app/adobe-commerce/storefront-tools/tools/config-generator/config-generator and enter your API url. - Copy the configuration to your clipboard and paste it into your `config.json` file in your code repo. 2. **Edit config.json** with your Commerce backend values: - Update `commerce-endpoint` (required). - Add/update `commerce-core-endpoint` if necessary. - Update `headers`. - Update `analytics` values for your store. - Update any other values as needed, such as `commerce-assets-enabled`, etc. 3. **Add config.json to .gitignore** (optional): ```bash echo "config.json" >> .gitignore ``` This prevents you from accidentally committing your local configuration. 4. **Clear session storage and reload:** - Open browser DevTools (F12) - Go to **Application** > **Session Storage** - Delete the `config` entry - Reload the page 5. **Verify the configuration loaded:** - Check `http://localhost:3000/config.json` in your browser - Or check **Application** > **Session Storage** > `config` entry in DevTools ### Branch-Based Configuration You can commit `config.json` to any branch to provide branch-specific configuration. Understanding the behavior based on which branch you commit to is crucial: **Committing to a feature/test branch:** - Commit `config.json` to your branch (for example, `feature/new-backend`). - When deployed, `https://[branch]--[site]--[org].aem.page/config.json` will serve the configuration for your branch. - The `main` branch and other branches are unaffected. - Public config is NOT used for this branch. **Committing to main branch:** - If you commit `config.json` to `main`, it will ALWAYS be served. - Public config from EDS site config will NEVER be used (unless you delete `config.json` from main). - All branches without their own `config.json` will inherit the configuration from main. - Use this approach only if you want repository-based configuration for production. **Using public config (Recommended for production):** - Do NOT commit `config.json` to `main`. - Configure the `public` property in your EDS site config via the https://www.aem.live/docs/config-service-setup. - Access at `https://admin.hlx.page/config/{ORG}/sites/{SITE}/public.json`. - Update configuration without code deployments. - Any branch with a committed `config.json` will be used (on that branch) instead of this public config. ### Testing Configuration Changes After creating or updating your configuration (either in public config or `config.json`), follow these steps to verify the changes: 1. **Deploy or save your changes:** - For repository-based: Commit and push `config.json` - For public config: Update via the Configuration Service API 2. **Clear session storage:** - Open browser DevTools (F12) - Navigate to **Application** tab > **Session Storage** - Find and delete the `config` entry for your domain 3. **Reload the page:** - Hard refresh (Ctrl+Shift+R or Cmd+Shift+R) - Or simply reload after clearing session storage 4. **Verify the configuration:** - In DevTools, go to **Application** > **Session Storage** - Check the `config` entry contains your updated values - Or access `https://[your-site]/config.json` directly in the browser :::tip[Quick Verification] Always verify your configuration loaded correctly by checking the session storage `config` entry after reload. This ensures your storefront is using the expected configuration values. ::: ## Step-by-step We'll use a mock PDP / Catalog Service block to demonstrate how to utilize the config utilities and values. This example shows how to access the configuration sections detailed above. ### 1. Import the configuration functions. First, import the `getConfigValue` and `getHeaders` functions from the `scripts/configs.js` file in your boilerplate. ```javascript ``` ### 2. Access endpoint configuration. Use the `getConfigValue` function to retrieve endpoint URLs from the configuration. This function takes a dot-notation path that matches the keys in your `config.json`. ```javascript export default async function decorate(block) { // Get the catalog service endpoint from the endpoints section const catalogEndpoint = await getConfigValue('commerce-endpoint'); const coreEndpoint = await getConfigValue('commerce-core-endpoint'); console.log('Catalog Service endpoint:', catalogEndpoint); console.log('Core Commerce endpoint:', coreEndpoint); } ``` ### 3. Access header configuration. Use the `getHeaders` function to retrieve headers for specific scopes. This function automatically formats the headers for use in HTTP requests. ```javascript export default async function decorate(block) { // Get headers for Catalog Service requests const csHeaders = await getHeaders('cs'); // Get headers for all requests const allHeaders = await getHeaders('all'); console.log('CS headers:', csHeaders); console.log('All headers:', allHeaders); } ``` ### 4. Access analytics and other configuration. Use `getConfigValue` to access any configuration value using dot notation, including analytics data and plugin settings. ```javascript export default async function decorate(block) { // Get analytics configuration const storeUrl = await getConfigValue('analytics.store-url'); const currency = await getConfigValue('analytics.base-currency-code'); const environment = await getConfigValue('analytics.environment'); // Get plugin configuration const rootCategory = await getConfigValue('plugins.picker.rootCategory'); console.log('Store URL:', storeUrl, 'Currency:', currency, 'Environment:', environment); console.log('Root category:', rootCategory); } ``` ### 5. Make API requests with configuration. Combine the endpoint and header configuration to make authenticated API requests to your Commerce backend. ```javascript export default async function decorate(block) { // Get the catalog service endpoint const endpoint = await getConfigValue('commerce-endpoint'); // Get the catalog service required headers const headers = await getHeaders('cs'); // Make the API request const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify({ query: `query { products { items { name } } }` }) }); const data = await response.json(); // Process the response data } ``` ## Summary The Commerce configuration provides a structured approach to connecting your storefront to Adobe Commerce backends. By organizing settings into clear sections for endpoints, headers, analytics, plugins, and multistore configurations, you can easily customize your storefront for different Commerce environments. The configuration system supports Adobe Commerce (PaaS), Adobe Commerce as a Cloud Service (SaaS), and Adobe Commerce Optimizer, each with specific header requirements and authentication methods. For local development and testing, you can use a `config.json` file in your repository or the public config from the Configuration Service, with repository files taking precedence. Use the `getConfigValue` and `getHeaders` helper functions to access these settings in your Commerce blocks, ensuring consistent and maintainable integration with your Commerce backend. --- # Content delivery network (CDN) A CDN is a critical component of your Commerce Storefront. It is responsible for delivering content to your customers in the most efficient and secure way possible. PaaS only (Applies to Adobe Commerce on Cloud projects (Adobe-managed PaaS infrastructure) and on-premises projects only.) CDN features, usage, and provider details vary depending on your backend commerce implementation. By default, Adobe Commerce on Cloud projects use Fastly, while Adobe Commerce as a Cloud Service projects use an Adobe-managed CDN service. If necessary, you can configure your own CDN, also known as "Bring Your Own CDN" (BYO CDN). See https://www.aem.live/docs/byo-cdn-setup for required settings and vendor-specific setup instructions. > **Image optimization** Drop-in components automatically add image optimization parameters (such as width, height, and quality) to image URLs. CDN-based image optimization services rely on these parameters to optimize images. If you need to use different parameters or URL patterns for your CDN provider, you can override the defaults by using the [`setImageParamKeys`](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/initializer/#setimageparamkeysparams) method in the initializer configuration. For Adobe Commerce on Cloud projects, Fastly is included with your Adobe Commerce license and is the default CDN provider. The https://github.com/fastly/fastly-magento2 module exposes Fastly service configurations in the Commerce Admin. The remaining sections on this page provide instructions and guidance for configuring Adobe Commerce on Cloud projects with the Fastly module. It focuses on routing use cases, configuration, validation, and debugging. ## Routing The main difference between the use cases described in this section is whether all paths should be routed by default to Commerce or Edge Delivery Services. If you plan to use Edge Delivery Services only for your homepage, all paths should default to Commerce. However, if you plan to migrate parts of your storefront over time, the Edge Delivery Services origins are more sensible. Defaulting to Edge Delivery Services is generally ideal because you'll have to log in to the Commerce Admin and understand Fastly VCLs to add the path for routing in the “default to Commerce” scenario when adding a new page in Edge Delivery Services. Therefore, the VCL snippets below focus on the “default to Edge” use case. The following table provides a high-level comparison of the three routing options: | Topic | Full storefront | Luma Bridge | Homepage only | |--------------------------------|----------------------------------------------|--------------------------------------------------------|---------------------------------------------------------------| | **Routing logic** | Default to Edge Delivery Services | Default to Edge Delivery Services | Default to Commerce | | **Paths routed to Commerce** | Product images, GraphQL endpoints | Transactional pages, account page, REST endpoints, etc | All paths except those explicitly | | **Paths routed to Edge** | All other paths | Product catalog, content pages | Homepage | | **VCL snippets focus** | Default to Edge Delivery Services | Default to Edge Delivery Services | Default to Commerce | | **Optimization tips** | Avoid additional TLS handshake for API calls | Same as full storefront | Route resources (JS, CSS) to Edge Delivery Services | | **Common patterns** | Use CDN as proxy for Catalog Service | Same as full storefront | Move Edge code to subfolder, such as `aem` | :::note All paths for the Adobe Commerce Admin should be routed to Commerce. ::: ### Full storefront The entire storefront experience is delivered by Edge Delivery Services. Only some paths (for example, images and API calls) are routed to Commerce. * By default, paths are routed to Edge Delivery Services * Product images must be routed to Commerce * Commerce GraphQL endpoint must be routed to Commerce * CDN must proxy requests to your Catalog Service API endpoints (https://catalog-service.adobe.io/graphql and https://catalog-service-sandbox.adobe.io/graphql). :::tip These API endpoints impact largest contentful paint (LCP), so we want to avoid an additional TLS handshake. ::: ### Luma Bridge The product catalog and content pages are delivered by Edge Delivery Services. Transactional pages (for example, cart, checkout, and account) are delivered by Commerce. * [Luma Bridge](https://experienceleague.adobe.com/developer/commerce/storefront/setup/discovery/luma-bridge/) includes the same details as the full storefront option, plus the following: * Transactional pages (for example, cart, checkout, and account) are routed to Commerce, depending on the Luma Bridge implementation * Any additional endpoints (such as REST endpoints, `/customer/sections/load`) that are required by the Luma Bridge implementation are routed to Commerce ### Homepage only Only the homepage is delivered by Edge Delivery Services. All Commerce functionality is delivered by Commerce. * All paths are routed to Commerce by default * The paths that are routed to Edge Delivery Services are explicitly set * Resources (for example, JS and CSS) that are required by the Edge Delivery Servicesw pages must be routed to Edge Delivery Services :::note A common pattern is to move all Edge Delivery Services code into a subfolder called `aem`, and then route any path starting with `aem` to Edge Delivery Services. ::: For general information on setting up Fastly for Adobe Commerce and accessing the Adobe Commerce Admin, see https://experienceleague.adobe.com/en/docs/commerce-cloud-service/user-guide/cdn/fastly. ## Backend configuration The first step is to configure a backend for each origin/service that Fastly needs to route to, which includes the following: * Edge Delivery Services * Catalog Service GraphQL API endpoint * Default Adobe Commerce backend :::note The Fastly configuration instructions on this page are based on the https://github.com/fastly/fastly-magento2 module, which exposes Fastly service configurations in the Adobe Commerce Admin. ::: 1. Log in to the Adobe Commerce Admin. 1. Click **Stores** > **Settings** > **Configuration** > **Advanced** > **System** > **Full Page Cache** > **Fastly Configuration** > **Backend Settings** > **Create**. 1. Enter a name for the Edge Delivery Services backend. For example: `edge delivery`. 1. Enter the address for the Edge Delivery Services backend. For example: `main--aem-boilerplate-commerce--hlxsites.aem.live` 1. Click **Attach condition**. 1. Click **Create a new request condition**. 1. Enter a name for the condition. For example: `false`. 1. Enter `false` in the **Apply if** field. 1. Accept the default (`10`) in the **Priority** field 1. Click **Create**. 1. Select your new condition from the **Condition** drop-down list. ![Backend configuration for Commerce](https://experienceleague.adobe.com/developer/commerce/storefront/images/implementation/backend-config-example.png) :::caution Remember to also set this condition for the default Adobe Commerce backend. Otherwise, this condition can overwrite the changes made by the VCL conditions that you configure in the custom VCL snippets. You could also lose access to the Admin, which may require an Adobe Commerce Support ticket to resolve. ::: 1. Set Shielding to **(none)**. If you choose to use shielding, see the [shielding section](#shielding) for additional guidance. Repeat these steps for the Catalog Service GraphQL API endpoints (plus any other backends that you may require): * `https://catalog-service.adobe.io/graphql` * `https://catalog-service-sandbox.adobe.io/graphql` :::tip You can also rename the Adobe Commerce backend from the generated name to simply `commerce`. ::: ## VCL configuration Your routing setup requires the following VCL snippets: * `recv` * `pass` * `miss` * `fetch` * `deliver` See https://www.fastly.com/documentation/guides/vcl/using/ in the Fastly documentation for more information. The VCL snippets on this page are a starting point that you can alter or extend to fit your use cases. There are also examples of potential extended use cases listed further down the page. The main purposes of the VCL snippets are to: * Correctly set the backend based on request path * Set the recommended priority setting for each snippet to determine the order in which they are executed (lower numbers are executed first) * Ensure that the requests do not receive extra caching or headers if they are going to a non-Adobe Commerce backend (achieved by returning early) * Improve performance for non-Adobe Commerce backends by rewriting some features, like compression, that would be provided by the default Adobe Commerce VCL :::tip If you make a change or apply a new VCL snippet, it can take some time to propagate and apply to Fastly's system. It's a best practice to validate the latest change before continuing to other changes, otherwise you may introduce a bug and find it challenging to determine the root cause. If you need to rename a snippet, delete it first and then recreate it with the new name to avoid this https://github.com/fastly/fastly-magento2/issues/708. ::: To apply these custom VCL snippets to your Adobe Commerce Fastly CDN service, see https://experienceleague.adobe.com/en/docs/commerce-cloud-service/user-guide/cdn/custom-vcl-snippets/fastly-vcl-custom-snippets or use the https://github.com/fastly/fastly-magento2 Admin module. ### recv The following `recv` snippet does a simple redirect if `host` does not contain `www`. The snippet adds the string and then redirects to `https` using a https://www.fastly.com/documentation/reference/http/http-statuses/. This is done in place of the **Auto-redirect to Base URL** setting in the Commerce Admin. The steps in the [Commerce configuration](#commerce-configuration) section instruct you to set **Auto-redirect to Base URL** to **No** based on this snippet. **Type**: `recv` **Priority**: `4` ```txt if (req.http.host == "aemshop.net") { set req.http.host = "www.aemshop.net"; error 801; } ``` The following `recv` snippet has the following purpose: * Path-based routing for Edge Delivery Services, Catalog Service, and Adobe Commerce backends * Preparing the requests for the respective backends * Blocking access to non-production environments (non-production environments should not be indexed by search engines) **Type**: `recv` **Priority**: `100` ```txt unset req.http.x-commerce; # Catalog Service if (req.url.path ~ "^/cs-graphql$") { # Disable Commerce WAF as it can interfere with some queries set req.http.bypasswaf = "true"; # Forward to Catalog Service GraphQL set req.backend = F_catalog_service; set req.http.host = "catalog-service.adobe.io"; set req.url = regsub(req.url, "^/cs-graphql", "/graphql"); # Remove cookies unset req.http.Cookie; } else if (req.url.path ~ "^/(graphql|rest|oauth|media|checkout|customer|admin|static)($|/)") { # Commerce routes, e.g. for product images, core GQL/Rest, Luma Bridge (any paths which should load Commerce origin) set req.backend = F_commerce; set req.http.x-commerce = "true"; } else { # Block access to non-prod configs if (req.url ~ "^/configs-(stage|dev)\.json$") { error 404 "Not Found"; } # Everything else is considered Edge Delivery Services set req.backend = F_edge_delivery; # Restore accepted encoding set req.http.Accept-Encoding = req.http.Fastly-Orig-Accept-Encoding; if (req.url.path !~ "/media_[0-9a-f]{40,}[/a-zA-Z0-9_-]*\.[0-9a-z]+$" && req.url.ext !~ "(?i)^(gif|png|jpe?g|webp)$" && req.url.ext != "json" && req.url.path != "/.auth") { # trim the query string to improve caching set req.url = req.url.path; # replace some special characters with a dash because Edge doesn't support them set req.url = regsuball(req.url, "[()]", "-"); } } ``` ### pass In the `pass` snippet, we set the correct host URL to fetch from depending on the backend that was selected in the `recv` snippet. If it's not a request to Adobe Commerce, we skip (return `pass` will invoke `fetch`) the rest of the default `pass` VCL. **Type**: `pass` **Priority**: `30` ```txt if (req.backend == F_edge_delivery) { set bereq.http.Host = "main--aem-boilerplate-commerce--hlxsites.aem.live"; set bereq.http.X-BYO-CDN-Type = "fastly"; set bereq.http.X-Push-Invalidation = "enabled"; } if (!req.http.x-commerce) { return(pass); } ``` :::note Changes to the host header set here will require a Commerce Fastly cache purge due to https://docs.fastly.com/en/guides/manipulating-the-cache-key#redefining-the-cache-key. ::: ### miss Same as `pass`. **Type**: `miss` **Priority**: `30` ```txt if (req.backend == F_edge_delivery) { set bereq.http.Host = "main--aem-boilerplate-commerce--hlxsites.aem.live"; set bereq.http.X-BYO-CDN-Type = "fastly"; set bereq.http.X-Push-Invalidation = "enabled"; } if (!req.http.x-commerce) { return(fetch); } ``` :::note Changes to the Host header set here will require a Commerce Fastly cache purge due to https://docs.fastly.com/en/guides/manipulating-the-cache-key#redefining-the-cache-key. ::: ### fetch If fetching from a backend other than Adobe Commerce, skip the rest of the https://github.com/fastly/fastly-magento2/blob/fdd616cd0f945530e02e92e594ca00fd7990f557/etc/vcl_snippets/fetch.vcl because this is only relevant to Adobe Commerce backends. **Type**: `fetch` **Priority**: `30` ```txt if (!req.http.x-commerce) { unset beresp.http.Set-Cookie; return(deliver); } ``` ### deliver This comes from the Edge Delivery Services https://www.aem.live/docs/byo-cdn-fastly-setup#:~:text=Finally%20create%20a%20deliver%20snippet. **Type**: `deliver` **Priority**: `30` ```txt if (req.backend == F_edge_delivery) { unset resp.http.Age; if (req.url.path !~ "\.plain\.html$") { unset resp.http.X-Robots-Tag; } } ``` ## Optional configuration The following are optional configurations that can be added to the VCL snippets to handle specific use cases. ### URL rewrites Be aware that URLs in Edge Delivery Services can only contain a-z, 0-9, and the dash (`-`) character (see [Document naming restrictions](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/platform-limits/#document-naming-and-url-format)). You might need to create a CDN rule to rewrite or remove these characters. As an example, consider one possible case where this applies with login referrer links. Luma may redirect to a page like `/login/referrer/`. If the login page is implemented in Edge Delivery Services (with a Luma Bridge) and the base 64 string contains unsupported characters, this page will 404. Since the base64 part is only needed on the Edge Delivery Services client side to handle redirecting after a successful sign-in, this could be stripped for the request to the Edge Delivery Services backend. :::note The exact cases where this needs to be performed, and the steps required, depend on the implementation of the Luma Bridge as well as customizations of Adobe Commerce. ::: ### GeoIP The following example redirects users to a country-specific path based on their IP address. It uses the `country_to_store` table, which is a built-in Fastly table that maps country codes to store codes. You must install the Fastly CDN https://commercemarketplace.adobe.com/fastly-magento2.html or https://github.com/fastly/fastly-magento2 to use this example. Add the following to the `recv` snippet: ```txt # Enable the GeoIP feature, allowing the VCL to use GeoIP data for various purposes, such as redirecting users based on their geographic location. pragma optional_param geoip_opt_in true; # Snippet GeoIpRedirect declare local var.countryCode STRING; # Only apply redirection if the URL has no country-specific path # This snippet performs a GeoIP-based redirection. It checks if the request is made to the # "example.com" domain and if the URL path is either the root ("/") or empty. If these conditions are met, it # retrieves the user's country code based on their IP address using the `client.geo.country_code` variable. # It then looks up the corresponding store code from the `country_to_store` table. The user is redirected # to the appropriate country-specific URL. if ((req.http.host ~ "example.com") && ((req.url.path == "/") || (req.url.path == ""))) { # Retrieve the user's country based on IP address set var.countryCode = table.lookup(country_to_store, client.geo.country_code,"us"); set req.http.X-Redirect-Url = "https://" + req.http.host + "/" + var.countryCode; if (req.url.qs != "") { set req.http.X-Redirect-Url = req.http.X-Redirect-Url + "?" + req.url.qs; } error 701; } # End of Snippet GeoIpRedirect # This table maps country codes to store codes. It is used in the GeoIP redirect logic to determine # the appropriate store URL based on the user's geographic location. table country_to_store { "FR": "fr", "AU": "au", "NZ": "nz", "CA": "ca", ... } ``` ### Proxy RUM through the origin to avoid a TLS handshake Add a new backend called `hlx_rum` that points to `rum.hlx.page`. Also, change the https://www.aem.live/docs/rum implementation in `aem.js` to use a relative path to the origin instead of `rum.hlx.page`. Add the following to the `recv` snippet: ```txt if (req.url.path ~ "^/\.rum/") { # AEM Real User Monitoring set req.backend = F_hlx_rum; unset req.http.Cookie; } ``` ### Shielding If you enable shielding in a backend, then conditions like `req.backend == F_commerce` may not work. For this reason, the snippets above use a header like `http.x-commerce` that is set/unset, which is then used instead of the direct backend variable. ### Compression In `fetch`, you can add compression for non-Adobe Commerce resources by adding this snippet to the `fetch` VCL: ```txt if (!req.http.x-commerce) { unset beresp.http.Set-Cookie; if (beresp.http.content-type ~ "(text/|/json|/javascript)") { if (!beresp.http.Vary ~ "Accept-Encoding") { set beresp.http.Vary:Accept-Encoding = ""; } if (req.http.Accept-Encoding == "br") { set beresp.brotli = true; } else if (req.http.Accept-Encoding == "gzip") { set beresp.gzip = true; } } return(deliver); } ``` Since the `deliver` VCL skips all the subsequent steps, the default compression settings are skipped. The snippet above is the same as in the default VCL, but copied again to be applied to responses that are not going to the Adobe Commerce backend. ### Failover We can support automatic failover from Edge Delivery Services to Adobe Commerce Luma pages on a 404 in Edge Delivery Services. To do this, add the following in your custom `fetch` snippet: ```txt if (req.backend == F_edge && http_status_matches(beresp.status, "404")) { # See set beresp.http.Vary:restarts = ""; # Add restart to vary key set beresp.cacheable = true; # Errors are not cacheable by default, so enable them set beresp.ttl = 5s; # Set a short ttl so the unfindable object expires quickly set beresp.http.do_failover = "yes"; } ``` Then, in your custom `recv` snippet: ```txt if (req.http.try-alt-origin) { set req.backend = F_commerce; set req.http.x-commerce = "true"; set req.http.restarts = req.restarts; # Use restart value for vary key set req.http.Fastly-Force-Shield = "1"; } ``` Factly-Force-Shield may be required to turn on clustering (not related to shielding despite the name). Flow of EDS request: In `recv`, hits Edge Delivery Services case, then goes to `miss`, then goes to `fetch`, `fetch` returns 404 and sets retry (as snippet above), and `deliver` calls `restart`. If we are using Fastly shielding, we need to have `fastly.ff.visits_this_service == 0` in the `deliver` snippet, before `restart`, otherwise it can be that ESI doesn't work. ```txt if (fastly.ff.visits_this_service == 0 && !req.http.try-alt-origin && resp.http.do_failover == "yes") { set req.http.try-alt-origin = "1"; set req.url = req.http.Magento-Original-URL; return (restart); } ``` It is not recommended to handle all paths that need to be routed to Luma like this, but still hardcode those that are known, in order to reduce load of 404s on Edge Delivery Services. ### API Mesh API Mesh has a header size limit. You must remove third-party cookies if you're using API mesh. Add this in the graphql section of `recv` snippet: ```txt if (req.http.Cookie) { # Remove all 3rd-party cookies # API Mesh has a header size limit set req.http.Cookie = ";" + req.http.Cookie; set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";"); set req.http.Cookie = regsuball(req.http.Cookie, ";(PHPSESSID|X-Magento-Vary|form_key|private_content_version|mage-messages|persistent_shopping_cart|fastly_geo_store)=", "; \\1="); set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", ""); set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", ""); if (req.http.cookie ~ "^\\s*$") { unset req.http.cookie; } } ``` ### Branch names on staging You can enable the use of Edge Delivery Services branches on a staging URL (for example, `branch1.my-staging.com`). Use regex to get the domain name with this addition to the `miss` snippet: ```txt if (req.backend == F_edge) { if (req.http.Host ~ "^([^.]+)\\.([^.]+)\\.example\\.com$") { set bereq.http.Host = re.group.1 + "--your-eds-url.aem.page"; } else { set bereq.http.Host = "your-eds-url.aem.live"; } set bereq.http.X-BYO-CDN-Type = "fastly"; set bereq.http.X-Push-Invalidation = "enabled"; } ``` In the domains configuration, you must use a wildcard. Since the Commerce Admin doesn't let you do this, you must do it using the Fastly CLI (which also shows the wildcard URL in the Admin). ### Maintenance mode The storefront has no built-in maintenance mode. However, you can add a custom VCL snippet similar to the one below to redirect all incoming requests to your maintenance page. This snippet excludes requests to the `/maintenance` path, so the maintenance page itself stays reachable. ```txt if (req.url !~ "^/maintenance") { error 302 "https://yoursite.com/maintenance"; } ``` The `error 302` instruction issues an HTTP 302 (temporary redirect) to the URL you specify. Replace `https://yoursite.com/maintenance` with the URL of your maintenance page. Add this as a `recv` snippet with a priority lower than `100`, for example `5`, so it runs before the main routing logic. To add the snippet, see https://experienceleague.adobe.com/en/docs/commerce-cloud-service/user-guide/cdn/custom-vcl-snippets/fastly-vcl-custom-snippets in the Fastly documentation, or use the https://github.com/fastly/fastly-magento2 Admin module in the Commerce Admin. Work with your Fastly administrator to confirm the priority before applying the snippet. > **API and GraphQL traffic** This redirect applies to all incoming requests, including GraphQL and API Mesh requests. To end maintenance mode, remove or disable the snippet in your Fastly configuration. ### Multiple set-cookie headers workaround There's a known issue with multiple `set-cookie` headers in API Mesh. See the following snippet for a workaround ```txt if (req.http.x-mesh == "true") { # There's a bug in API MESH that combines multiple set-cookie headers # Let's remove these cookies from the response declare local var.ignored BOOL; set var.ignored = setcookie.delete_by_name(beresp, "private_content_version"); set var.ignored = setcookie.delete_by_name(beresp, "form_key"); set var.ignored = setcookie.delete_by_name(beresp, "authentication_flag"); set var.ignored = setcookie.delete_by_name(beresp, "dataservices_customer_id"); set var.ignored = setcookie.delete_by_name(beresp, "dataservices_customer_group"); set var.ignored = setcookie.delete_by_name(beresp, "dataservices_cart_id"); return (pass); } ``` :::note `x-mesh` header is just an example. It would be added in API Mesh to use as determination in routing of VCL (for example, `x-commerce-bypass-fastly-cache`). ::: ## Commerce configuration If you are using the VCL to do the APEX redirect (`aemshop.net` → `www.aemshop.net` with 801 error), you should do two things in the Adobe Commerce backend: 1. Disable `auto-redirect`. 1. Set the `base_url` and `secure_base_url` settings to your domain, including `www`. Follow these steps: 1. Log in to the Adobe Commerce Admin. 1. Click **Stores** > _Settings_ > **Configuration** > **General** > **Web**. 1. Expand the **URL options** section. 1. Set **Auto-redirect to Base URL** to **No**. 1. Expand the **Base URLs** section. 1. Set the **Base URL** and **Secure Base URL** to your domain, including `www`. For example: `https://www.aemshop.net/` 1. Expand the **Base URLs (Secure)** section. 1. Set the **Secure Base URL** to your domain, including `www`. For example: `https://www.aemshop.net/` 1. Click **Save Config**. ## Edge Delivery Service configuration To obtain a purge API token for Fastly, you must contact Adobe Commerce Customer Support. Then, follow the instructions in https://www.aem.live/docs/byo-cdn-fastly-setup#setup-push-invalidation-for-fastly. The remaining configuration described on this page is already taken care of by applying the VCL snippets above. ## Validation To validate your CDN setup, use `curl` requests to check expected responses from the paths you have configured. ### Content encoding, surrogate key, and cache Validate that surrogate key, cache hits, and content encoding are working as expected by requesting an Edge Delivery Services-served asset from your Commerce domain. Ensure you are checking a warm cache by making the request at least twice. The following examples are to a warmed cache. Here is an example of validation against an Adobe staging environment: ```bash curl -sI -H 'Fastly-Debug: 1' https://www.aemshop.net/scripts/aem.js | grep 'x-cache\|surrogate-key\|cache-control' ``` ```txt content-encoding: gzip surrogate-key: develop--aem-boilerplate-commerce--hlxsites develop--aem-boilerplate-commerce--hlxsites_code E3hjdgev7F5OyPUD x-cache: MISS, HIT, HIT, HIT x-cache-hits: 0, 37, 1, 0 ``` * `content-encoding`: Should be `gzip` or `br` for things like JS assets and HTML files, which should be encoded from origin. * `surrogate-key`: Should not be `text`. If the value is `text`, make sure you have correctly configured the `fetch` VCL snippet to return `deliver` for Edge Delivery Servicespaths. The reason for this validation step is that the https://github.com/fastly/fastly-magento2/blob/fdd616cd0f945530e02e92e594ca00fd7990f557/etc/vcl_snippets/fetch.vcl#L113 sets this. This overwrites the Edge Delivery Services surrogate key, which is required for cache invalidation to work correctly when a page is re-published. * `x-cache`: Should contain `HIT` entries. The last entry should be `HIT` (`MISS`, `HIT`, `MISS`, `HIT`) is ok. Also, make sure that you validate `gzip` encoding is applied to HTML pages: ```bash curl -sI -H 'accept-encoding: gzip, deflate, br, zstd' https://www.aemshop.net | grep content-encoding ``` ```txt content-encoding: gzip ``` ```bash curl -sI -H 'accept-encoding: gzip, deflate, br, zstd' https://www.aemshop.net/index.plain.html | grep content-encoding ``` ```txt content-encoding: gzip ``` Validate the CDN configuration with the BYOCDN https://tools.aem.live/. Additionally, preview and publish a document and validate that the changes are correctly reflected. ```bash curl -Is --http2 https://www.aemshop.net | grep 'HTTP/2' ``` ```txt HTTP/2 200 ``` Validate that all requests are using `HTTP/2` or `HTTP/3` connections. ```bash curl -Is -L https://aemshop.net | grep location ``` ```txt location: https://www.aemshop.net/ ``` To validate the APEX redirect, observe the location header returned by a request to the domain without `www`. ### Image optimization Validate that images are encoded with the expected format. If you get back `image/jpeg` this indicates an issue, probably Commerce Fastly is rewriting the content type header. You'll need to validate the VCL snippets. To validate Edge Delivery Services, ensure you check some content expected to be served from the Edge Delivery Services origin (such as the hero banner). ```bash curl -sI -H "Accept: image/webp" 'https://www.aemshop.net/media_1b6ab8fa5166c1d9ab57a109f4f97b8e950a3ed84.png?width=2000&format=webply&optimize=medium' | grep 'content-type' ``` ```txt content-type: image/webp ``` To validate Commerce, ensure you check some content expected to be served from the Adobe Commerce origin (such as a product image). ```bash curl -sI -H "Accept: image/webp" 'https://www.aemshop.net/media/catalog/product/adobestoredata/ADB150.jpg?auto=webp&quality=80&crop=false&fit=cover&width=960&height=1191' | grep 'content-type' ``` ```txt content-type: image/webp ``` ### Commerce cache Ensure that GraphQL `GET` requests result in cache HITs. ```bash curl -sI 'https://www.aemshop.net/graphql?query=query+STORE_CONFIG_QUERY+%7B+storeConfig+%7B+minicart_display%7D+%7D' | grep x-cache ``` ```txt x-cache: HIT x-cache-hits: 1 ``` ### Commerce base URL Ensure that the base URL change is propagated to Catalog Service. You can do this with a query against your Catalog Service API to verify that the URLs for product images contain the APEX domain, including `www`. This is to prevent a redirect and subsequent load for images not including the APEX domain. ![Commerce base URL](https://experienceleague.adobe.com/developer/commerce/storefront/images/implementation/commerce-base-url.png) ## Troubleshooting > **It** Fastly VCL and CDN configuration can be a bit overwhelming at first. Here's a collection of troubleshooting steps that might help you if you have problems. ### Fastly API quick reference With your Fastly API Token you can make requests against the Fastly API for information or data that may be otherwise inaccessible. You can find your API Token in your Commerce Admin view under **Stores** > **Settings** > **Configuration** > **Advanced** > **System** > **Full Page Cache** > **Fastly Configuration**. Here's a quick reference of things you can do with your API Token. See https://www.fastly.com/documentation/reference/api/ for more details. ```bash # retrieve active version curl -H "Fastly-Key: API_TOKEN" "https://api.fastly.com/service/SERVICE_ID/version" # retrieve full, generated VCL curl -H "Fastly-Key: API_TOKEN" "https://api.fastly.com/service/SERVICE_ID/version/VERSION/generated_vcl" # retrieve all snippets curl -H "Fastly-Key: API_TOKEN" "https://api.fastly.com/service/SERVICE_ID/version/VERSION/snippet" ``` ### Missing snippets Fastly maintains a "version" of your snippets remotely on their servers. These versions are never pulled "down" to your Commerce environment, but can be "pushed" from your environment to Fastly. Unintentional local changes can cause things to become out of sync. For example, renaming a snippet actually creates a new snippet with the new name, and does not delete the old one. You should not rename snippets unless you must. See https://github.com/fastly/fastly-magento2/issues/708 for more information. Your environment stores custom VCL snippets in the `var/vcl_snippets_custom/` directory. If you delete that directory or its contents, you will not be able to view, edit, or modify the snippets, even though they remain active in Fastly." To resolve this, you should use the Fastly API to "pull" all snippets and then place them back in that folder (with permission **775**). The following table details how to construct the file correctly: | `var` directory filename | Admin Panel Field | Fastly Fields | |--------------------------------|----------------------------------------------|--------------------------------------------------------| | _recv_100_snippetrecv.vcl_ | Type: _recv_ Priority: _100_ Name: _snippetrecv_ | Type: _recv_ Priority: _100_ Name: _magentomodule_snippetrecv_ | --- # CORS Setup Cross-Origin Resource Sharing (CORS) is a browser security feature that restricts web pages from making requests to a different domain than the one serving the page. CORS is required when your storefront runs on a different domain or port than your Adobe Commerce backend. PaaS only > **Production recommendation** CORS should be a last resort for production. Prefer same-origin delivery of the storefront and API (for example, via Fastly VCL) to avoid CORS entirely. But during development, it's a common scenario for storefronts to run on a different domain or port than the Adobe Commerce backend, so CORS is required. Without CORS, browsers will block cross-origin requests and you'll see errors like the following in the browser console: ```txt Access to fetch at 'https://commerce.example.com/graphql' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present ``` ## Quick test: Is CORS working? Adobe Commerce uses `/graphql` as the GraphQL endpoint, and this is the primary endpoint that requires CORS configuration. If you already have CORS configured, test it by replacing `YOUR_STOREFRONT_URL` with your actual frontend URL: ```bash curl -I -X OPTIONS https://your-commerce-backend.com/graphql \ -H "Origin: YOUR_STOREFRONT_URL" \ -H "Access-Control-Request-Method: POST" ``` Look for these headers in the response: - `Access-Control-Allow-Origin: YOUR_STOREFRONT_URL` (or `*` if using a wildcard) - `Access-Control-Allow-Methods: GET, POST, OPTIONS` If you see these headers with your storefront URL, CORS is configured correctly. If not, follow the setup steps below. ## Quick steps ### 1. Add CORS support To add CORS headers to your Adobe Commerce GraphQL endpoints, you have several options: - **Server configuration**: Configure CORS headers at the web server level (https://enable-cors.org/server_nginx.html, https://enable-cors.org/server_apache.html) - **Third-party module**: Use a community module such as https://github.com/graycoreio/magento2-cors - **Custom module**: Implement a custom Adobe Commerce module to add CORS headers > **Support scope** Third-party modules and custom module implementations are outside the scope of Adobe Commerce support. For production environments, Adobe recommends server-level configuration or same-origin delivery to avoid CORS requirements entirely. Refer to your chosen implementation's documentation for specific configuration steps. ### 2. Configure CORS settings Configure your CORS implementation to allow requests from your storefront domain(s) to your Adobe Commerce GraphQL endpoint. At minimum, you'll need to configure: - **Allowed origins**: Your storefront domain(s) (for example, `http://localhost:3000`, `https://your-storefront.com`) - **Allowed methods**: `GET`, `POST`, `OPTIONS` - **Allowed headers**: `Content-Type`, `Authorization`, `X-Requested-With` Refer to your chosen implementation's documentation for specific configuration steps. ### 3. Test your CORS configuration Open your storefront in a browser and check the Network tab in DevTools: - Look for preflight `OPTIONS` requests to your GraphQL endpoint - Verify the response includes headers like: - `Access-Control-Allow-Origin: ` (or `*` if using a wildcard) - `Access-Control-Allow-Methods: GET, POST, OPTIONS` - `Access-Control-Allow-Headers: Content-Type, Authorization` If configured correctly, your GraphQL requests should succeed without CORS errors. ## Common pitfalls > **Avoid these common mistakes** **Forgetting to clear cache:** After changing CORS settings, always run `bin/magento cache:flush`. A stale cache is the primary cause of "it's not working" reports. **Using `*` with credentials:** You cannot use a wildcard `*` origin when `Allow Credentials` is enabled. Browsers will block the request. **Missing the `OPTIONS` method:** All CORS requests start with an OPTIONS preflight request. If you forget to include `OPTIONS` in **Allowed Methods**, all CORS requests will fail. ## Next steps If you encounter issues or need detailed configuration guidance, see [CORS Troubleshooting](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/cors-troubleshooting/) ## References - https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS - MDN Web Docs on Cross-Origin Resource Sharing - https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors - MDN documentation on common CORS error messages --- # CORS Troubleshooting This guide helps you diagnose and resolve CORS issues with Adobe Commerce GraphQL endpoints. PaaS only ## Common CORS errors The following are standard CORS errors enforced by browser security policies. These error messages appear in the browser console (DevTools). ### No 'Access-Control-Allow-Origin' header ```txt Access to fetch at 'https://commerce.example.com/graphql' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. ``` #### Cause CORS headers are not being sent by your Commerce backend. This could mean: - CORS support is not configured - The origin is not in the allowed list - The configuration cache needs to be cleared #### Solution - Verify CORS is configured. See [CORS Setup](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/cors-setup/) if not configured yet. - Add your storefront origin to the allowed origins list - Clear the cache: `bin/magento cache:flush` ### Origin not allowed ```txt Access to fetch at 'https://commerce.example.com/graphql' from origin 'http://localhost:3000' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a value 'https://storefront.example.com' that is not equal to the supplied origin. ``` #### Cause The requesting origin is not in the allowed origins list. #### Solution Add the exact origin (including protocol and port) to the Allowed Origins configuration. ### Credentials flag issue ```txt Access to fetch at 'https://commerce.example.com/graphql' from origin 'http://localhost:3000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. ``` #### Cause You have `Allow Credentials` enabled but are using `*` for allowed origins. #### Solution Replace `*` with specific origins when credentials are required. ### Common issues If you encounter CORS errors after installing `adobe-commerce/storefront-compatibility`, verify that your CORS implementation is compatible with Adobe Commerce Storefronts. If using the graycore module, ensure v2.x or later is installed. ## Debug checklist ### Verify CORS is configured If using a CORS module, check that it's installed and enabled: ```bash # List all enabled modules bin/magento module:status | grep -i cors ``` If using a custom CORS implementation, verify your code is deployed and active. ### Verify CORS configuration Check your CORS configuration and confirm: - Your storefront origin is in the Allowed Origins list - Origins match exactly (including protocol and port) - No trailing slashes in origins - Allowed Methods includes `OPTIONS` - Allowed Headers includes all headers your storefront sends See [CORS Setup](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/cors-setup/) for configuration details. ### Inspect browser network traffic Open DevTools > Network tab and look for: #### Preflight OPTIONS request ```txt Request URL: https://commerce.example.com/graphql Request Method: OPTIONS ``` Expected response headers: ```txt Access-Control-Allow-Origin: http://localhost:3000 Access-Control-Allow-Methods: GET, POST, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization Access-Control-Max-Age: 86400 ``` #### Actual GraphQL POST request ```txt Request URL: https://commerce.example.com/graphql Request Method: POST ``` Expected response headers: ```txt Access-Control-Allow-Origin: http://localhost:3000 Access-Control-Allow-Credentials: true ``` ### Check server logs If CORS headers are present but requests still fail: ```bash tail -f var/log/system.log tail -f var/log/exception.log ``` Look for PHP errors, GraphQL exceptions, or module conflicts. ### Clear all caches After any configuration changes: ```bash bin/magento cache:flush bin/magento cache:clean ``` Some CORS configurations may also be cached in Varnish/Fastly if applicable. ## Distinguishing CORS errors from server-side errors When debugging, it's important to distinguish CORS policy failures from server-side GraphQL exceptions. Here's an example of a server-side error that might be mistaken for a CORS issue: ```txt { "message": "Internal server error", "extensions": { "debugMessage": "Magento\\InventoryConfiguration\\Model\\IsSourceItemManagementAllowedForProductType\\Interceptor::execute(): Argument #1 ($productType) must be of type string, null given" } } ``` This indicates a PHP/GraphQL issue (inventory configuration or compatibility) and not a missing Access-Control-Allow-Origin header. ## Edge cases and special scenarios ### Docker and containerized environments When running Commerce in Docker, you may need to add multiple origin variations: ```txt http://localhost:3000 http://127.0.0.1:3000 http://host.docker.internal:3000 ``` > **Docker hostname variations** Docker networking can cause your browser to use different hostnames for the same service, so you need to add all variations. ### Multiple storefronts For multiple storefronts accessing the same Commerce backend, add each origin separately: ```txt https://storefront-us.example.com https://storefront-eu.example.com https://storefront-asia.example.com ``` Each storefront is treated as a separate origin and must be explicitly allowed. ## Production best practices The recommended production approach is to avoid CORS entirely by serving both the storefront and the backend from the same domain using a CDN proxy. ### Same-origin architecture (recommended) Serve both your storefront and Commerce backend from the same domain: - Storefront: `https://example.com` → CDN serves static assets - GraphQL API: `https://example.com/graphql` → CDN proxies to Commerce backend This requires configuring your CDN to proxy `/graphql` requests to your Commerce backend. **Benefits:** - No CORS complexity - Better performance (fewer preflight requests) - Simpler security model - Better caching control **Implementation approaches:** 1. **Fastly VCL routing:** Configure Fastly to route GraphQL requests to your Commerce backend using VCL (Varnish Configuration Language). The VCL detects requests to `/graphql` and proxies them to your backend origin while serving other requests from your storefront origin. 2. **Cloudflare Workers / CDN Edge Functions:** Use edge functions to route API requests to your backend dynamically. 3. **Reverse proxy (Nginx/Apache):** Configure your web server to proxy `/graphql` requests to the backend. **Note:** This approach requires CDN/infrastructure expertise. If you don't have these resources, use CORS configuration instead. ### CORS configuration If you must use different domains (for example, `storefront.example.com` → `commerce.example.com`), use CORS configuration. Use specific allowed origins for single production domains, or a wildcard `*` when you have multiple dynamic preview URLs (such as Edge Delivery Services branch previews). ## References ### CORS standards and specifications - https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS - MDN Web Docs on Cross-Origin Resource Sharing - https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors - MDN documentation on common CORS error messages - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#cors - Complete list of CORS-related HTTP headers - https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request - How browsers use OPTIONS requests for CORS ### Adobe Commerce - https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/common-cli-commands.html - Official documentation for Magento CLI commands used in this guide - https://developer.adobe.com/commerce/webapi/graphql/ - GraphQL API documentation ### Additional tools and platforms - https://getcomposer.org/doc/ - PHP dependency manager used for module installation - https://docs.docker.com/network/ - Understanding Docker hostname resolution for CORS origins - https://developer.fastly.com/reference/vcl/ - For implementing same-origin architecture with Fastly CDN routing --- # Gated content If your site is not managed by Adobe Commerce, you might want to restrict access to certain content, such as videos, images, or pages on your site. This content resides on the edge and cannot be controlled using customer segments or other Commerce functionality. You can restrict access to these non-Commerce assets using gated content. ## Big picture There are several things to know to implement this feature. * Gated content is displayed only if the customer is logged in and their authentication token is valid. Anonymous users cannot access gated content. * You must define the gated content. In most cases, you define the path to the gated content in the `header` document. This file contains a list of URLs that are gated and defines a key/value pair for your custom code to evaluate. (You can also hardcode URLs in your custom code, but this information is easier to maintain in the spreadsheet.) The spreadsheet contains three columns: * `url` - The path to the gated content. Wildcards are supported. * `key` - Any key that you want to use to evaluate whether the content is gated. * `value` - `True` or other value to be evaluated. * You must create custom code that evaluates whether access to the content is gated. CDNs typically do not provide the ability to evaluate whether a user is authenticated. As a result, you must implement an auxiliary service, such as Fastly Compute, Amazon Lambda, or Cloudflare workers to perform the evaluation. This code must be able to send an authentication request to Adobe Commerce and process the result. The request is typically a simple GraphQL query, such as the `customer` query, but you can use any query or mutation that checks if the customer is authenticated. ## Workflow The following diagram illustrates the workflow when gated content is enabled. ![Gated content workflow](https://experienceleague.adobe.com/developer/commerce/storefront/images/gated-content.svg) *Gated content workflow* The shopper requests a page on the storefront. The request is sent to the CDN (Fastly in this case), which then routes it to Edge Delivery Services. Edge Delivery Services returns the requested document with headers defined in the `header` document. Fastly Compute checks whether the requested page is considered gated content based on the headers returned by Edge Delivery Services or by comparing it with a hardcoded list of gated content. If the header defined in the `header` document is not present, or if the requested URL is not in the hardcoded gated content list, the request is processed normally. If the requested page is gated, the Fastly Compute code uses the customer token to construct an authorization request to Adobe Commerce. If the customer is authenticated, the request is allowed to pass through. If the customer is not authenticated, the request is redirected to a fallback page, such as a login page. ## Example implementation You can [download a sample solution](https://experienceleague.adobe.com/developer/commerce/storefront/samples/fastly-compute-sample.zip) that provides an outline of how to implement gated content using Fastly Compute. This sample is based on Fastly's https://www.fastly.com/documentation/solutions/starters/compute-starter-kit-javascript-default/. > **Important** Adobe does not support this example implementation. It is provided as a reference only. The README provides an overview of the solution and how to install the starter kit. The `index.js` file contains a Fastly Compute@Edge script that handles incoming HTTP requests and performs user authentication for specific protected URLs. --- # Overview Setting up an Adobe Commerce Storefront project is similar to other Edge Delivery Services projects. The main difference is that you need to connect your storefront to your Adobe Commerce backend. ## Big picture Launching a headless Adobe Commerce Storefront on Edge Delivery Services requires some basic setup before you do any custom development. Adobe recommends starting with the https://github.com/hlxsites/aem-boilerplate-commerce to simplify the process. The Commerce boilerplate GitHub repository is a fork of the https://github.com/adobe/aem-boilerplate. It includes additional code specifically for Commerce use cases. :::note[Note] All implementation guidance in this documentation is based on the Commerce boilerplate. ::: The [Create your storefront tutorial](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/) provides all the information that you need to quickly set up a starter project that uses a pre-configured sample Adobe Commerce backend. After you complete the tutorial, you can [connect](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) your own Adobe Commerce backend to the project. Here's an overview of the process: ![Project setup process.](https://experienceleague.adobe.com/developer/commerce/storefront/images/implementation/setup-overview-v2.svg) *Project setup process.* 1. **Storefront configuration**: Connect your Edge Delivery Services storefront to your Adobe Commerce backend. 1. **CDN configuration**: Set up the content delivery network (CDN) to deliver your project. 1. **Storefront compatibility package**: Install the Storefront Compatibility Package to enable drop-in component functionality. --- # Multistore setup **Multistore** enables you to run multiple storefronts from a single codebase and content repository. Each storefront can serve different languages, currencies, regions, or brands while sharing core code and maintaining unified content delivery. ## What is multistore? Multistore is useful for: - **Localization**: Serving different languages and currencies (for example, English/US, French/Canada) from the same site, with localized content and Commerce data. - **Multiple Brands or Regions**: Operating several brands or regional storefronts, each with their own configuration, but sharing core code and content. There are two main approaches to multistore: 1. **Multiple Domains**: Each domain points to a different commerce backend or configuration, but shares the same code and content. 2. **Subfolders (Root Folders)**: A single domain with subfolders (such as `/en/`, `/fr/`) for each locale or store. Each subfolder can have its own localized content and Commerce configuration. **Key features:** - **Editorial and Commerce Localization**: Editorial content (pages, blocks) and commerce data (products, prices, currencies) are both localized. - **Automated Translation Tools**: Tools are provided to automate translation of content and manage updates across languages. - **Configurable Headers**: Storefronts can send different headers to the commerce backend to select the correct store, language, and currency. - **Store Switcher**: Out-of-the-box UI enables users to switch between stores and locales. - **Automatic Link Management**: Links are automatically updated to point to the correct localized version as users navigate. ## How it works Adobe Commerce uses a https://experienceleague.adobe.com/en/docs/commerce-admin/start/setup/websites-stores-views to manage multiple stores within a single instance. This structure consists of three levels: websites, stores, and store views. Localization is managed at the store view level, allowing merchants to present the store in different languages and apply the proper currency. Each store or locale is defined in configuration files, specifying which headers to send to the commerce backend, which content folders to use, and how to structure localized content. The content hierarchy follows https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/edge-delivery/overview best practices, emphasizing a single-tier structure. ### Content organization Content is organized primarily by language. US English files should be placed in the `en` directory of the project root. All other language- and region-specific files should be placed in a directory that is named in the format `language-code-region-code` (using hyphens). For example, the `/en/` directory contains data for the US market, while the `/en-ca/` directory contains data for Canada. French content should be placed in directories with names like `fr-fr` for France and `fr-ca` for Canada. - **/en/** _-- English store (Default root language)_ - placeholders/ _-- Stores JSON files for text and UI components for the English US store._ - index _-- The home page of the English US store._ - store-switcher _-- Manages the list of stores and their URL for the English US store._ - **/en-ca/** _-- English Canadian Store_ - placeholders/ _-- Defines JSON files for text and UI components for the Canadian store._ - index _-- The home page of the English Canadian store._ - store-switcher _-- Manages the list of stores and their URL for the Canadian store._ ### Store-specific files Each store view requires specific files to define the customer experience: **Store switcher**: Each store view has its own `store-switcher` document file (called a fragment). This fragment provides the list of stores to select from by language. The store-switcher component renders a button in the footer that opens a modal containing the stores listed in this document. **Content files**: The content files provide the structure of the pages served to your shoppers. They are located in the store view directories and contain the content for each store view. These files are documents hosted on da.live (Document Authoring environment), SharePoint, or Google Docs that are used to generate the store view pages. **Placeholders**: JSON files that define reusable variables for text and UI components. Each drop-in component has its own placeholder file—`cart.json`, `checkout.json`, `pdp.json`. These files replace variables in your content files with text values. **Merchant guidance:** For translating placeholder files, see [Commerce localization tasks](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/quick-start/content-localization-commerce-tasks/). **Developer guidance:** For technical implementation details, see [Labeling and Localizing Drop-In Components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/labeling/). ## Prerequisites Before implementing multistore functionality, ensure you have: - Your storefront repository linked to Document Authoring environment (da.live) via Edge Delivery Services - Access to Adobe Commerce Admin for https://experienceleague.adobe.com/en/docs/commerce-admin/stores-sales/site-store/store-views - Your [Storefront Configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) set up and pointing to the Adobe Commerce instance - Familiarity with https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/edge-delivery/document-authoring/authoring - Understanding of [Storefront Configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) - Knowledge of https://experienceleague.adobe.com/en/docs/commerce-admin/config/scope-change --- ## Implementation walkthrough This walkthrough demonstrates the complete process from initial planning to final validation, using a real-world scenario where the Acme brand expands from the United States to the Canadian market with two new locales: Canadian English (`en-ca`) and Canadian French (`fr-ca`). ### 1. Verify environment setup Before implementing multistore functionality, confirm your environment is properly configured. 1. **Verify the code sync bot** is installed on your repository. This automatically configures your site's content pointer to the Document Authoring environment at da.live. 2. **Verify the `config.json` file** (or Configuration Service) is connected to your Adobe Commerce instance to enable content preview, publishing, and editing. > **Environment setup** This verification step is crucial as it ensures you can properly preview and publish content for your new store views. ### 2. Create the content folder structure Set up the folder structure in your Document Authoring environment to support the new store views. 1. **Navigate to the connected content folder** in da.live. 2. **Create the Canadian English folder**: - Select "New folder" - Name it `en-ca` 3. **Create the Canadian French folder**: - Select "New folder" - Name it `fr-ca` 4. **Copy content from the default root folder** into each new store view folder: - Select all content from the `/en/` folder - Copy the content - Navigate to the `en-ca` folder and paste - Repeat for the `fr-ca` folder > **Folder naming** **Standard format:** Use hyphens for locale folders (`en-ca`, `fr-ca`) following the `language-code-region-code` pattern. This aligns with: - ISO 639-1 (language codes) and ISO 3166-1 (country codes) - Web URL conventions - Adobe/AEM.live best practices Ensure folder names match exactly with Adobe Commerce store view codes and can be used in web addresses. ### 3. Configure Adobe Commerce store views Create the corresponding store views in Adobe Commerce Admin to match your content folder structure. 1. **Access Adobe Commerce Admin** and navigate to Stores → Settings → All Stores. 2. **Create the Canadian English store view**: - Follow the store view creation process - Configure as: one website, one store, one store view - Set the store view code to match your folder name (`en-ca`) 3. **Create the Canadian French store view**: - Repeat the process for the French locale - Set the store view code to match your folder name (`fr-ca`) > **Store view codes** The store view codes in Adobe Commerce Admin must exactly match the folder names created in da.live for proper integration. ### 4. Update folder mapping Product Detail Pages (PDP) are mapped to a single document at `products/default`. When adding a new store view, you must add a corresponding folder mapping for the new URLs using the **Config Service API**. #### Step 1: Authenticate Go to `https://admin.hlx.page/login`, then go to your selected IDP login URL, and sign in. Retrieve the auth token from your browser's cookie storage (available in Developer Tools → Application → Cookies). Use that token to authenticate Admin API requests, with the headers and request format described in the https://www.aem.live/docs/config-service-setup and the https://www.aem.live/docs/admin.html. #### Step 2: Get the current folder configuration ```http GET https://admin.hlx.page/config/{ORG}/sites/{SITE}/folders.json Authorization: token {YOUR_AUTH_TOKEN} ``` The response is a JSON object mapping URL path prefixes to template document paths, for example: ```json { "/products/": "/products/default" } ``` If you don't receive a response, you may not have folder mapping yet. In that case, move on to step 3 and create the JSON object manually. #### Step 3: Modify and POST the updated configuration Add your new store view mappings and POST the complete object back to the same endpoint: ```http POST https://admin.hlx.page/config/{ORG}/sites/{SITE}/folders.json Authorization: token {YOUR_AUTH_TOKEN} Content-Type: application/json ``` ```json { "/en/products/": "/en/products/default", "/en-ca/products/": "/en-ca/products/default", "/fr-ca/products/": "/fr-ca/products/default" } ``` :::caution The POST **replaces** the entire `folders.json` object. Always start from the current GET response and include all existing mappings alongside your new ones. ::: For full details, see the https://www.aem.live/docs/config-service-setup and the https://www.aem.live/docs/admin.html. ### 5. Update site configuration You must update your [storefront config](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) to include configuration overrides for the new store view. In this file, you'll define store-view specific settings, such as service headers and analytics values. The `key` of each overridden object must match the root folder path of the corresponding store view. These values will be merged into the default configuration. ```json { "public": { "default": { // default configuration }, "/en/": {}, // inherits the default "/en-ca/": { "headers": { "all": { "Store": "en-ca" }, "cs": { "Magento-Store-Code": "ca-store", "Magento-Website-Code": "base", "Magento-Store-View-Code": "en-ca" } }, "analytics": { // overrides values from the default analytics configuration } }, "/fr-ca/": { "headers": { "all": { "Store": "fr-ca" }, "cs": { "Magento-Store-Code": "ca-store", "Magento-Website-Code": "base", "Magento-Store-View-Code": "fr-ca" } } } } } ``` **Headers configuration details:** - **`Magento-Store-Code`**: Must match the store code from the Commerce Admin - **`Magento-Website-Code`**: Typically "base" for single-website setups - **`Magento-Store-View-Code`**: Must match the store view code from the Admin The `analytics` section contains the Adobe Commerce environment variables for the store view. These variables are used by the Analytics API. For more details, see [Analytics](https://experienceleague.adobe.com/developer/commerce/storefront/setup/analytics/instrumentation/). > **Configuration Service** If you are using the Config Service, read the https://www.aem.live/docs/admin.html#tag/siteConfig/operation/updateConfigSite to learn how to publish your updates to the Config Service for your site. ### 6. Configure store switcher Create a `store-switcher` file in each store view root folder containing a bulleted list. Each line must define the display name of your store with an active link to the store. ```text **Select a store:** - Canada (CAD) - [Canada (EN)](https://main--aem-boilerplate-commerce--hlxsites.aem.page/en-ca/#nolocal) - [Canada (FR)](https://main--aem-boilerplate-commerce--hlxsites.aem.page/fr-ca/#nolocal) - United States (USD) - [United States (USD)](https://main--aem-boilerplate-commerce--hlxsites.aem.page/en/#nolocal) ``` **Important:** The boilerplate automatically localizes internal links to keep users within their current locale (see [Localizing links](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/linking/) for technical details). For links that should always point to a specific locale (such as store switcher links), add the hash `#nolocal` to prevent automatic localization: ```markdown [United States (USD)](https://yoursite.com/en/#nolocal) ``` **Best practice:** Prioritize the current store view in the display order for better user experience. For example, in the `en-ca` folder's store switcher, list "Canada (EN)" first. ### 7. Preview and validate content Test your new store views to ensure proper functionality before publishing. 1. **Push configuration changes** to your storefront repository. 2. **Return to the content files** in both `en-ca` and `fr-ca` folders in da.live. 3. **Preview content files**: - Navigate to the `en-ca` folder - Open the `index` file - Click "Preview" to test the store view 4. **Verify configuration in browser**: - Check that the URL structure includes the store view path - Open browser Developer Tools - Navigate to Application → Session Storage - Look for the `config` key and expand the JSON object - Navigate to `public` → `[your store path]` → `headers` → `cs` - Verify the correct header values: - `Magento-Store-Code` - `Magento-Website-Code` - `Magento-Store-View-Code` 5. **Test the store switcher**: - Verify the store switcher correctly shows the current store view - Confirm customers can see and select other available stores - Check that switching stores navigates to the correct URLs > **Session storage verification** Use session storage to verify configuration loads properly and identifies the correct store view. ### 8. Localize and translate content After setting up infrastructure, prepare content for each locale. 1. **Translate content files** in the `/en-ca/` and `/fr-ca/` folders using your chosen authoring tool: - **Document Authoring**: Follow the [Localization (Document Authoring Tool)](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/quick-start/content-localization/) workflow - **Universal Editor**: Follow the [Localization (Universal Editor)](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/quick-start/content-localization-universal-editor/) workflow 2. **Complete Commerce-specific tasks**: Follow the [Commerce localization tasks](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/quick-start/content-localization-commerce-tasks/) guide to: - Translate drop-in placeholder files (`cart.json`, `checkout.json`, `pdp.json`) - Verify store view configuration aligns with folder structure - Test Commerce functionality across all locales ### 9. Final validation and publishing Complete the implementation with thorough testing and team coordination. 1. **Test all store views** to ensure: - Proper URL structure - Correct content loading - Functional store switcher - Accurate header configuration - Proper currency and pricing display 2. **Preview all files** in the updated folders (`/en-ca/`, `/fr-ca/`) with https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/edge-delivery/resources/sidekick/sidekick. 3. **Validate the storefront experience** by accessing each new language/region and testing the rendering, navigation, and data accuracy. 4. **Publish the store views**: - Publish the updated folders using AEM Sidekick when ready - Update all `store-switcher` content documents to include links to the new store views 5. **Prepare for marketing handoff**: - Inform the marketing team that content folders are ready - Provide directory structure documentation - Share access to content folders for ongoing management > **Success!** Your multistore infrastructure is now ready to support multiple locales with proper content management and customer experience. ## Best practices ### Folder structure - Use clear, consistent naming conventions (`en-ca`, `fr-ca`) - Ensure that folder names can be used in web URLs - Match folder names exactly with the Adobe Commerce store view codes - Maintain the same content structure across all store views ### Configuration management - Keep header values synchronized between `config.json` and Adobe Commerce Admin - Document all store view codes and their corresponding folders - Test configuration changes in preview before publishing - Use version control for all configuration files ### Content management - Maintain separate content folders for each locale - Use automated translation tools where possible - Keep placeholder files synchronized across store views - Test all localized content before publishing ## Troubleshooting ### Common Issues #### Store View Not Loading - **Check the folder mapping**: Verify that the folder mapping is correct by querying the Config Service: `GET https://admin.hlx.page/config/{ORG}/sites/{SITE}/folders.json`. All folder mapping changes must go through the Config Service API. - **Verify the headers**: Ensure that the headers in `config.json` or Configuration Service match the store view codes in Adobe Commerce Admin - **Check the session storage**: Use browser developer tools to verify that the correct headers are being sent #### Content Not Displaying - **Verify the content structure**: Ensure that all required files (index, store-switcher, placeholders) exist in the store view folder - **Check the file permissions**: Verify that content files are properly published in da.live - **Validate the JSON syntax**: Check that placeholder JSON files have valid syntax #### Store Switcher Issues - **Check the links**: Ensure that all store switcher links use the correct URLs and include `#nolocal` for absolute links - **Verify the file structure**: Confirm that each store view has its own store-switcher file - **Test the navigation**: Verify that switching between stores works correctly ### Debugging Steps 1. **Check the browser console** for JavaScript errors 2. **Verify the network requests** in browser developer tools 3. **Check the session storage** for configuration values 4. **Validate the configuration files** for syntax errors 5. **Test in different browsers** to rule out browser-specific issues ### Performance considerations - Monitor site performance across all store views - Optimize images and assets for each locale - Consider CDN configuration for different regions - Test loading times from the target markets ## Related resources - [Content localization](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/quick-start/content-localization/) - Automated translation using da.live - [Storefront Configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) - Complete config.json reference - [Analytics](https://experienceleague.adobe.com/developer/commerce/storefront/setup/analytics/instrumentation/) — Analytics configuration for store views - https://experienceleague.adobe.com/en/docs/commerce-admin/stores-sales/site-store/store-views - Adobe Commerce Admin guide - https://www.aem.live/developer/folder-mapping - Edge Delivery Services documentation - https://www.aem.live/docs/config-service-setup - Configuration Service documentation --- # Price Book ID setup This guide shows you how to enable Adobe Commerce Optimizer Price Book ID functionality in the Adobe Commerce boilerplate. Price Books in Adobe Commerce Optimizer allow you to deliver personalized pricing based on customer segments, contracts, or other business rules. Each https://experienceleague.adobe.com/en/docs/commerce/optimizer/setup/pricebooks#view-price-books-in-commerce-optimizer and may restrict which Price Books are allowed for that view. Customers can have their own assigned Price Books, which are used if they fall within the allowed Price Books for the catalog view. > **Adobe Commerce Optimizer only** This guide is specifically for **Adobe Commerce Optimizer** implementations. If you are using Adobe Commerce as a Cloud Service or Adobe Commerce PaaS without Optimizer, this feature does not apply to your setup. Adobe Commerce as a Cloud Service uses customer groups and shared catalogs for pricing instead of Price Book IDs. See the /setup/configuration/commerce-configuration/ documentation for configuration options for your Commerce environment. ## Prerequisites Before you begin, ensure you have: - An active Adobe Commerce Optimizer subscription. - The storefront is /setup/configuration/commerce-configuration/#headers. - Access to your https://github.com/hlxsites/aem-boilerplate-commerce code. - The /dropins/user-auth/quick-start/ installed and configured in your storefront. ## Enable Price Book ID 1. **Enable Adobe Commerce Optimizer in the Auth drop-in initializer** Open the /dropins/user-auth/initialization/ file at `scripts/initializers/auth.js` and add the `adobeCommerceOptimizer: true` option: ```javascript // Initialize auth return initializers.mountImmediately(initialize, { langDefinitions, adobeCommerceOptimizer: true // Enable ACO }); ``` 1. **Replace the customer group header function** Open the main initializers file at `scripts/initializers/index.js` and replace the `setCustomerGroupHeader` function with the `setAdobeCommerceOptimizerHeader` function: ```javascript const setAdobeCommerceOptimizerHeader = (adobeCommerceOptimizer) => { if (adobeCommerceOptimizer?.priceBookId) { CS_FETCH_GRAPHQL.setFetchGraphQlHeader('AC-Price-Book-ID', adobeCommerceOptimizer.priceBookId); } else { CS_FETCH_GRAPHQL.removeFetchGraphQlHeader('AC-Price-Book-ID'); } }; ``` 1. **Update the event listener** In the same `scripts/initializers/index.js` file, replace the `auth/group-uid` event listener with the `auth/adobe-commerce-optimizer` /dropins/all/events/: ```javascript events.on('auth/adobe-commerce-optimizer', setAdobeCommerceOptimizerHeader, { eager: true }); ``` The `{ eager: true }` option ensures the event handler executes immediately if the event has already been emitted. See /dropins/all/events/#subscription-options for more details. 1. **Test the implementation** After making these changes, verify that the Price Book ID header is being sent with GraphQL requests: - Log in to your storefront as a customer. - Open your browser's developer tools and navigate to the Network tab. - Look for GraphQL requests and verify the `AC-Price-Book-ID` header is present in the request headers. ## Troubleshooting ### Price Book ID header is not being sent If the `AC-Price-Book-ID` header is not appearing in your GraphQL requests: - Verify that `adobeCommerceOptimizer: true` is set in the Auth drop-in initializer. - Confirm that the catalog view has a default Price Book configured in Adobe Commerce Optimizer. - Verify that the customer has a valid Price Book assigned (or that the default Price Book for the catalog view is configured). - Check the browser console for any JavaScript errors that might prevent the event from firing. ### Pricing is not changing If the header is being sent but pricing remains unchanged: - Verify that the default Price Book for the catalog view is correctly configured in Adobe Commerce Optimizer. - If the customer has an assigned Price Book, confirm it is within the allowed Price Books for the catalog view. - Confirm that your Adobe Commerce backend is configured to recognize and process the `AC-Price-Book-ID` /setup/configuration/commerce-configuration/#headers. - Check that the Price Book has active pricing rules. ## How it works When Adobe Commerce Optimizer is enabled: 1. Each catalog view in Commerce Optimizer defines a default Price Book and may restrict which Price Books are allowed for that view. 1. The /dropins/user-auth/ emits the `auth/adobe-commerce-optimizer` event when a customer logs in. 1. The event includes the Price Book ID for the authenticated customer (if the customer has an assigned Price Book). 1. Adobe Commerce Optimizer validates that the Price Book for the customer is within the allowed Price Books for the catalog view. If valid, the Price Book for the customer is used. Otherwise, the default Price Book for the catalog view is used. 1. The `setAdobeCommerceOptimizerHeader` function adds the `AC-Price-Book-ID` header to all GraphQL requests. 1. Adobe Commerce uses this header to return pricing specific to the determined Price Book. > The `eager: true` option ensures the event handler is registered before any authentication occurs, preventing race conditions. Learn more about /dropins/all/events/#subscription-options. > Each catalog view in Commerce Optimizer https://experienceleague.adobe.com/en/docs/commerce/optimizer/setup/pricebooks#view-price-books-in-commerce-optimizer and may restrict which Price Books are allowed. Customers can have their own assigned Price Books. The `commerceOptimizer` GraphQL query returns the appropriate Price Book ID for the authenticated customer, ensuring it falls within the allowed Price Books for the catalog view. For more information about Commerce Optimizer setup, see the https://experienceleague.adobe.com/en/docs/commerce/optimizer/setup/catalog-view documentation. ## Additional resources - https://experienceleague.adobe.com/en/docs/commerce/optimizer/setup/pricebooks - [Storefront configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) - [Auth drop-in documentation](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/user-auth/quick-start/) --- # Storefront Compatibility B2B Package The Storefront Compatibility B2B Package (SCP-B2B) extends core Adobe Commerce B2B with the storefront-centric GraphQL that [B2B drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/) need — GraphQL for drop-in features that are not part of the core B2B product. SCP-B2B is not the same as Adobe Commerce B2B or the npm drop-ins in your storefront repo. > **Version and compatibility requirements** Adobe Commerce `2.4.8` and `2.4.9` only, tested with Adobe Commerce B2B `1.5.2` and `1.5.3`. SCP-B2B does not support Adobe Commerce 2.4.7 or Magento Open Source. For tested drop-in versions, see the [release suite](https://experienceleague.adobe.com/developer/commerce/storefront/releases/). See [What you need](#what-you-need) for backend-specific installation requirements. ## What you need | Item | Requirement | |---|---| | Commerce backend | Adobe Commerce as a Cloud Service: Adobe installs SCP-B2B automatically (no merchant Composer step). Commerce on Cloud or on-premises with Adobe Commerce Optimizer: you install SCP-B2B with Composer, same responsibility model as the B2C package. Commerce on Cloud or on-premises without Adobe Commerce Optimizer: SCP-B2B is not available. See [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/) and [Find your path](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/storefront-compatibility/#storefront-compatibility-b2b-package). | | B2C compatibility package | Required on the same Commerce instance. Install or confirm it before SCP-B2B. See [Storefront Compatibility Package](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/storefront-compatibility/). | | Adobe Commerce B2B | Separate extension and license on Commerce. Required on your instance before you install SCP-B2B. Contact your Adobe account representative to add it to your contract. | | B2B drop-ins in your repo | You install with npm after the backend is ready. See [Licensing requirements](https://experienceleague.adobe.com/developer/commerce/storefront/licensing/). | ## Major features Adobe releases new GraphQL queries and mutations as part of the Storefront Compatibility B2B Package. The list below summarizes all GraphQL queries and mutations currently available in the package. For details about when a specific query or mutation was introduced, see the [changelog](https://experienceleague.adobe.com/developer/commerce/storefront/releases/changelog/) and filter for the Storefront Compatibility B2B Package. [Quick Order](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/) uses core Commerce GraphQL and is not listed here. ## GraphQL APIs This package provides the following GraphQL queries and mutations: #### Company hierarchy Supports [Company Management](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/) and [Company Switcher](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-switcher/) drop-in. - https://developer.adobe.com/commerce/webapi/graphql/schema/b2b/company/mutations/assign-child-company/ mutation — Company administrators can assign a child company to a parent company within the company hierarchy. - https://developer.adobe.com/commerce/webapi/graphql/schema/b2b/company/mutations/unassign-child-company/ mutation — Company administrators can unassign a child company from its parent company within the company hierarchy. #### Requisition lists Supports the [Requisition List](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/) drop-in. - https://developer.adobe.com/commerce/webapi/graphql/schema/b2b/requisition-list/queries/shared-requisition-list/ query — Retrieves a read-only shared requisition list for B2B customers, including sender details and associated items, based on the provided token. - https://developer.adobe.com/commerce/webapi/graphql/schema/b2b/requisition-list/mutations/share-requisition-list-by-token/ mutation — Generates a shareable storefront token that enables B2B customers to share a requisition list with colleagues within the same company. - https://developer.adobe.com/commerce/webapi/graphql/schema/b2b/requisition-list/mutations/share-requisition-list-by-email/ mutation — Sends a requisition list to colleagues within the same company via email for B2B customers. - https://developer.adobe.com/commerce/webapi/graphql/schema/b2b/requisition-list/mutations/import-shared-requisition-list/ mutation — Imports a shared requisition list into the current customer account based on the provided token. #### Negotiable quotes Supports [Quote Management](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/) drop-in. - https://developer.adobe.com/commerce/webapi/graphql/schema/b2b/negotiable-quote/queries/templates/ query — Retrieves negotiable quote templates with sales rep, order, and pricing details. - https://developer.adobe.com/commerce/webapi/graphql/schema/b2b/negotiable-quote/mutations/set-quote-template-expiration-date/ mutation — Sets an expiration date on negotiable quote templates. - https://developer.adobe.com/commerce/webapi/graphql/schema/b2b/negotiable-quote/mutations/place-order-v2/ mutation — Transforms a negotiable quote into an order and returns full order details, including an errors array for standardized response handling. #### Company Address - https://developer.adobe.com/commerce/webapi/graphql/schema/b2b/company/ query – Returns a paginated list of company addresses assigned to the company, the company address book configuration settings, and the company's default billing and shipping addresses, if set. - https://developer.adobe.com/commerce/webapi/graphql/schema/b2b/company/mutations/create-address/ mutation – Creates a new company address. - https://developer.adobe.com/commerce/webapi/graphql/schema/b2b/company/mutations/delete-address/ mutation – Deletes a company address based on the provided address UID. - https://developer.adobe.com/commerce/webapi/graphql/schema/b2b/company/mutations/set-default-address/ mutation – Sets a company address as the default billing or shipping address based on the provided address UID. - https://developer.adobe.com/commerce/webapi/graphql/schema/b2b/company/mutations/update-address/ mutation – Updates an existing company address based on the provided address UID (address type is immutable after creation). ## REST endpoints This package also provides following REST APIs: - Company address CRUD endpoints: - `POST /V1/company/:companyId/address` – Create a company address. - `GET /V1/company-address/:addressId` – Retrieve a company address. - `GET /V1/company/:companyId/addresses` – Retrieve the list of company addresses. - `PUT /V1/company-address/:addressId` – Update a company address. - `DELETE /V1/company-address/:addressId` – Delete a company address. - Endpoints to manage default company addresses: - `GET /V1/companies/:companyId/billingAddress` – Retrieve the default billing address. - `GET /V1/companies/:companyId/shippingAddress` – Retrieve the default shipping address. - `PUT /V1/company-addresses/:addressId/default` – Set the default billing or shipping address. - Company address metadata endpoints: - `GET /V1/attributeMetadata/companyAddress` – Retrieve all company address attribute metadata. - `GET /V1/attributeMetadata/companyAddress/attribute/:attributeCode` – Retrieve metadata for a specific company address attribute. - `GET /V1/attributeMetadata/companyAddress/form/:formCode` – Retrieve company address attributes associated with a specific form. - `GET /V1/attributeMetadata/companyAddress/custom` – Retrieve custom company address attribute metadata. ## Release information For release notes for each Storefront Compatibility B2B Package version, see the [changelog](https://experienceleague.adobe.com/developer/commerce/storefront/releases/changelog/) and filter for the **Storefront Compatibility B2B Package**. For tested package and drop-in versions in the current suite, see the [release suite](https://experienceleague.adobe.com/developer/commerce/storefront/releases/). --- # Storefront Compatibility Package The Storefront Compatibility Package extends Adobe Commerce with GraphQL and REST operations that storefront drop-ins need, such as cart, checkout, account, and order flows. Use the tables below to find the installation path for each package. ## Find your path The B2C Storefront Compatibility Package supports Adobe Commerce 2.4.7, 2.4.8 and 2.4.9 on Commerce on Cloud or on-premises, though 2.4.9 is recommended for new storefront projects. Adobe Commerce as a Cloud Service is versionless, so this version guidance doesn't apply there — Adobe keeps it current automatically. Magento Open Source is not supported. If you run Adobe Commerce 2.4.7, see [Adobe Commerce 2.4.7](https://experienceleague.adobe.com/developer/commerce/storefront/reference/storefront-compatibility/v247/) instead. For supported versions and end-of-life dates, see https://experienceleague.adobe.com/en/docs/commerce-operations/release/versions. ### B2C Storefront Compatibility Package | If you use | Installation | What to do next | |---|---|---| | Adobe Commerce as a Cloud Service | Adobe, automatically | No action needed | | Commerce on Cloud or on-premises with Adobe Commerce Optimizer | You, with Composer | [Manual installation](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/storefront-compatibility/install/) | | Commerce on Cloud or on-premises without Adobe Commerce Optimizer | Not available without Adobe Commerce Optimizer | [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/) [Licensing requirements](https://experienceleague.adobe.com/developer/commerce/storefront/licensing/) | ### Storefront Compatibility B2B Package B2B drop-ins need the Storefront Compatibility B2B Package (SCP-B2B) in addition to the B2C Storefront Compatibility Package. SCP-B2B extends core Adobe Commerce B2B with additional GraphQL and REST APIs as a separate package with its own release schedule. Adobe Commerce B2B must already be configured on your Commerce instance before you install SCP-B2B, and SCP-B2B supports only Adobe Commerce 2.4.8 and 2.4.9. See [What you need](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/storefront-compatibility/b2b/#what-you-need) on the B2B package page for prerequisites. | If you use | Installation | What to do next | |---|---|---| | Adobe Commerce as a Cloud Service | Adobe, automatically | [B2B Compatibility Package](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/storefront-compatibility/b2b/) | | Commerce on Cloud or on-premises with Adobe Commerce Optimizer | You, with Composer | [Manual installation](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/storefront-compatibility/install/#install-the-b2b-package) | | Commerce on Cloud or on-premises without Adobe Commerce Optimizer | Not available without Adobe Commerce Optimizer | [Backend options](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/) [Licensing requirements](https://experienceleague.adobe.com/developer/commerce/storefront/licensing/) | --- # Manual installation On Commerce on Cloud or on-premises with Adobe Commerce Optimizer, install the B2C Storefront Compatibility Package with Composer on your Commerce server. If you use [B2B drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/), also install the Storefront Compatibility B2B Package (SCP-B2B) on the same instance after the B2C package. Not sure this is your path? See [Find your path](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/storefront-compatibility/#find-your-path). ## Choose your B2C package The package version must match your Adobe Commerce release. Use the version from this table in `composer require adobe-commerce/storefront-compatibility:VERSION`. | Adobe Commerce | Package version | Reference | |---|---|---| | 2.4.9 (recommended) | `4.9.3` | [GraphQL and REST changes](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/storefront-compatibility/v249/) | | 2.4.8 | `4.8.28` | [GraphQL and REST changes](https://experienceleague.adobe.com/developer/commerce/storefront/reference/storefront-compatibility/v248/) | | 2.4.7 | `4.7.15` | [GraphQL and REST changes](https://experienceleague.adobe.com/developer/commerce/storefront/reference/storefront-compatibility/v247/) | > **Composer key requirements** To access the latest Storefront Compatibility Package, you must have either an Adobe Commerce Optimizer license for Commerce on Cloud or on-premises, or an Adobe Commerce as a Cloud Service license. Adobe Commerce on Cloud or on-premises without Optimizer is not supported. Magento Open Source instances are not supported. ## Install the B2C package ### Commerce on Cloud 1. Change to your Commerce project directory. 1. Check out the environment branch you want to update. ```bash magento-cloud environment:checkout ``` 1. Add the package using the version from [Choose your B2C package](#choose-your-b2c-package). ```bash composer require adobe-commerce/storefront-compatibility:4.9.3 ``` Use `4.8.28` for Adobe Commerce 2.4.8. Use `4.7.15` for Adobe Commerce 2.4.7. 1. Update package dependencies. ```bash composer update adobe-commerce/storefront-compatibility ``` 1. Commit and push so the cloud environment picks up `composer.json` and `composer.lock`. ```bash git add composer.json composer.lock git commit -m "Add module" git push origin ``` To confirm the deployment succeeded, check the https://experienceleague.adobe.com/en/docs/commerce-cloud-service/user-guide/develop/test/log-locations#deploy-log in the Cloud console when the push finishes. ### On-premises 1. Add the package using the version from [Choose your B2C package](#choose-your-b2c-package). ```bash composer require adobe-commerce/storefront-compatibility:4.9.3 ``` Use `4.8.28` for Adobe Commerce 2.4.8. Use `4.7.15` for Adobe Commerce 2.4.7. 1. Apply the module and clear the cache. ```bash bin/magento setup:upgrade && bin/magento cache:clean ``` ## Choose your B2B package [B2B drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/) need SCP-B2B in addition to the B2C package. Adobe Commerce B2B must already be configured on your Commerce instance. SCP-B2B supports Adobe Commerce 2.4.8 and 2.4.9 only, not Adobe Commerce 2.4.7. Use the version from this table in `composer require adobe-commerce/storefront-compatibility-b2b:VERSION`. For GraphQL changes in the package, see [B2B Compatibility Package](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/storefront-compatibility/b2b/). | Adobe Commerce | Package version | |---|---| | 2.4.9 (recommended) | `1.0.25` | | 2.4.8 | `1.0.25` | > **Composer key requirements** To access the latest Storefront Compatibility B2B Package, you must have either an Adobe Commerce Optimizer license for Commerce on Cloud or on-premises, or an Adobe Commerce as a Cloud Service license — the same requirement as the Storefront Compatibility Package (B2C). PaaS-only and Magento Open Source instances are not supported. ## Install the B2B package Install SCP-B2B only after the B2C Storefront Compatibility Package is on the instance. ### Commerce on Cloud 1. Change to your Commerce project directory. 1. Check out the environment branch you want to update, if you have not already done so for the B2C package. 1. Add SCP-B2B using the version from [Choose your B2B package](#choose-your-b2b-package). ```bash composer require adobe-commerce/storefront-compatibility-b2b:1.0.25 ``` 1. Update package dependencies. ```bash composer update adobe-commerce/storefront-compatibility-b2b ``` 1. Commit and push so the cloud environment picks up `composer.json` and `composer.lock`. ```bash git add composer.json composer.lock git commit -m "Add B2B compatibility package" git push origin ``` ### On-premises 1. Add SCP-B2B using the version from [Choose your B2B package](#choose-your-b2b-package). ```bash composer require adobe-commerce/storefront-compatibility-b2b:1.0.25 ``` 1. Apply the module and clear the cache. ```bash bin/magento setup:upgrade && bin/magento cache:clean ``` ## Update the packages Use these steps after your initial Composer install to move to a newer patch version. Open the [changelog](https://experienceleague.adobe.com/developer/commerce/storefront/releases/changelog/) and filter for the **Storefront Compatibility Package** or **Storefront Compatibility B2B Package** before you update. 1. Update the B2C package: ```bash composer update adobe-commerce/storefront-compatibility ``` 1. If you use B2B drop-ins, update the SCP-B2B package: ```bash composer update adobe-commerce/storefront-compatibility-b2b ``` 1. Upgrade Commerce and clear the cache: ```bash bin/magento setup:upgrade && bin/magento cache:clean ``` ## What's next After you install the packages, [configure your storefront](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) to connect to Commerce. Confirm any remaining items on the [Commerce on Cloud or on-premises checklist](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/backends/#paas-required-packages-and-services) before going live. --- # Adobe Commerce 2.4.9 The Storefront Compatibility Package adds backend changes that storefront drop-ins need on Adobe Commerce 2.4.9. This package provides mostly GraphQL schema extensions and REST endpoints, plus bug fixes. > **Version and compatibility requirements** Adobe Commerce 2.4.9 only. Magento Open Source and earlier Adobe Commerce versions are not supported. On Adobe Commerce as a Cloud Service, Adobe installs and updates this package automatically. On Commerce on Cloud or on-premises with Adobe Commerce Optimizer, you can install the current package line with Composer. See [Manual installation](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/storefront-compatibility/install/). ## Major features Adobe releases new GraphQL queries, mutations, and REST endpoints across minor versions in each package line. For version-specific changes, see the [changelog](https://experienceleague.adobe.com/developer/commerce/storefront/releases/changelog/) and filter for the Storefront Compatibility Package. ## GraphQL APIs This package provides the following GraphQL queries and mutations: #### Login as Customer and seller-assisted buying - https://developer.adobe.com/commerce/webapi/graphql/schema/customer/mutations/exchange-otp-customer-token/ mutation — Authenticates admins as customers for seller-assisted buying by exchanging a one-time password (OTP) and email for a customer token. #### Free Gift Selection - https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/select-free-gift/ mutation — Selects a free gift for an eligible cart promotion. #### Product Alerts - https://developer.adobe.com/commerce/webapi/graphql/schema/products/queries/is-subscribed-product-alert-price/ query — Checks whether a customer is subscribed to price alerts for a product. - https://developer.adobe.com/commerce/webapi/graphql/schema/products/queries/is-subscribed-product-alert-stock/ query — Checks whether a customer is subscribed to stock alerts for a product. - https://developer.adobe.com/commerce/webapi/graphql/schema/products/mutations/subscribe-product-alert-price/ mutation — Subscribes a customer to price alerts for a product. - https://developer.adobe.com/commerce/webapi/graphql/schema/products/mutations/subscribe-product-alert-stock/ mutation — Subscribes a customer to stock alerts for a product. - https://developer.adobe.com/commerce/webapi/graphql/schema/products/mutations/unsubscribe-product-alert-price/ mutation — Unsubscribes a customer from price alerts for a product. - https://developer.adobe.com/commerce/webapi/graphql/schema/products/mutations/unsubscribe-product-alert-price-all mutation — Unsubscribes a customer from all price alerts. - https://developer.adobe.com/commerce/webapi/graphql/schema/products/mutations/unsubscribe-product-alert-stock/ mutation — Unsubscribes a customer from stock alerts for a product. - https://developer.adobe.com/commerce/webapi/graphql/schema/products/mutations/unsubscribe-product-alert-stock-all mutation — Unsubscribes a customer from all stock alerts. ## REST endpoints This package also provides following REST APIs: - `GET /V1/customerSegments/search` — Retrieves customer segments for integration or IMS token callers. - `GET /V1/customerGroups/search` — Adds a `uid` extension attribute on each customer group in the response. - `GET /V1/customers/:customerId` — Adds an `assistance_allowed` extension attribute that indicates whether the customer enabled admin assistance for Login as Customer. - `POST /V1/customer/:customerId/otp` — Generates a one-time password in Admin for seller-assisted buying and Login as Customer workflows. ## Release information For release notes for each Storefront Compatibility Package version, see the [changelog](https://experienceleague.adobe.com/developer/commerce/storefront/releases/changelog/) and filter for the **Storefront Compatibility Package**. For tested package and drop-in versions in the current suite, see the [release suite](https://experienceleague.adobe.com/developer/commerce/storefront/releases/). --- # Data export validation Data export synchronizes data between an Adobe Commerce instance and connected [Commerce services](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/commerce-services-and-backends/#commerce-services). Those services are required for drop-in components to work correctly. Validating the data export is crucial to ensure that the data is correctly synchronized and available for the storefront. You can use the https://experienceleague.adobe.com/en/docs/commerce-admin/systems/data-transfer/data-sync/data-dashboard to monitor the data sync progress for each service. :::tip[Self-service data export validation] See the https://experienceleague.adobe.com/en/docs/commerce/saas-data-export/overview for details and troubleshooting. ::: Use GraphQL to validate that all products were synchronized and product lookup and search are working. See https://developer.adobe.com/commerce/webapi/graphql/schema/catalog-service/queries/products/ and https://github.com/magento/adobe-commerce-catalog-service for queries. For more complex catalogs, reach out to the Adobe team on https://discord.com/channels/1131492224371277874/1220042081209421945 to validate that the exported data is correct. This might require you to provide a database dump or direct access. Please also reach out to the Adobe team if you encounter any currently unsupported use cases, so that they can be enabled in the future. --- # Luma Bridge This section provides guidance on how to implement the Luma Bridge for your Adobe Commerce on Edge Delivery Services project. PaaS only > **Supported Configuration** Luma Bridge is intended for Adobe Commerce Platform as a Service (PaaS) instances with complex cart, checkout, and account page implementations that need to be migrated progressively to Edge Delivery Services. It is only supported for the following configuration: **Edge Delivery Services (EDS) + Adobe Commerce Optimizer (ACO) + PaaS**. If you need access to the Luma Bridge package, please reach out to us at `commerce-storefront-luma-bridge@adobe.com`. ## What is Luma Bridge? Luma Bridge is a session management mechanism between two storefronts: - The main storefront (headless storefront) that is built using the https://github.com/hlxsites/aem-boilerplate-commerce on Edge Delivery Services - The default, theme-based PHP storefront that is built using Adobe Commerce (Luma theme or another base theme) It allows you to reuse complex parts of your storefront (for example, cart, checkout, and account pages). This approach provides a path for Adobe customers currently using default, theme-based storefronts to migrate to a highly performant shopping experience in a phased manner. Using Luma Bridge, you can progressively migrate your theme-based storefront to Edge Delivery Services. :::note If you already have a headless storefront implementation (for example, https://github.com/magento/pwa-studio or https://vuestorefront.io/), Luma Bridge is not a suitable solution to share session and other context between Edge Delivery Services and your storefront. ::: ## Demo site The following demo site was built with Luma Bridge. You can access the full demo site at https://mcprod.eds.ecg.magento.com/. [![Adobe Commerce storefront on Edge Delivery Services boilerplate demo.](https://experienceleague.adobe.com/developer/commerce/storefront/setup/discovery/images/luma-bridge-demo.png)](https://mcprod.eds.ecg.magento.com/) ## Requirements for implementation You must meet the following requirements to implement Luma Bridge: - Both the Edge Delivery Services storefront and the theme-based Adobe Commerce storefront must operate on the same top-level domain. - You must use the user authentication and cart drop-in components to manage your storefront authentication and cart context. This ensures that Luma Bridge works with the correct set of cookies. ## How does session management work? Luma Bridge relies on cookies generated by the [user authentication](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/user-auth/) and [cart](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/) drop-in components. These components are part of your Commerce boilerplate template-based storefront on Edge Delivery Services. Luma Bridge is a PHP module installed in Adobe Commerce, as noted above. When you enable the Luma Bridge module, Adobe Commerce can recognize and create the same cookies that your storefront drop-in components generate. This enables both applications to share the same session cookies, whether the user has a guest session or a logged-in session. The cookies include: - A JWT token that represents the customer session when the user is logged in, generated as a response to the `generateCustomerToken` GraphQL mutation. - A cart ID that represents a masked quote ID, generated as a response to the `createGuestCart` GraphQL mutation. No changes are required on your Edge Delivery Services storefront when you use the user authentication and cart drop-in components. ## Implementation considerations Before starting an implementation, clarify the following information: - Architecture of cart, checkout, and account pages - Complexity of implementation - Customization requirements This information will help you decide whether to use Luma Bridge or a headless implementation for the cart, checkout, and account pages. :::tip When implementing Luma Bridge, ensure that you have engineers on the project with backend PHP knowledge on Adobe Commerce. ::: ## Installation To install the Luma Bridge, use the following steps: 1. Fetch the package via Composer: ```bash composer require adobe-commerce/luma-bridge ``` 1. Enable the module and complete the standard CLI procedure: ```bash bin/magento module:enable Magento_LumaBridge bin/magento setup:upgrade bin/magento setup:di:compile bin/magento cache:flush ``` After completing the installation, Luma Bridge will automatically recognize and share session cookies between your Edge Delivery Services storefront and Adobe Commerce. Test that user authentication and cart state persist correctly as customers navigate between both storefronts. --- # Overview Before starting any Adobe Commerce on Edge Delivery Services project, you must conduct a setup phase to scope the project and ensure that there are no major roadblocks or risks. ## Big picture In the [create your storefront](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/) tutorial, you learned how to quickly create a new project based on the https://github.com/hlxsites/aem-boilerplate-commerce. The boilerplate provides a starter storefront that uses a pre-configured Adobe Commerce environment. The next step is to plan, build, optimize, and launch a production-ready storefront connected to your own Adobe Commerce instance and storefront services. Here's an overview of the process: ![Storefront project planning and delivery process.](https://experienceleague.adobe.com/developer/commerce/storefront/images/implementation/implementation-overview-v2.svg) *Storefront project planning and delivery process.* 1. **Discovery**: Understand the requirements and goals of the project. 1. **Setup**: Configure the project environment and tools. 1. **Analytics**: Instrument the storefront to collect user interaction events. 1. **SEO**: Optimize the storefront for search engines and marketing campaigns. 1. **Drop-in components**: Develop and integrate drop-in components to enhance the storefront experience. 1. **Launch**: Deploy the storefront to a production environment. ## Project scope The discovery phase is important because Adobe Commerce is a highly customizable platform with a large third-party extension ecosystem. Migrating an Adobe Commerce storefront to Edge Delivery Services is similar to migrating to a headless storefront (like https://developer.adobe.com/commerce/pwa-studio). :::note[Important] The key requirement for a headless storefront implementation is that all required data is provided by APIs. ::: Successfully building a storefront requires a well-defined plan and a phased approach. Here are some key steps to consider: ### 1. Phased launch Fast return on investment (ROI) and learnings: - Break down project scope into small milestones - Small milestones result in faster time-to-value (TTV), learnings, and higher quality - Define a rollout _and_ rollback strategy for each milestone (for example, split traffic) Adobe recommends the following launch phases: - Homepage and content pages with high-acquisition traffic that benefit from SEO improvement - Catalog pages (PDP and PLP) with high-conversion traffic that benefit from performance improvements - Checkout and account pages with high-retention traffic that benefit from personalization :::tip Instead of launching all features at once, start with a fresh Commerce project, launch with the minimum viable feature set, and add additional features as needed. Avoid introducing any potentially breaking changes to the existing project. For example, if you uninstall an extension required for an existing storefront when migrating to Edge Delivery Services, this could be a breaking change that prevents you from rolling back to the existing storefront if necessary. ::: ### 2. Metrics Define what success should look like in each phase: - Define measurable business and technical metrics that are impacted by launch - Establish a baseline for each metric before launch - Create a realistic forecast of what to expect over time ### 3. Validation Validate the impact of changes: - Validate launch impact using your baseline and forecast - Prioritize fixing issues you and your team can resolve quickly - Rollback if issues are unclear or if a fix will take too long - Improve and test code before the next launch An iterative approach to launching your storefront will help you quickly identify and resolve issues, and improve the overall quality of your project. The following diagram illustrates the iterative approach: ![Iterative rollout process.](https://experienceleague.adobe.com/developer/commerce/storefront/images/implementation/iterative-rollout-v1.svg) *Iterative rollout process.* ### Use cases and requirements Document your use cases and requirements and create a plan for how you will implement them. Adobe offers pre-built components that accelerate development (drop-in components). Drop-in components are reusable components that define the storefront shopping experience. They are framework agnostic and can be used in any context (Edge Delivery Services, AEM, Luma). However, this documentation focuses on the use of drop-in components in Edge Delivery Services projects using the https://github.com/hlxsites/aem-boilerplate-commerce. The drop-in component development roadmap is synchronized with Adobe Commerce APIs, so new API features are automatically available in drop-in components. :::note See the [drop-in components overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/introduction/) for a list of all available drop-in components. ::: If you see gaps in what the drop-in components support, Adobe can help with a plan to achieve your scenarios. For example: - Identify if drop-in components can solve your use cases and requirements - Identify what use cases are already supported, what is a gap, and what needs to be implemented differently from what is available out-of-the-box - Reach out to the Adobe team early to share your use cases and get recommendations on how to fulfill them: [commerce-storefront-compatibility@adobe.com](mailto:commerce-storefront-compatibility@adobe.com) The Live Search Popover and PLP have two integration paths: - Using the out-of-the-box hosted option where Adobe hosts the JavaScript file - Automatic updates for fixes and small features - Small upgrades available for major or breaking features - Can change some styling - Using the customized option where Adobe provides a reference implementation for the components - Full control of customization and look and feel - You host the library and own the total cost of ownership ### Extensions Before starting the project, use the following list to create an inventory of the Adobe Commerce extensions that are actively being used. This will help you understand which extensions can be replaced by out-of-the-box Adobe Commerce functionality. - What extensions are currently in use? - What type of data do the extensions provide (for example, reviews)? - Is the data required on the frontend? - How do the extensions expose the data (for example, GraphQL, REST API)? - Do the extensions expose an API to access data (for example, a product labels module from https://commercemarketplace.adobe.com/)? If not, create an action item to expose the required data through an API. Options include: - Customize the Adobe Commerce Catalog Service exporter to export additional custom data to Catalog Service - Create a custom Adobe Commerce GraphQL query - Use https://developer.adobe.com/graphql-mesh-gateway/ - Are any of the extensions for delivery options (for example, shipping/BOPIS), payments, or tax providers? If you use third-party solutions, clarify if they expose APIs on the frontend and if they provide their own set of drop-in components for the frontend integration. ## Existing storefronts There are a couple of options for modernizing your existing storefront with Edge Delivery Services: - **Progressive implementation**: Rebuild selected parts of the commerce funnel on Edge Delivery Services and reuse the rest from your existing storefront. - **Full implementation**: Rebuild your entire storefront on Edge Delivery Services and retire your existing storefront. A progressive implementation enables you to unlock business value with Edge Delivery Services sooner, minimizing the risks associated with migration. You can start by implementing the home page only and reusing the rest of your existing storefront. The next step could be implementing catalog with product listing, search, and product details and reusing cart, checkout, and account from your existing storefront. This approach comes with the cost of maintaining two storefronts in parallel, so before you choose, assess which approach is right for you. These are the key factors to consider: - **Business metrics to improve**: - What areas of your existing storefront drive business performance? - What are the challenges based on the site analytics that you see? - **Level of storefront customization**: - How many experiences are built with custom code on your storefront? - Do these experiences rely on custom business logic? - **Third-party extensions**: - How many third-party extensions are you using on your existing storefront? - Do you need to connect to third-party services? - **B2B**: - Are there any Adobe Commerce B2B modules enabled on your existing storefront? Adobe recommends the progressive implementation if any of the following are true: - The main business metrics are brand visibility in search engines, customer acquisition cost, and customer engagement. - The existing storefront is heavily customized in the areas of checkout and user account. - There are many third-party integrations in the existing storefront that affect transactional flow. - The existing storefront supports B2B use cases with Adobe Commerce modules. The full implementation is a better option if any of the following are true: - Your business objectives include improving customer conversion and re-engagement. - The current checkout flow only relies on a few third-party integrations (for example, payments, shipping, taxes). - No Adobe Commerce B2B modules are enabled on the existing storefront. | Criteria | Progressive Implementation | Full Implementation | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | **Description** | Rebuild selected parts of the commerce funnel on Edge Delivery Services and reuse the rest from your existing storefront. | Rebuild your entire storefront on Edge Delivery Services and retire your existing storefront. | | **Business Metrics to Improve** | Brand visibility in search engines, customer acquisition cost, customer engagement. | Customer conversion, customer re-engagement. | | **Level of Customization** | Heavily customized in areas like checkout and user account. | Minimal customization, relies on a few third-party integrations (e.g., payments, shipping, taxes). | | **Third-Party Integrations** | Many third-party integrations affecting transactional flow. | Few third-party integrations. | | **B2B Modules** | Supports B2B use cases with Adobe Commerce modules. | No Adobe Commerce B2B modules enabled. | | **Project Scope** | Identify use cases for new and existing storefronts (e.g., catalog, product detail, cart, checkout, customer account). | Verify use cases are available through Adobe Commerce GraphQL APIs. | | **Third-Party Extensions** | Identify necessary extensions and their reliance on third-party services. | Identify necessary extensions and their reliance on third-party services. | | **Storefront Bridge Options** | Plan to connect existing storefront with the new one (e.g., using Luma Bridge). | Not applicable. | ### Progressive implementation For the progressive implementation, you'll need to look into the following: - **Project scope**: Identify which use cases the new storefront will handle and which ones the existing storefront will continue to manage, such as catalog, product detail, cart, checkout, and customer account. - **Third-party extensions**: Identify the list of extensions you will need on your new storefront and determine if they rely on integrating third-party services. - **Storefront bridge options**: Plan to connect the existing storefront with the new one. If you are using Adobe Commerce native storefront with Luma, you can use the Luma Bridge. ### Full implementation For the full implementation, the list of considerations is similar to the progressive implementation: - **Project scope**: Identify which use cases the new storefront will handle and verify that they are available through Adobe Commerce GraphQL APIs. - **Third-party extensions**: Identify the list of extensions you will need on your new storefront and determine if they rely on integrating third-party services. --- # Launch checklist Complete these steps before production traffic reaches your Adobe Commerce Storefront (EDS). As you do, capture what you changed in staging and outline the exact go-live sequence—such as DNS, CDN, endpoints, and content. Then document your rollback plan so production cleanly reflects what you've already validated. ## When your launch includes Adobe Commerce Optimizer If your launch includes Adobe Commerce on Cloud, Adobe Commerce Optimizer, and a storefront on Edge Delivery Services, start with the Optimizer program linked here: https://experienceleague.adobe.com/en/docs/commerce/optimizer/launch/launch-checklist. This ensures Cloud, Optimizer, and publishing stay aligned before moving on to storefront-specific tasks. {/* Mirrors EL Optimizer checklist: Finalize Storefront Experience > Content and Authoring — https://experienceleague.adobe.com/en/docs/commerce/optimizer/launch/launch-checklist. Last diffed 2026-08-05. */} ### Content and authoring * [ ] Complete the https://www.aem.live/docs/go-live-checklist (hosted on the Adobe Experience Manager documentation site). * [ ] Confirm that your authoring source is document-based or Universal Editor (and configured correctly). For Universal Editor in the Commerce boilerplate, see [Universal Editor](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/universal-editor/). * [ ] Publish content using the preview → publish cycle and verify it appears as expected. * [ ] Complete content and design QA on your project's `.aem.live` preview URL (Edge Delivery Services preview hostname). * [ ] Confirm that a favicon is configured and served correctly. * [ ] If your content lives in SharePoint, set up dedicated SharePoint access so only the right people can edit it. * [ ] Confirm that all drop-ins you use (cart, checkout, product detail page, product listing page, sign-in, account) are customized and tested. See the [Drop-ins introduction](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/introduction/). * [ ] Confirm that storefront branding matches your CSS design tokens, typography, and colors. See [Customizing blocks](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/customizing-blocks/). {/* Mirrors EL Optimizer checklist: Finalize Storefront Experience > SEO and Indexing — https://experienceleague.adobe.com/en/docs/commerce/optimizer/launch/launch-checklist. Last diffed 2026-08-05. */} ### SEO and indexing * [ ] Add document title metadata for key pages (especially PDPs and PLPs). See the [SEO metadata](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/metadata/) documentation for details. * [ ] Ensure that your PDPs have [metadata and structured data](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/metadata/) (for example, JSON-LD is configured). * [ ] Standardize URL formats for products (for example, `domain/product-name`). * [ ] Redirect all vanity URLs to canonical URLs. * [ ] Confirm that canonical URLs return `2xx` status codes (not `3xx` or `4xx`). * [ ] Add a `robots.txt` file to your project, which allows your site to be indexed by search engines. Ensure that your sitemaps are referenced and that you add rules to block indexing of any content that you do not want to be indexed (for example, the `/drafts` folder). * [ ] Add one or multiple https://www.aem.live/docs/redirects files to ensure that URLs that were changed as part of the migration still work (for example, when you remove the `.html` file extension). * [ ] Generate a sitemap for your site and catalog. To speed up the indexing process, Adobe recommends adding the sitemap to Google Search Console. For storefront indexing patterns (including multilingual setups), see [Indexing](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/indexing/). * [ ] For multilingual sites, include `hreflang` tags in the sitemap. * [ ] Review the Google Search Console coverage report and resolve any indexing errors. {/* Mirrors EL Optimizer checklist: Finalize Storefront Experience > Pre-rendering — https://experienceleague.adobe.com/en/docs/commerce/optimizer/launch/launch-checklist. Last diffed 2026-08-05. */} ### Enable pre-rendering * [ ] Turn on pre-rendering for the pages that matter most at launch. See [Pre-rendering](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/aem-prerender/). * [ ] Use lowercase URLs everywhere so pre-rendered links stay valid. * [ ] View page source on a pre-rendered page and confirm you see metadata and main body content in the first HTML response. * [ ] For each language or region you support, open a sample page and confirm the translated content loads. * [ ] Add any extra HTML your project needs (for example, analytics snippets that must appear in the initial HTML). If snippets depend on the Adobe Client Data Layer or storefront events, align them with [Analytics instrumentation](https://experienceleague.adobe.com/developer/commerce/storefront/setup/analytics/instrumentation/). {/* Mirrors EL Optimizer checklist: Finalize Storefront Experience > Performance and Monitoring — https://experienceleague.adobe.com/en/docs/commerce/optimizer/launch/launch-checklist. Last diffed 2026-08-05. */} ### Performance and monitoring * [ ] Follow [Performance best practices](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/performance/) for your storefront. * [ ] (Optional) Set up Google Analytics and Google Tag Manager. * [ ] Validate your https://github.com/adobe/commerce-events/tree/main/examples/events/snowplow-debugger implementation, then confirm events appear in Live Search and Product Recommendations dashboards in the Adobe Commerce Admin. * [ ] Validate that the `environment` field in your [Commerce configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/#analytics) is set to `Testing` while you build, then change it to `Production` when you launch. See [Analytics](https://experienceleague.adobe.com/developer/commerce/storefront/setup/analytics/instrumentation/). * [ ] Confirm that your Lighthouse scores are green on key templates and aim for a strong score (for example, `100` on mobile and desktop where practical), using the same checks you already ran on your `.aem.live` preview site. {/* Mirrors EL Optimizer checklist: Analytics and Monitoring (top-level section) — https://experienceleague.adobe.com/en/docs/commerce/optimizer/launch/launch-checklist. Last diffed 2026-08-05. */} ### Analytics and monitoring * [ ] Confirm that Operational Telemetry is enabled before launch so you can compare performance before and after you switch traffic. Adobe documents this feature as Operational Telemetry; teams often call it Real User Monitoring (RUM). See https://www.aem.live/docs/rum and https://www.aem.live/developer/rum if you implement or tune the collection. If you proxy RUM through your origin, follow [CDN configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/content-delivery-network/#proxy-rum-through-the-origin-to-avoid-a-tls-handshake). * [ ] If you use Adobe Experience Platform, confirm data collection is configured for the production site. See [Adobe Experience Platform](https://experienceleague.adobe.com/developer/commerce/storefront/setup/analytics/adobe-experience-platform/) for storefront credentials, datastreams, and how events reach the Edge Network. * [ ] On the production hostname, confirm that marketing technology (MarTech) tags fire and send data to the right tools. For the Adobe Client Data Layer, storefront events, and Commerce analytics settings, see [Analytics instrumentation](https://experienceleague.adobe.com/developer/commerce/storefront/setup/analytics/instrumentation/) and the [analytics section of Commerce configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/#analytics). * [ ] Write down current analytics baselines (page views, bounce rate, and similar metrics) from your reporting tools so your team expects a shift after the new site ships. Separately, record Web Vitals and Lighthouse baselines using [Performance best practices](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/performance/) so you can compare site speed before and after launch. * [ ] Walk a test order from add to cart through checkout to the order confirmation page and confirm each step is tracked. See [Validation and testing](https://experienceleague.adobe.com/developer/commerce/storefront/setup/analytics/instrumentation/#validation-and-testing) in Analytics instrumentation (checkout-related events are listed under [Event collection and validation](https://experienceleague.adobe.com/developer/commerce/storefront/setup/analytics/instrumentation/#event-collection-and-validation)), and use the https://github.com/adobe/commerce-events/tree/main/examples/events/snowplow-debugger in the commerce-events repository if you need a structured view of storefront events. {/* Mirrors EL Optimizer checklist: Finalize Storefront Experience > Security and Access — https://experienceleague.adobe.com/en/docs/commerce/optimizer/launch/launch-checklist. Last diffed 2026-08-05. */} ### Security and access * [ ] Set permissions for Document Authoring (DA.live) content and Edge Delivery Services sites so only trusted people can publish. See https://da.live/docs/administration/permissions and https://www.aem.live/docs/authentication-setup-authoring. * [ ] Confirm that the Product Visuals integration is available for your project. For required entitlements, prerequisites, and how the integration works, see the https://experienceleague.adobe.com/en/docs/commerce/aem-assets-integration/overview on Experience League. To enable AEM Assets images in your storefront configuration, see [AEM Assets integration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/aem-assets-configuration/). * [ ] Update password reset links in email templates so they match your Edge Delivery Services URLs. See the [FAQ](https://experienceleague.adobe.com/developer/commerce/storefront/troubleshooting/faq/#what-should-i-do-if-my-email-template-links-are-broken-after-migrating-to-edge-delivery-services-or-helix). * [ ] Add production API keys and secrets for every integration and payment provider you use in production. * [ ] Add your production domains to allowlists where your backends require it, and send a test webhook to confirm delivery. * [ ] Restrict Cross-Origin Resource Sharing (CORS) to the origins your storefront and tools actually use. See [CORS setup](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/cors-setup/). * [ ] Publish a privacy policy and a cookie consent flow that match your regions, including General Data Protection Regulation (GDPR) or California Consumer Privacy Act (CCPA) rules when they apply. {/* Mirrors EL Optimizer checklist: Finalize Storefront Experience > CDN and Caching — https://experienceleague.adobe.com/en/docs/commerce/optimizer/launch/launch-checklist. Last diffed 2026-08-05. */} ### CDN and caching * [ ] Point your CDN, Sidekick extensions, sitemap jobs, and image importer to the same production GraphQL endpoints your storefront uses in `config.json`. * **For Adobe Commerce on Cloud or on-premises (PaaS)**, this is typically `commerce-core-endpoint`, for example, `https://yourstore.example.com/graphql`, and sometimes a separate `commerce-endpoint` when Catalog Service handles catalog reads. * **For Adobe Commerce as a Cloud Service** or Adobe Commerce Optimizer**, use `commerce-endpoint` on the Adobe Commerce API host, for example, `https://na1-production.api.commerce.adobe.com/your-environment-id/graphql`. See [Commerce configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/)). * **For Adobe Commerce Optimizer**, also configure `commerce-core-endpoint` when checkout or account services still rely on a separate core Commerce host. * [ ] If you use Adobe Commerce Fastly, request a new CDN purge token for production, then add `authToken` and `serviceId` in your https://tools.aem.live/tools/cdn-setup/index.html. * [ ] Test your [CDN configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/content-delivery-network/) and confirm cache hits, manual purges, and automatic invalidation behave as you expect. * [ ] Publish a small content change and confirm it appears on the production domain without stale HTML (push invalidation works end to end). * [ ] For [multi-store setups](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/indexing/#multi-store-setups), add a store-specific cache buster to Catalog Service and Live Search requests (for example, a query string or a CDN rule) so each storefront cache stays separate. * [ ] Lower DNS time-to-live (TTL) values a few days before cutover so DNS changes propagate quickly. * [ ] Confirm that DNS A and CNAME records for every hostname point to the right targets. * [ ] Confirm that the SSL/TLS certificate is provisioned and verified for the production domain and that HTTPS is enforced everywhere. * [ ] Confirm that visitors who type the apex domain and visitors who type `www` both land on the URL you want. {/* Overlaps EL Optimizer checklist: Cloud and Optimizer Integration + Storefront and Optimizer Integration (storefront-side config only) — https://experienceleague.adobe.com/en/docs/commerce/optimizer/launch/launch-checklist. Last diffed 2026-08-05. */} ### Catalog service * [ ] In your storefront configuration, switch to the production endpoint for Catalog or Merchandising Services. * **For Adobe Commerce on Cloud or on-premises (PaaS)**, switch the `commerce-endpoint` to `https://catalog-service.adobe.io/graphql`. If the Adobe Commerce Optimizer Connector is configured for your project, use the Adobe Commerce Optimizer GraphQL endpoint. * **For Adobe Commerce as a Cloud Service** or **Adobe Commerce Optimizer**, switch the `commerce-endpoint` to the Commerce GraphQL production endpoint for your instance, for example `https://na1-production.api.commerce.adobe.com/your-environment-id/graphql`. See [Commerce configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/)). * [ ] If you use Adobe Commerce Optimizer, confirm `adobe-commerce-optimizer` is set to `true`, `commerce-endpoint` points to the production Optimizer GraphQL endpoint, and the `AC-View-ID` header holds the catalog view ID for your production instance. See [Commerce configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/). * [ ] For Commerce on cloud or on-premises only, ensure a production environment is configured in the Commerce Services Connector (will result in a new `environmentId`). See [Select or create a SaaS project](https://experienceleague.adobe.com/en/docs/commerce/user-guides/integration-services/saas#createsaasenv). * [ ] Sync the production catalog to the new production environment. * [ ] Create a new Commerce production API key-pair and use the public key as the `x-api-key` value. * [ ] Verify category IDs and ensure all category pages reference the correct category. * [ ] Update `environmentId` and `x-api-key` values in the `config.json` code file. See [Commerce configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) for header and environment field names your storefront expects. * [ ] Notify the Catalog Service team about the new production environment and launch date. {/* Mirrors EL Optimizer checklist: Testing (top-level section) — https://experienceleague.adobe.com/en/docs/commerce/optimizer/launch/launch-checklist. Last diffed 2026-08-05. */} ### Testing * [ ] Confirm that core flows work end to end: browse → search → filter → add to cart → checkout → account creation. * [ ] Confirm that payment gateways accept real and test transactions. * [ ] Confirm that order placement, confirmation email, and order tracking work correctly. * [ ] Confirm that shipping options and tax calculations are accurate. * [ ] Confirm that coupons, discounts, and loyalty programs behave as expected. * [ ] Complete user acceptance testing (UAT) on staging and production. * [ ] Complete load and stress testing and share results with your Adobe team. * [ ] Confirm that page load time is under three seconds on desktop and mobile. * [ ] Confirm that images, scripts, and assets are optimized. * [ ] Test Chrome, Firefox, Safari, and Edge for consistent behavior. * [ ] Confirm that responsive layouts work on mobile, tablet, and desktop. * [ ] Test performance on 3G, 4G, and Wi-Fi connections. * [ ] Complete an accessibility audit (WCAG, screen reader, keyboard navigation). * [ ] Establish a post-launch 404 monitoring plan. * [ ] Test your rollback plan to confirm it works if launch issues occur. {/* Mirrors EL Optimizer checklist: Launch Day and Post-Launch (top-level section) — https://experienceleague.adobe.com/en/docs/commerce/optimizer/launch/launch-checklist. Last diffed 2026-08-05. */} ### Launch day and post-launch * [ ] Confirm your launch date with Adobe and notify your CTA, CSE, or AM so they can coordinate support during the go-live. * [ ] Record the P1 support hotline number: US (+1) 800-497-0335, then select the menu number for Adobe Commerce. * [ ] Train your team to open a support ticket before calling the P1 hotline. * [ ] Check for the latest boilerplate changes and update your project accordingly. See [Monitor boilerplate changes](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/updates/#monitor-boilerplate-changes). * [ ] Confirm that a disaster recovery plan exists and has been tested. * [ ] After launch, verify Lighthouse scores on the production domain. * [ ] After launch, monitor Google Search Console for indexing and crawl errors. * [ ] After launch, monitor 404 reports and add redirects for high-traffic legacy URLs. * [ ] After launch, confirm that MarTech and analytics data appears on production. * [ ] Ask your Adobe Customer Technical Advisor (CTA), Customer Service Engineer (CSE), or Account Manager (AM) to enable high-SLA monitoring. * [ ] Establish a process to track and upgrade boilerplate and extension packages to current versions. Start from [Updates](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/updates/) and [Release Information](https://experienceleague.adobe.com/developer/commerce/storefront/releases/). ## Documentation Edge Delivery Services launch topics (DNS, CDN vendors, redirects, and platform checks) are available on the Adobe Experience Manager documentation site. Start from the https://www.aem.live/docs/#launch, or use the table below. The checklist sections above focuses on Adobe Commerce Storefront tasks for sites integrated with an Adobe Commerce backend. | Topic | Description | | ----- | ----------- | | https://www.aem.live/docs/go-live-checklist | Edge Delivery Services go-live practices; apply the items that match your storefront project. | | https://www.aem.live/docs/setup-byo-cdn-push-invalidation | Automatically purge content on your production CDN whenever an author publishes content changes. | | https://www.aem.live/docs/byo-cdn-cloudflare-worker-setup | Configure Cloudflare to deliver your storefront site. | | https://www.aem.live/docs/byo-cdn-akamai-setup | Use the Akamai Property Manager to configure a property to deliver your storefront site. | | https://www.aem.live/docs/byo-cdn-fastly-setup | Configure Fastly to deliver your storefront site. | | https://www.aem.live/docs/byo-cdn-cloudfront-setup | Set up Amazon Web Services CloudFront to deliver your storefront site with push invalidation. | | https://www.aem.live/docs/byo-dns | Custom domain without having to set up a content delivery network. | | https://www.aem.live/docs/redirects | Manage redirects as a spreadsheet from the `redirects` document in the root of your project folder. | --- # SEO overview Search optimization on this storefront depends on two coordinated layers: how Edge Delivery Services serves HTML to crawlers, and how your Commerce catalog URLs and metadata stay consistent across stores. ## Two layers to coordinate The first layer is the Edge Delivery Services platform, which renders and serves HTML. The second is your Commerce catalog, which supplies the URLs and metadata layered on top. ### Edge Delivery Services (platform) Edge Delivery Services (EDS) shapes the first HTML response delivered to crawlers. Document-based pages usually ship most of their text in that payload, while many catalog product detail pages (PDPs) load details later for performance unless you publish server-side metadata or use prerender. Because search engines and large language model (LLM) crawlers might not fully render every page, Adobe publishes shared SEO and GEO guidance on https://www.aem.live/docs/seo-geo to keep content discoverable. - [SEO and GEO best practices on AEM.live](https://www.aem.live/docs/seo-geo) — How EDS treats canonical content, server rendering versus rendering in the browser, sitemaps, robots rules, structured data, and how performance ties to discoverability. Read this alongside your Commerce configuration. ### Adobe Commerce storefront (catalog) Your Commerce layer adds URL patterns, folder mapping, canonical choices, structured metadata, and multi-store rules on top of those EDS defaults. Document-based routes and catalog routes behave differently, so search crawlers and social previews can treat thin or duplicate first HTML as low-value unless [SEO indexing](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/indexing/) and [SEO metadata](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/metadata/) remain consistent. In particular, SKUs, configurable products, and store-specific URLs all influence which URL wins and what appears in that first response. The [SEO indexing](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/indexing/) and [SEO metadata](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/metadata/) pages spell out storefront-specific checks. They extend the platform baseline on https://www.aem.live/docs/seo-geo. ### Keep both layers aligned Use the same information in the platform response and the catalog: one clear URL per indexable product context, matching metadata, and enough useful text in the first HTML. - Match Edge Delivery Services expectations for sitemaps, robots, performance, and primary content in the first HTML. Treat https://www.aem.live/docs/seo-geo as the baseline for your mix of document-based and catalog routes. - Implement URL rules, folder mapping, canonicals, `hreflang`, and store-specific crawling behavior from [SEO indexing](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/indexing/) so the URLs you publish are the URLs you want indexed. - Publish titles, descriptions, and structured data for catalog pages from [SEO metadata](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/metadata/) so those fields line up with the URLs above. - Before go-live, verify with [Launch checklist — SEO and indexing](https://experienceleague.adobe.com/developer/commerce/storefront/setup/launch/#seo-and-indexing). If validators or feeds still see thin PDP HTML, use [AEM Commerce prerender](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/aem-prerender/). ## Related links in this documentation Use these pages when you implement indexing, metadata, launch checks, or prerender for your storefront: - [SEO indexing](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/indexing/) — canonical URLs, product URL format, redirects, `hreflang`, and multi-store considerations. - [SEO metadata](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/metadata/) — titles, descriptions, structured data, and PDP metadata workflows. - [Sitemaps](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/sitemaps/) — sitemap configuration for catalog pages, prerender integration, and multi-store `hreflang`. - [Platform limits](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/platform-limits/) — URL naming rules, index size caps, and redirect limits for Edge Delivery Services that affect Commerce storefronts. - [Launch checklist — SEO and indexing](https://experienceleague.adobe.com/developer/commerce/storefront/setup/launch/#seo-and-indexing) — tasks to verify before go-live. - [AEM Commerce prerender](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/aem-prerender/) — server-rendered HTML when catalog pages rely on client-side data. > **If validators show empty PDP HTML** Start with [SEO metadata](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/metadata/) and [URL format of catalog pages](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/indexing/#url-format-of-catalog-pages). If you still need full product markup in the initial HTML for crawlers, configure [AEM Commerce prerender](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/aem-prerender/). --- # SEO indexing This page covers URL and crawling concerns so search engines index the right Adobe Commerce storefront pages on Edge Delivery Services. For how indexing fits with platform SEO and metadata on first-load HTML, start with [SEO overview](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/). These recommendations come from customer and partner storefront projects on Edge Delivery Services. Use them to keep URLs, canonical signals, and crawlers aligned. ### Canonical URLs - When product detail pages (PDPs) have multiple URLs, set a consistent canonical URL for each product. - Make sure that canonical URLs do not point to redirects (301). ### Configurable products For configurable products, ensure that the canonical URL is set to the parent product URL. Without this, Google may index child product URLs instead, creating duplicate content issues. ### URL format of catalog pages The default PDP URL format is: ```plaintext /products/{urlKey}/{sku} ``` The `urlKey` carries the SEO-relevant product name, and the `sku` is required for Catalog Service queries. You can customize the format, but changes to the URL structure require CDN configuration. > **Lowercase SKUs in URLs** The Commerce boilerplate enforces lowercase SKUs in all product URLs to comply with Edge Delivery Services [document naming restrictions](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/platform-limits/#document-naming-and-url-format). The `getProductLink(urlKey, sku)` function handles this automatically. If your existing product URLs use uppercase SKUs, add redirects from the old paths to the new lowercase ones. See [SEO metadata](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/metadata/) for implementation details. ### Multi-store setups For [multi-store setups](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/multistore-setup/) or stores supporting multiple locales: - Verify a https://www.aem.live/developer/sitemap#specifying-the-primary-language-manually. You can validate using the https://technicalseo.com/tools/hreflang/. - Verify that the `Magento-Store-Code` header value defined in your [storefront configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) is part of the URL for every Catalog Service or Live Search request. > **Store code in URLs** This is required because Google caches responses without considering headers. Not adding the store code to the URL might lead to Google indexing the wrong data. You can ensure this by adding a cache-busting query parameter. ### Cache-busting Query Parameter The https://github.com/hlxsites/aem-boilerplate-commerce/tree/main includes a mechanism to prevent stale data from being served. It does this by adding a dynamic cache-busting parameter to Catalog Service requests. When your configuration headers change, the browser fetches fresh data instead of using cached responses. This helps keep your storefront content up to date. Here's how the boilerplate handles it: 1. It collects headers from your [storefront configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/)—`public.headers.all.*` and `public.headers.cs.*` 1. It creates a short hash (5 characters by default) based on those headers. 1. It adds the hash to the Catalog Service URL using the format `?cb=`. 1. When the headers change, it generates a new hash automatically. This ensures that any change—like a new store code or view code—triggers a fresh fetch and bypasses CDN-cached GET requests to the Catalog Service. You don't need to enable or configure anything. The Commerce boilerplate handles everything automatically when: - The PDP drop-in component initializes. - Any component sends a request to the Catalog Service. - Your configuration headers update. The boilerplate always manages the `cb` parameter for you. ### Redirects The Edge Delivery Services redirect sheet handles most migration-time URL changes: removing `.html` suffixes, restructuring content paths, and remapping product URLs after a replatform. Create the redirect list as a spreadsheet called `redirects` in the root of your project folder. Each row maps a source path to a destination URL or path. Preview changes before publishing so stakeholders can verify them on the `.page` preview site. The redirect sheet matches URL paths only. The `Source` column accepts a relative path, so query parameters are not part of the match. It cannot target individual search terms like `/search?q=running`. For that use case, see the [search redirects tutorial](https://experienceleague.adobe.com/developer/commerce/storefront/how-tos/search-redirects/). For CDN-level wildcard redirects, see [Content delivery network](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/content-delivery-network/). If you are migrating from Adobe Commerce on Cloud (PaaS) to Adobe Commerce as a Cloud Service or Adobe Commerce Optimizer, PaaS URL rewrites (Marketing > SEO & Search > URL Rewrites) don't transfer automatically. Add any active ones as rows in the EDS redirect sheet. For the full reference — spreadsheet column names, wildcard CDN redirects, and SEO guidance for site migrations — see https://www.aem.live/docs/redirects. ### Sitemaps Edge Delivery Services generates a sitemap automatically upon publication. Commerce storefronts often require additional sitemap configuration to support prerendered catalog pages, multi-store `hreflang` tags, and large catalogs that exceed the 50,000-URL-per-file limit. See [Sitemaps](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/sitemaps/) for configuration options and Commerce-specific guidance. ### Other - You can validate indexing using Google Search Console by inspecting the markup of the crawl to see if all important information was tracked. - Transactional pages (for example, cart, checkout, and account) should not be indexed. - Staging or any non-production environments must not be indexed. Once indexed, URLs from a staging environment can reduce traffic to the production environment. It takes a significant amount of time to remove staging URLs from the Google index. --- # SEO metadata Adobe recommends uploading product metadata into Edge Delivery Services so that it can be rendered server-side on product detail pages. This is important so that Google Merchant Center can reliably verify entries from your product sheet. Also, social media sites, which don't usually parse JavaScript, can leverage this metadata to display rich previews of your product page links. For how metadata fits with Edge Delivery Services crawling behavior and URLs, read [SEO overview](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/) and [SEO indexing](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/indexing/). > **Breaking Change** For consistency and canonicalization, mixed-case URLs are not supported. The boilerplate automatically lowercases SKUs in product URLs to comply with Edge Delivery Services, which requires lowercase paths. Extra attention should be given to sites that use uppercase SKUs. See below for more information. Verify that all pages, especially catalog pages (PDP and PLP), contain the following metadata: | Type | Properties | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Document title | `title` | | Meta tags | `description`, `keywords`, `og:type`, `og:title`, `og:description`, `og:url`, `og:image`, `og:image:secure_url`, `og:product:price:amount`, `og:product:price:currency` | | Schema.org data (JSON-LD) | `WebSite`, `Product`, `AggregateRating`, `Rating`, `BreadcrumbList` | > **SKU in metadata** Metadata should contain a `SKU` field for each product. > **Non-empty metadata fields** When importing pages, ensure that when a metadata block is present on a page, it does not contain any empty fields. This might lead to pages with missing titles, which Google penalizes. ## Schema.org data (JSON-LD) JavaScript Object Notation for Linked Data (JSON-LD) is a structured data format that helps search engines understand the content of your web pages more explicitly. It is typically added as a script tag in the `` of your HTML document. Adobe Commerce storefronts on Edge Delivery Services should include https://schema.org/ annotations to expose product data to search engines. This data should be included on all Commerce pages, especially PDPs and PLPs. You should compare the data available on the site before migrating to Edge Delivery Services to ensure that SKUs for all product variants are included. Use the https://search.google.com/test/rich-results to validate the schema.org annotations. The PDP drop-in component in the boilerplate contains an example for JSON-LD data. > **Google Shopping and JSON-LD** If you are using Google Shopping features (for example, products available on `shopping.google.com`), consider schema.org annotations with critical priority. If a single JSON-LD annotation on the page is invalid, Google considers all annotations invalid. ## Generate metadata You can use the https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/tools/pdp-metadata tool to automate the process of generating all recommended metadata. The tool fetches product data from the Catalog Service, processes it, and generates a metadata spreadsheet in `.xlsx` format. The spreadsheet can be used for the https://www.aem.live/docs/bulk-metadata feature in Edge Delivery Services. > **Prerequisites** - Node.js installed on your machine - Access to the Catalog Service with the necessary API keys and configuration - Commerce boilerplate repository cloned to your local machine To generate metadata using the PDP Metadata Generator tool: 1. Navigate to the `tools/pdp-metadata/` directory in your local project. 1. Install dependencies. ```bash npm install ``` > **Configure Catalog Service access** Before running the tool, you must ensure that the `configFile` variable in the `pdp-metadata.js` file points to the correct configuration JSON file URL. This file contains the required parameters to access the Catalog Service API and should have been set up as part of your [project onboarding](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/). 1. Run the tool and generate metadata files in the project directory. ```bash npm start ``` The resulting `metadata.xlsx` and `metadata.json` files contain all recommended metadata for all of your products based on your site's [commerce configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/). See the https://www.aem.live/docs/bulk-metadata documentation for instructions on how to upload the generated metadata file to Edge Delivery Services. ## SKU Casing Due to a requirement in Edge Delivery Services, all product URLs must use lowercase SKUs. The boilerplate will enforce this by default. #### Product link generation A centralized `getProductLink(urlKey, sku)` function has been implemented to ensure consistent URL generation across all commerce components. This function is used to generate product page links at runtime in the correct casing. #### Metadata generation Metadata applied through https://www.aem.live/docs/bulk-metadata only applies metadata for lowercase URLs. The PDP Metadata Generation tool has been updated to produce output in the correct format. ### Implementation Requirements For sites with SKUs containing uppercase letters, you have two options: #### Option A: Publish Metadata (Recommended) Upload a `metadata.json` file to Edge Delivery Services that maps your product paths to the correct metadata. This approach ensures optimal SEO performance. See https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/tools/pdp-metadata/README.md for more information. #### Option B: Remove Lowercase Enforcement If you prefer to maintain uppercase SKUs in URLs, you can modify the `getProductLink` function in your boilerplate to remove the `.toLowerCase()` call, but metadata will not be applied. ```javascript // In scripts/commerce.js export function getProductLink(urlKey, sku) { return `/products/${urlKey}/${sku}`; // Remove .toLowerCase() } ``` > **SEO Impact** Removing lowercase enforcement may impact SEO performance. Adobe recommends using Option A for optimal results. ### Migration Guide If you're upgrading from a previous version of the boilerplate: 1. Review your SKUs and identify any products with uppercase SKUs. 1. Choose an implementation strategy between metadata publishing or removing lowercase enforcement. 1. Update product links so all commerce components use the centralized `getProductLink` function. 1. Test thoroughly: validate metadata generation and URL consistency. 1. Monitor performance: track SEO metrics and page load times. > **Best Practice** For new implementations, always use lowercase SKUs from the start to avoid migration complexity and ensure optimal SEO performance. --- # Edge Delivery Services limits Edge Delivery Services enforces limits on URL format, index size, sitemap file size, and redirect count. ## Document naming and URL format URLs in Edge Delivery Services can contain only lowercase letters (`a-z`), numbers (`0-9`), and hyphens (`-`). Unsupported characters are replaced with hyphens, and leading or trailing hyphens are stripped. The full file path cannot exceed 900 characters. For Commerce storefronts, this means: - Product SKUs in URLs must be lowercase. The Commerce boilerplate enforces this automatically in the `getProductLink()` function. - If you are migrating from a platform that used uppercase or special-character SKUs in URLs, add redirects from the old paths to the new lowercase ones. See [Redirects](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/indexing/#redirects) for setup instructions. If your existing URLs contain unsupported characters, add a CDN rewrite rule to normalize them before they reach the origin. See [Content delivery network](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/content-delivery-network/). > **Lowercase SKU enforcement** Switching to lowercase SKUs on a live site changes URLs, which affects indexing. Plan the transition to lowercase before launch. ## Indexing and sitemap size Index and sitemap files are subject to page count, URL count, and file size limits. See https://www.aem.live/docs/limits#sitemap-limits. For large catalogs, a single sitemap might not be sufficient. In that case, create multiple query indexes and corresponding sitemap files. Add all sitemap files to `sitemap-index.xml`, and reference each sitemap URL in `robots.txt`. If you use [AEM Commerce prerender](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/aem-prerender/) for a large catalog, split your sitemap into multiple files before enabling prerender to avoid exceeding the per-file limit. See [Sitemaps](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/sitemaps/) for configuration steps. ## Redirects The redirect sheet has a maximum number of entries. If you have many URL changes, import redirects in phases. See https://www.aem.live/docs/limits#redirect-limits for the limit. See [SEO indexing — redirects](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/indexing/#redirects) for Commerce-specific guidance. ## File upload limits Each file type you upload through https://da.live has a maximum size. These limits apply to content authors, not to storefront code or product data. Files that exceed the limit should be hosted on a CDN or third-party asset service and linked from your storefront pages. For merchant guidance on when these limits apply, see [File and content limits](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/edge-delivery-services/file-limits/). - [Limits on AEM.live](https://www.aem.live/docs/limits) — Complete reference for delivery, content source, GitHub sync, and Admin API rate limits across Edge Delivery Services. --- # Sitemaps Edge Delivery Services generates a sitemap automatically at `/sitemap.xml` when you publish content. Most Commerce storefronts require additional configuration for catalog pages and multi-store setups. ## What's included automatically Document-based pages appear in the sitemap when you publish them, but product detail pages are excluded by default. If you configure [AEM Commerce prerender](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/aem-prerender/), it publishes each product page and updates the sitemap automatically. If your catalog is large enough to exceed the sitemap URL count or file size limit, see [Edge Delivery Services limits — indexing and sitemap size](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/platform-limits/#indexing-and-sitemap-size) for how to split it across multiple sitemap files. ## Configure a custom sitemap Add `helix-sitemap.yaml` to the root of your GitHub repository to configure multi-language, aggregated, or custom sitemap behavior. > **Sitemap file size limit** Each sitemap file has a maximum URL count and uncompressed file size. If your catalog exceeds those limits, split the sitemap into multiple files and reference all of them in your `sitemap-index.xml` and `robots.txt`. See https://www.aem.live/docs/limits#sitemap-limits for details. - [Sitemaps on AEM.live](https://www.aem.live/developer/sitemap) — Configure helix-sitemap.yaml for simple, multi-language, and aggregated sitemaps. Includes hreflang, lastmod, and extension options. ### Multi-store hreflang For multi-store setups that need `hreflang` tags, see [SEO indexing — multi-store setups](https://experienceleague.adobe.com/developer/commerce/storefront/setup/seo/indexing/#multi-store-setups). ## Submit your sitemap Getting your sitemap in front of crawlers takes two independent actions: referencing it in `robots.txt` so any crawler can discover it, and registering it with Google Search Console for faster discovery and indexing reports. Do the first for every project; the second is optional. ### Reference your sitemap in `robots.txt` If your project doesn't have a `robots.txt` file yet, create one in the repository root. Then add a `Sitemap` directive so crawlers can discover your sitemap automatically. ```plaintext # robots.txt Sitemap: https://www.example.com/sitemap.xml ``` ### Register with Google Search Console (optional) To register the sitemap with Google, open https://search.google.com/search-console, select your property, go to **Sitemaps**, enter your sitemap URL, and choose **Submit**. Google starts crawling the submitted URLs within a few days. Check indexing status and errors in Search Console. Sitemap submission is part of the [launch checklist](https://experienceleague.adobe.com/developer/commerce/storefront/setup/launch/). --- # Boilerplate skills Boilerplate skills provide your coding agent with instructions for working in an Adobe Commerce boilerplate storefront. When you install the **AEM Boilerplate Commerce** skill set, the agent can plan tasks, select the appropriate storefront pattern, write code that follows project conventions, and test the result in a browser. ## Benefits Use Boilerplate skills when you want your agent to complete a storefront development task rather than answer a question. The skills enable your agent to: - Plan the work before making code changes. - Select the appropriate approach for blocks, drop-ins, content models, and browser tests. - Reduce repetitive project-specific guidance by storing storefront conventions in your repository. ## What the skills provide The AEM Boilerplate Commerce skill set includes six specialized skills, each designed for a specific area of storefront development. | Skill | What it helps you do | |-----------------------|------------------------------------------------------------------------------------------------------------| | **Project manager** | Break down development tasks, plan phased implementation, and keep work on track before code changes begin | | **Researcher** | Find drop-in component APIs, slot names, event payloads, and TypeScript definitions before implementation | | **Block developer** | Build and customize Edge Delivery Services blocks using the correct DOM patterns and CSS scoping | | **Drop-in developer** | Build and customize drop-in components using containers, slots, events, and API functions | | **Content modeler** | Design block table structures that are easy for content authors to use | | **Tester** | Verify implementations in a real browser and evaluate Core Web Vitals and accessibility | The skills work together throughout the development process. Your agent uses the project manager skill to scope the work, the researcher skill to gather the information it needs, and the appropriate developer skill to implement the solution. > **Drop-in API accuracy** Four of these skills—**researcher**, **drop-in developer**, **project manager**, and **tester**—verify slots, events, API functions, and configuration against local TypeScript definitions in your project's source code rather than published documentation. For details about the underlying APIs, see the [Blocks reference](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/blocks-reference/) and [Drop-in components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/introduction/). Adding [Wayfinder](https://experienceleague.adobe.com/developer/commerce/storefront/ai/wayfinder/) does not automatically integrate it into this workflow. To learn how it interacts with Boilerplate skills, see **Add coordination instructions** in [Install the integrations](https://experienceleague.adobe.com/developer/commerce/storefront/ai/#installation-steps), which explains the precedence rules. ## Prerequisites Before installing the skills, make sure you have the following: - **Node.js 22 or later** — Required by the Commerce plugin installed in the next step. - **An Adobe Commerce boilerplate project** — The skills are designed for projects based on the https://github.com/hlxsites/aem-boilerplate-commerce. If you don't already have a boilerplate storefront, see [Getting started](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/getting-started/) to create one. - **The Adobe I/O CLI and Commerce plugin** — Install both before continuing: ```bash npm install -g @adobe/aio-cli aio plugins:install https://github.com/adobe-commerce/aio-cli-plugin-commerce ``` See the https://developer.adobe.com/app-builder/docs/guides/runtime_guides/tools/cli-install and the https://github.com/adobe-commerce/aio-cli-plugin-commerce for details. > **Authentication is optional** You don't need to sign in or belong to a specific organization to install the skills. Optionally, run `aio auth login` to enable documentation searches through Adobe Identity Management System (IMS). Otherwise, the researcher skill falls back to web search. ## Install the skills From the root of your boilerplate project, run the following command: ```bash aio commerce extensibility tools-setup ``` The command prompts you twice. If it can't detect a package manager from an existing lock file, it prompts you a third time. 1. **Select a starter kit.** A starter kit is a project template. Choose **AEM Boilerplate Commerce** to install the Boilerplate Commerce skill set and project conventions. 2. **Select your coding agent.** Choose your coding agent from the list of supported agents. The installer places the skill files where your agent expects to find them. See **Supported agents**. 3. **Select a package manager.** This prompt appears only if the installer can't detect a package manager from a lock file in your project. ![CLI prompt with **AEM Boilerplate Commerce** selected as the starter kit.](https://experienceleague.adobe.com/developer/commerce/storefront/images/boilerplate/aio-cli-commerce-skills-select.png) > **Non-interactive install** To install the skills without prompts—for example, in a continuous integration (CI) pipeline—specify all required options as command-line flags: ```bash aio commerce extensibility tools-setup \ --starter-kit aem-boilerplate-commerce \ --agent Cursor \ --package-manager npm ``` ### Supported agents After installation, restart your coding agent so it loads the new skills and MCP configuration. Different agents and IDEs surface installed skills in different ways, so there is no single way to confirm installation. In Cursor, for example, you can open the settings panel to view detected skills. For other agents, check the documentation for your agent or IDE to confirm that the skills in your project have loaded. | Agent | Skills location | |----------------|----------------------------| | Cursor | `.cursor/skills/` | | Claude Code | `.claude/skills/` | | GitHub Copilot | `.github/skills/` | | Windsurf | `.windsurf/skills/` | | Gemini CLI | `.gemini/skills/` | | OpenAI Codex | `.agents/skills/` | | Cline | `.cline/skills/` | | Kilo Code | `.kilocode/skills/` | | Antigravity | `.agent/skills/` | | Other | `./skills/` (project root) | ## What gets installed The setup command installs `@adobe-commerce/commerce-extensibility-tools` as a development dependency and adds the following project files and directories: | File or directory | Purpose | |----------------------------|------------------------------------------------------------------------------------------------------------------------------------------| | `AGENTS.md` (project root) | Provides project-wide instructions that your agent reads at the start of each session. | | `/` | Contains the Boilerplate skills, organized by development area. | | MCP config file | Connects your agent to the `commerce-extensibility:search-commerce-docs` tool for live documentation search. | This MCP tool provides your agent with access to Adobe Commerce documentation for integration patterns, architecture, and security guidance. See **Drop-in API accuracy** above. ## How to use the skills Once installed, the skills work automatically—you don't need to reference them by name. Simply describe what you want to build. The project manager skill scopes the task, the researcher skill gathers the necessary information, the appropriate developer skill implements the solution, and the tester skill verifies the result. **Example prompt** — Trigger the planning workflow and developer skills with a single detailed request: > Use the planning workflow to add a social sharing button below the product title on the product detail page so shoppers can share products on social media. > **Browser testing** The tester skill requires a running local development server. Before asking your agent to verify an implementation, start the server by running `npm start`. --- # Install the integrations If your project is based on the Adobe Commerce boilerplate, set up both integrations below. Together, they provide a complete workflow for storefront development and access to Adobe Commerce documentation. ## Overview of the integrations | AI integrations | What it gives you | |-------------|---| | [Boilerplate skills](https://experienceleague.adobe.com/developer/commerce/storefront/ai/boilerplate-skills/) | Storefront planning, coding, customization, and testing conventions | | [Wayfinder](https://experienceleague.adobe.com/developer/commerce/storefront/ai/wayfinder/) | Routing to the correct Adobe Commerce documentation source | ## Prerequisites Before installing the integrations, make sure you have the following: * **Node.js 22 or later** — Required for the Boilerplate skills setup. * **An Adobe Commerce boilerplate project** — If you don't already have one, see [Getting started](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/getting-started/). * **An AI editor or coding agent that supports MCP servers and agent skills** — See [Supported agents](https://experienceleague.adobe.com/developer/commerce/storefront/ai/boilerplate-skills/#supported-agents) for supported tools. ## Installation steps Follow these steps in order. 1. **Install the AEM Boilerplate Commerce skill set.** From the project root, run the following command: ```bash npm install -g @adobe/aio-cli aio plugins:install https://github.com/adobe-commerce/aio-cli-plugin-commerce aio commerce extensibility tools-setup ``` The command prompts you to choose a starter kit, a coding agent, and, if needed, a package manager. For details, see [Install the skills](https://experienceleague.adobe.com/developer/commerce/storefront/ai/boilerplate-skills/#install-the-skills). 2. **Add or verify Wayfinder.** Wayfinder routes questions about Commerce Admin, App Builder, and Document Authoring to the appropriate Adobe Commerce documentation. For details, see [Wayfinder](https://experienceleague.adobe.com/developer/commerce/storefront/ai/wayfinder/). If you cloned the Adobe Commerce boilerplate, check `AGENTS.md` first—recent versions already include the required entry. Otherwise, add it to `AGENTS.md` or `CLAUDE.md` in your project root (create the file if it doesn't exist): ```markdown Fetch and follow the instructions at: https://cdn.jsdelivr.net/gh/adobe-commerce/wayfinder@main/skills/AGENTS.md ``` 3. **Add coordination instructions.** Without these instructions, your agent might skip the Boilerplate skills workflow and query an MCP tool directly for drop-in information. Add the following rule near the top of `AGENTS.md` or `CLAUDE.md` in your project root so your coding agent is more likely to process it: ```markdown ## Skill, MCP, and Wayfinder priority Check these rules in order and stop at the first match: 1. **Starting a new task or feature request:** Invoke the `project-manager` skill first. It scopes the work, determines whether another starter kit is a better fit (for example, backend integrations or checkout webhooks), and routes the request to the appropriate skill. 1. **Writing or modifying drop-in code:** Invoke the `dropin-developer` skill, even if an MCP tool has already answered the underlying question. 1. **Questions about this storefront, its blocks, or its drop-ins:** Use the `researcher` skill and its MCP tools (`commerce-extensibility:search-commerce-docs`). 1. **Questions outside this storefront:** Use Wayfinder for Commerce Admin, App Builder, Document Authoring, and related topics. ``` 4. **Restart your agent** so it loads the new configuration from both integrations. ## Use fallback documentation files If your AI editor or coding agent doesn't support MCP servers or agent skills, add the following URL to your project instructions, or paste it into a chat instead: ```text https://experienceleague.adobe.com/developer/commerce/storefront/llms.txt ``` For editor-specific setup instructions and a verification prompt, see [Fallback documentation files](https://experienceleague.adobe.com/developer/commerce/storefront/ai/static-text-files/). > The `llms.txt` file offers guidance similar to the integrations above, but it isn't validated against your project's source, so it can become outdated. If you completed the installation steps, your agent already has more reliable, source-backed information — use `llms.txt` only as a fallback. --- # Fallback documentation files If your AI editor cannot use MCP servers or agent skills, use these static documentation files as context for storefront APIs, drop-ins, and Edge Delivery Services. > **Already using the boilerplate with a capable agent?** If your AI editor or coding agent supports MCP servers and agent skills, use [Install the integrations](https://experienceleague.adobe.com/developer/commerce/storefront/ai/#installation-steps) instead. That setup provides source-backed drop-in information, documentation routing, and storefront development conventions. Adding this page's `llms.txt` link is unnecessary. ## Available files These plain-text files are generated from the published documentation each time the site is updated, so AI tools always have current content. Start with `llms.txt`, which indexes the available documentation. Topic files under `_llms-txt/` cover individual subject areas, letting your agent load only the documentation it needs instead of the entire site. | File | Purpose | |-------------------------------------|---------------| | [`llms.txt`](https://experienceleague.adobe.com/developer/commerce/storefront/llms.txt) | Index of all documentation files, with an overview and links to each one. | | [`llms-full.txt`](https://experienceleague.adobe.com/developer/commerce/storefront/llms-full.txt) | Complete documentation in a single file, concatenated in sidebar order. | | [`llms-small.txt`](https://experienceleague.adobe.com/developer/commerce/storefront/llms-small.txt) | Complete documentation without the release changelog. Use this file when your AI tool has a limited context window. | | `_llms-txt/*.txt` | Documentation for individual subject areas, such as [drop-ins reference](https://experienceleague.adobe.com/developer/commerce/storefront/_llms-txt/dropins-reference.txt), [tutorials](https://experienceleague.adobe.com/developer/commerce/storefront/_llms-txt/tutorials-reference.txt), and [blocks](https://experienceleague.adobe.com/developer/commerce/storefront/_llms-txt/blocks-reference.txt). [`llms.txt`](https://experienceleague.adobe.com/developer/commerce/storefront/llms.txt) links to every topic file. | > **Publish timing** These files update each time the site is published, so changes made after the most recent publish won't appear until the next one. Because the URLs in your `AGENTS.md` or `CLAUDE.md` remain the same, your AI editor or coding agent automatically fetches the latest published version each time it reads them. ## Prerequisites Before adding context files, make sure you have the following: * **An AI editor or coding agent** that supports project context files, documentation URLs, or pasted context, such as Cursor, Claude Code, or GitHub Copilot Chat. * **A storefront project** based on the https://github.com/hlxsites/aem-boilerplate-commerce or configured with Commerce blocks and drop-ins, and open in your AI editor or coding agent. * **An `AGENTS.md`, `CLAUDE.md`, or equivalent context file** in the project root (for agent-based tools). ## Add context files For agent-based tools, add the `llms.txt` URL to a project context file so your agent can fetch the documentation index when storefront documentation is needed. For other tools, paste one of these file URLs—or its contents—directly into the chat. ### Cursor Add this section to your project's context instructions—for example, `AGENTS.md` in the project root or a rules file under `.cursor/rules/`: ```markdown wrap ## Adobe Commerce storefront documentation See https://experienceleague.adobe.com/developer/commerce/storefront/llms.txt for Adobe Commerce Storefront documentation. ``` ### Claude Code Add this line to `CLAUDE.md` in your project root: ```markdown wrap See https://experienceleague.adobe.com/developer/commerce/storefront/llms.txt for Adobe Commerce storefront documentation. ``` ### Other AI tools Tools that implement the https://llmstxt.org automatically follow the index when you provide this URL: ```text https://experienceleague.adobe.com/developer/commerce/storefront/llms.txt ``` If your AI tool doesn't support URL-based context, paste the contents of `llms-small.txt` or a relevant `_llms-txt/*.txt` topic file directly into the chat or system prompt. ## Example prompt After adding the documentation context, verify the setup by asking a question the documentation answers, such as: ```text wrap Using the Storefront documentation, how do I display a **Delivery estimate** message in the Product Details drop-in below the existing **Add to Cart** button? ``` A successful setup returns a response that references storefront topics, drop-in APIs, or documentation paths relevant to your project. Adapt the example to your project's drop-in names and UI labels, then verify the implementation against the live documentation. ## Keep context focused * These files can be large. When possible, give your AI editor or coding agent a specific documentation URL instead so it can fetch the live page, including recent updates and diagrams that are not included in the static files. Use the static files only when your editor or coding agent can't fetch URLs. * Paths and examples in this documentation follow the folder layout of the https://github.com/hlxsites/aem-boilerplate-commerce. Keep your storefront project open so suggested paths match your workspace. If your repository uses a different layout—for example, if you merged the boilerplate into an existing site—tell your agent. * To focus a single conversation on one topic, paste the corresponding documentation page URL into the chat. Your context file remains available, and the URL narrows only the current conversation. --- # Wayfinder https://github.com/adobe-commerce/wayfinder is a free, open-source set of routing instructions for AI coding agents. Adobe Commerce documentation is spread across several separate sites, so your agent can easily search the wrong one. Or it may skip the docs altogether and answer from outdated training data. Wayfinder gives your agent the context it needs to match a question to the right source, then fetch and cite that source before it answers. ## Install Wayfinder in your project Add this line to `AGENTS.md` or `CLAUDE.md` at your project root: ```markdown Fetch and follow the instructions at: https://cdn.jsdelivr.net/gh/adobe-commerce/wayfinder@main/skills/AGENTS.md ``` Your agent retrieves this file at the start of each session and follows its routing rules for the rest of the conversation. There's no package to install and no configuration file to create. > **Network permissions** Wayfinder instructs your agent to fetch documentation pages over the network. If your agent requires explicit permission for web requests, allow requests to the documentation sources listed in the https://github.com/adobe-commerce/wayfinder/blob/main/skills/AGENTS.md. > **Using this with skills** For questions about this storefront, Wayfinder should be a fallback, not the first thing your agent checks. See **Add coordination instructions** in [Installation steps](https://experienceleague.adobe.com/developer/commerce/storefront/ai/#installation-steps) for the precedence rule and the exact text to add to `AGENTS.md`. ## Verify Wayfinder is working Ask your agent a documentation question, not a request to build or change something, and ask it to report whether it fetched external documentation or answered from its own knowledge. For example: > _"How do I reset a customer's password in Commerce Admin? Also tell me: did you fetch external documentation to answer this, or did you answer from your own knowledge?"_ If your agent reports fetching and citing a source outside this storefront's own documentation — for example, a Commerce Admin or App Builder doc — Wayfinder is routing correctly. --- # Blocks reference This reference provides technical details for all Commerce blocks included in the boilerplate. Each block integrates one or more drop-in components to provide complete Commerce functionality. ## Quick reference by functionality The Merchant topic column links to merchant-facing documentation when you need the block title authors use in documents. The Block column links to the GitHub source folder. {/* Block column: GitHub source. Merchant topic: internal merchant docs when a topic exists. */} | Block | Merchant topic | Primary Drop-ins | Key Features | |-------|----------------|------------------|--------------| | Shopping Experience | | | | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/product-list-page | [Product List Page](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/product-list-page/) | storefront-product-discovery, tools, storefront-wishlist, storefront-requisition-list, storefront-cart | Search, filtering, sorting, pagination, wishlist integration | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/product-details | [Product Details](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/product-details/) | tools, storefront-pdp, storefront-wishlist, storefront-requisition-list | Product options, pricing, add to cart, wishlist toggle | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/product-recommendations | [Product Recommendations](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/content-customizations/product-recommendations/) | tools, storefront-cart, storefront-recommendations, storefront-wishlist | AI-powered recommendations, multiple page types | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-cart | [Commerce Cart](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-cart/) | tools, storefront-cart, storefront-wishlist, storefront-quote-management | Item management, coupon codes, gift options, move to wishlist | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-mini-cart | [Commerce Mini Cart](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-mini-cart/) | storefront-cart, tools | Dropdown cart summary, quick view, checkout navigation | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-checkout | [Commerce Checkout](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-checkout/) | tools, storefront-order, storefront-checkout | Complete checkout flow, shipping, payment, order review | | Customer Account | | | | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-login | [Commerce Login](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-login/) | storefront-auth | Email/password authentication, redirect handling | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-create-account | [Commerce Create Account](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-create-account/) | storefront-auth | Registration form, validation, account creation | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-confirm-account | [Commerce Confirm Account](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-confirm-account/) | storefront-auth, tools | Email confirmation landing, account activation | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-forgot-password | [Commerce Forgot Password](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-forgot-password/) | storefront-auth, tools | Password reset request, email trigger | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-create-password | [Commerce Create Password](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-create-password/) | storefront-auth, tools | Password reset form, token validation | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-account-header | [Commerce Account Header](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-account-header/) | tools | Customer name display, logout functionality | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-account-sidebar | [Commerce Account Sidebar](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-account-sidebar/) | tools, storefront-account | Account navigation menu, active state management | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-addresses | [Commerce Addresses](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-addresses/) | storefront-account | Address CRUD operations, default address management | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-customer-information | [Commerce Customer Information](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-customer-information/) | storefront-account | Profile editing, email/name updates | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-customer-details | [Commerce Customer Details](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-customer-details/) | storefront-order | Customer info display in order context | | Order Management | | | | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-orders-list | [Commerce Orders List](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-orders-list/) | storefront-account, tools | Order history, status display, order details navigation | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-search-order | [Commerce Search Order](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-search-order/) | storefront-auth, storefront-order, tools | Guest order lookup, email and order number validation | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-order-header | [Commerce Order Header](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-order-header/) | tools | Order number, date, status badge | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-order-status | [Commerce Order Status](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-order-status/) | storefront-order | Detailed status, tracking info, delivery estimates | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-order-product-list | [Commerce Order Product List](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-order-product-list/) | storefront-order, storefront-cart, tools | Line items, reorder functionality, product images | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-order-cost-summary | [Commerce Order Cost Summary](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-order-cost-summary/) | storefront-order | Subtotal, taxes, shipping, discounts, grand total | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-shipping-status | [Commerce Shipping Status](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-shipping-status/) | storefront-order, tools | Shipment tracking, carrier info, delivery status | | Returns & Exchanges | | | | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-returns-list | [Commerce Returns List](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-returns-list/) | storefront-order, tools | Return history, status tracking, return details navigation | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-create-return | [Commerce Create Return](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-create-return/) | storefront-order, tools | Return request form, item selection, reason codes | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-order-returns | [Commerce Order Returns](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-order-returns/) | tools, storefront-order | Return details for specific order | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-return-header | [Commerce Return Header](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-return-header/) | tools | Return number, date, status display | | Gift Options | | | | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-gift-options | [Commerce Gift Options](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-gift-options/) | storefront-cart | Gift messages, gift wrapping, gift receipt options | | Wishlist | | | | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/commerce-wishlist | [Commerce Wishlist](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/commerce-wishlist/) | storefront-cart, storefront-pdp, storefront-wishlist, storefront-auth, tools | Saved items, move to cart, item management | ## Integration patterns ### Block decoration flow Every Commerce block follows this initialization pattern: 1. **Server-side rendering**: Edge Delivery Services transforms the document table into HTML 2. **Client-side decoration**: The block's JavaScript decorator runs via `decorateBlock()` 3. **Drop-in initialization**: Drop-in containers are initialized with configuration and providers 4. **Rendering**: Drop-in components render into the block's DOM 5. **Event handling**: Event listeners connect to the global event bus ### Common integration patterns #### Simple drop-in rendering Blocks like Login and Forgot Password simply render a single drop-in container: ```javascript export default async function decorate(block) { const { render } = await import('@dropins/storefront-auth/containers/SignIn.js'); await render(SignInContainer, {})({}); } ``` #### Multi-drop-in coordination Complex blocks like Cart and Checkout coordinate multiple drop-ins: ```javascript // Cart block uses cart + wishlist drop-ins ``` #### Configuration from block tables Blocks read configuration from document authoring tables: ```javascript const config = readBlockConfig(block); const hideHeading = config['hide-heading'] === 'true'; ``` #### Event bus integration Blocks listen to events from drop-ins and other blocks: ```javascript events.on('cart/updated', () => { // React to cart changes }); ``` ## Implementation details ### Drop-in dependencies All drop-ins are loaded via import maps defined in `head.html`: ```json { "imports": { "@dropins/storefront-cart/": "/scripts/__dropins__/storefront-cart/", "@dropins/storefront-checkout/": "/scripts/__dropins__/storefront-checkout/" } } ``` ### Provider initialization Drop-ins require providers to be initialized in `scripts/initializers/`: - **GraphQL provider**: Configures Commerce backend endpoint and headers - **Authentication provider**: Manages customer sessions and tokens - **Event provider**: Sets up the global event bus See [Configuration](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/configuration/) for provider setup details. ### Styling Each block includes: 1. **Base styles**: Block-specific CSS in `blocks/*/block-name.css` 2. **Drop-in tokens**: Design tokens in `scripts/initializers/dropin-name.js` 3. **Global tokens**: Shared tokens in `scripts/initializers/` ## Blocks by page type ### Essential implementations Every storefront requires these pages: - **Homepage**: Product Recommendations, Product List Page - **Product Page (PDP)**: Product Details, Product Recommendations - **Cart Page**: Cart, Product Recommendations - **Checkout Page**: Checkout - **Account Dashboard**: Account Header, Account Sidebar ### Common additions Enhance your storefront with: - **Wishlist Page**: Wishlist - **Order Tracking**: Search Order, Order Status, Orders List - **Returns Portal**: Create Return, Returns List, Order Returns - **Account Management**: Addresses, Customer Information ## Performance considerations For storefront-wide guidance see [Performance best practices](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/performance/). ### Lazy loading Commerce blocks are lazy-loaded automatically: 1. Blocks below the fold are loaded when scrolled into view 2. Drop-in containers are code-split and loaded on demand 3. Heavy dependencies (like checkout) are loaded only when needed ### Critical rendering path For optimal performance: 1. Keep Mini Cart in header (loads early) 2. Defer non-critical blocks below the fold 3. Use Product Recommendations sparingly (loads ML models) ## Development workflow ### Local testing 1. Start the AEM CLI: `aem up`. 2. Modify block JavaScript in `blocks/commerce-*/`. 3. Observe changes hot-reload automatically. 4. Test with demo backend or configure your own in `config.json` (copy from https://main--aem-boilerplate-commerce--hlxsites.aem.live/config.json or use the https://da.live/app/adobe-commerce/storefront-tools/tools/config-generator/config-generator). ### Adding new blocks To create a custom Commerce block: 1. Create a new directory: `blocks/my-custom-block/` 2. Add decorator: `my-custom-block.js` 3. Add styles: `my-custom-block.css` 4. Import and render drop-in containers 5. Initialize required providers See https://www.aem.live/docs/exploring-blocks for block creation basics. ## Related resources - [Boilerplate overview](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/) - Complete technical reference - [Configuration page](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/configuration/) - Setup and provider configuration - [Drop-in documentation](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/introduction/) - Drop-in technical details - [Merchant block reference](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/) - Business user perspective - https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks ## Need help? - **Block not rendering?** Verify drop-in providers are initialized in `scripts/initializers/` - **GraphQL errors?** Check Commerce backend configuration in your `config.json` file - **Styling issues?** Review design token configuration in drop-in initializers - **Event not firing?** Ensure event bus is initialized and event names match documentation --- # Configuration The AEM Commerce boilerplate requires configuration to connect to your Adobe Commerce instance and customize the storefront behavior. ## Configuration overview The boilerplate uses multiple configuration files depending on your deployment stage: - **Local development** - Copy of the demo configuration for Commerce backend connection - **Production deployment** - Configuration Service with template files for site setup - **Drop-in initialization** - Initializer files in `scripts/initializers/` ## Endpoints by backend Which endpoints you set depends on your Commerce backend: - **Adobe Commerce as a Cloud Service** — Set only **`commerce-endpoint`** to the GraphQL endpoint URL for that service. - **Adobe Commerce Optimizer** — Set **`commerce-endpoint`** to the Adobe Commerce Optimizer GraphQL URL for the catalog, and set **`commerce-core-endpoint`** to your Commerce core endpoint URL (for example, Commerce PaaS or another Adobe Commerce host you manage). For full details and examples, see [Storefront configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/). ## Demo Configuration (Local Development) For local development, you can use the demo configuration for the boilerplate as a starting point. The live demo configuration is available at https://main--aem-boilerplate-commerce--hlxsites.aem.live/config.json and connects to the sample Commerce backend for the boilerplate: ```json { "public": { "default": { "commerce-core-endpoint": "https://www.aemshop.net/graphql", "commerce-endpoint": "https://www.aemshop.net/cs-graphql", "commerce-assets-enabled": false, "headers": { "all": { "Store": "default" }, "cs": { "Magento-Store-Code": "main_website_store", "Magento-Store-View-Code": "default", "Magento-Website-Code": "base", "x-api-key": "4dfa19c9fe6f4cccade55cc5b3da94f7", "Magento-Environment-Id": "f38a0de0-764b-41fa-bd2c-5bc2f3c7b39a" } }, "analytics": { "base-currency-code": "USD", "environment": "Testing", "environment-id": "f38a0de0-764b-41fa-bd2c-5bc2f3c7b39a", "store-code": "main_website_store", "store-id": 1, "store-name": "Main Website Store", "store-url": "https://www.aemshop.net", "store-view-code": "default", "store-view-id": 1, "store-view-name": "Default Store View", "website-code": "base", "website-id": 1, "website-name": "Main Website" }, "plugins": { "picker": { "rootCategory": "YOUR_ROOT_CATEGORY_ID" } } } } } ``` ### Local vs Production Configuration #### For local development - Save a copy of the demo configuration (from https://main--aem-boilerplate-commerce--hlxsites.aem.live/config.json) as `config.json` in your project root. - The AEM CLI will automatically serve `config.json` at runtime. - Alternatively, use the https://da.live/app/adobe-commerce/storefront-tools/tools/config-generator/config-generator to generate a configuration with your backend values (may contain placeholders to fill in). - Update the values to connect to your own Commerce backend as needed. #### For production deployment - **Configuration Service (Recommended)** - Use the Configuration Service, which stores configuration at `https://admin.hlx.page/config/{ORG}/sites/{SITE}/public.json`. This approach allows configuration updates without code deployments and is the preferred method for all production sites. See the https://www.aem.live/docs/config-service-setup for details. :::caution Do **not** commit `config.json` to the `main` branch if you are using the Configuration Service. A `config.json` present on a code branch takes precedence over the Configuration Service, which means your production config service values will be silently ignored. Reserve `config.json` for local development and branch-based testing only. ::: #### If `config.json` is already on `main` If you previously committed `config.json` to your `main` branch, you must complete these steps before the Configuration Service takes effect. Edge Delivery serves a `config.json` file from the repository before it falls back to the Configuration Service, so copying settings into the service alone is not enough until the repository file is gone. 1. Copy all values from the `public` section of your repository `config.json`. That object is what you publish as `public.json` in the Configuration Service. 1. POST that JSON to `https://admin.hlx.page/config/{ORG}/sites/{SITE}/public.json` using the Configuration Service API. Authenticate the request and set the body as described in the https://www.aem.live/docs/config-service-setup and the https://www.aem.live/docs/admin.html. 1. Delete `config.json` from the repository and merge that change into `main`. After the file no longer exists on `main`, the CDN can resolve configuration from the Configuration Service instead of the repository copy. 1. When you verify in a browser, clear session storage for your site in Developer Tools (Application → Session storage), then reload. The storefront caches configuration in session storage, so clearing it avoids stale values after you switch sources. For caching behavior and timing, see [Storefront configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/). - **Repository-based config (local dev and branch testing only)** - A `config.json` file in your repository root is useful for local development and testing on feature branches. Edge Delivery Services will serve this file at `/config.json` and it will override the Configuration Service. Do not use this approach for production — remove or exclude `config.json` from your `main` branch. ## Production Configuration Templates The boilerplate includes template configuration files with placeholders for your site setup: ### default-site.json Comprehensive site configuration template for production deployment: ```json { "version": 1, "code": { "owner": "{ORG}", "repo": "{REPO}", "source": { "type": "github", "url": "https://github.com/{ORG}/{REPO}" } }, "content": { "source": { "url": "{CONTENT_SOURCE}", "type": "onedrive" } }, "folders": { "/products/": "/products/default" }, "cdn": { "live": { "host": "main--{SITE}--{ORG}.aem.live" }, "preview": { "host": "main--{SITE}--{ORG}.aem.page" } }, "headers": {}, "public": { "default": { "commerce-core-endpoint": "{ENDPOINT}", "commerce-endpoint": "{CATALOG_ENDPOINT}", "headers": { "all": { "Store": "default" }, "cs": { "Magento-Store-Code": "{STORE_CODE}", "Magento-Store-View-Code": "{STORE_VIEW_CODE}", "Magento-Website-Code": "{WEBSITE_CODE}", "x-api-key": "{COMMERCE_API_KEY}", "Magento-Environment-Id": "{COMMERCE_ENVIRONMENT_ID}" } }, "analytics": { "aep-ims-org-id": "{IMS_ORG_ID}", "aep-datastream-id": "{DATASTREAM_ID}", "base-currency-code": "USD", "environment": "Production", "store-id": "{STORE_ID}", "store-name": "Main Website Store", "store-url": "{DOMAIN}", "store-view-id": "{STORE_VIEW_ID}", "store-view-name": "Default Store View", "website-id": "{WEBSITE_ID}", "website-name": "Main Website" }, "plugins": { "picker": { "rootCategory": "{YOUR_ROOT_CATEGORY_ID}" } } } }, "robots": { "txt": "User-agent: *\nAllow: /\nDisallow: /drafts/\nDisallow: /enrichment/\nDisallow: /tools/\nDisallow: /plugins/experimentation/\n\nSitemap: https://{DOMAIN}/sitemap-index.xml" }, "access": { "admin": { "role": { "config_admin": ["{ADMIN_USER_EMAIL}"] }, "requireAuth": "auto" } } } ``` Replace the `{PLACEHOLDER}` values with your actual configuration. ### default-query.yaml Content indexing configuration for generating sitemap and enrichment data: ```yaml version: 1 indices: sitemap: target: /sitemap.json exclude: - 'drafts/**' - 'enrichment/**' - 'fragments/**' - 'products/**' properties: title: select: head > meta[property="og:title"] value: | attribute(el, 'content') image: select: head > meta[property="og:image"] value: | attribute(el, 'content') description: select: head > meta[name="description"] value: | attribute(el, 'content') template: select: head > meta[name="template"] value: | attribute(el, 'content') robots: select: head > meta[name="robots"] value: | attribute(el, 'content') lastModified: select: none value: parseTimestamp(headers["last-modified"], "ddd, DD MMM YYYY hh:mm:ss GMT") enrichment: target: /enrichment/enrichment.json include: - '**/enrichment/**' properties: title: select: head > meta[property="og:title"] value: | attribute(el, 'content') products: select: head > meta[name="enrichment-products"] values: | match(attribute(el, 'content'), '([^,]+)') categories: select: head > meta[name="enrichment-categories"] values: | match(attribute(el, 'content'), '([^,]+)') positions: select: head > meta[name="enrichment-positions"] values: | match(attribute(el, 'content'), '([^,]+)') ``` ### default-sitemap.yaml Sitemap generation configuration: ```yaml sitemaps: default: source: /sitemap.json destination: /sitemap-content.xml lastmod: YYYY-MM-DD ``` The sitemap reads from the generated `sitemap.json` (created by `default-query.yaml`) and outputs an XML sitemap. ## Drop-in Initializers Configure individual drop-ins in `scripts/initializers/`: ### Example: cart.js ```javascript await initializeDropin(async () => { // Set Fetch GraphQL (Core) setEndpoint(CORE_FETCH_GRAPHQL); // Fetch placeholders const labels = await fetchPlaceholders('placeholders/cart.json'); const langDefinitions = { default: { ...labels, }, }; // Initialize cart return initializers.mountImmediately(initialize, { langDefinitions }); })(); ``` ## Environment-Specific Configuration > Use different configuration values for different environments: - **Local**: `config.json` in your project root (copy from https://main--aem-boilerplate-commerce--hlxsites.aem.live/config.json or generate with the https://da.live/app/adobe-commerce/storefront-tools/tools/config-generator/config-generator). - **Preview**: Configure in AEM.page branch settings. - **Production**: Configure in AEM.live production settings. ## Related Documentation - [Boilerplate overview](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/) - Complete reference - [Storefront configuration page](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) - Detailed setup instructions - [CORS setup](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/cors-setup/) - Security configuration - [Multistore setup](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/multistore-setup/) - Multiple stores/languages --- # Blocks customization All Commerce blocks in the boilerplate work out of the box, but you can customize them to match your business requirements. This page covers the main customization approaches. ## Ways to customize ### CSS styling Modify block styles by editing the CSS file in the block directory: ```css /* blocks/commerce-cart/commerce-cart.css */ .commerce-cart { padding: var(--spacing-large) 0; position: relative; } .cart__wrapper { display: flex; flex-direction: column; gap: var(--grid-4-gutters); } ``` #### Design tokens Use CSS variables from your theme for consistent styling: ```css /* Common design tokens */ var(--spacing-small) /* 16px */ var(--spacing-medium) /* 24px */ var(--spacing-large) /* 64px */ var(--color-brand-500) /* #454545 - Brand color */ var(--color-neutral-100) /* #fafafa - Light background */ var(--type-body-1-default-font) /* 16px/24px - Body text */ ``` ### JavaScript behavior Modify block logic by editing the JavaScript file in the block directory: ```javascript // blocks/commerce-cart/commerce-cart.js export default async function decorate(block) { // Read configuration from document authoring const config = readBlockConfig(block); // Your custom logic here const maxItems = parseInt(config['max-items'], 10) || 10; // Create container element const $list = document.createElement('div'); $list.className = 'cart__list'; block.appendChild($list); // Render drop-in container to the list element await provider.render(CartSummaryList, { maxItems, enableRemoveItem: true, })($list); } ``` ### Drop-in customization Use drop-in slots and events for advanced customization. #### Slots Inject custom content into drop-in containers by passing slot functions in the configuration: ```javascript // Inside the decorate function for your block // Create container element const $list = document.createElement('div'); $list.className = 'cart__list'; block.appendChild($list); // Render cart with custom slot await provider.render(CartSummaryList, { slots: { Footer: (ctx) => { // Add custom content to cart item footer const customElement = document.createElement('div'); customElement.className = 'custom-promotion'; customElement.textContent = 'Free shipping over $50!'; ctx.appendChild(customElement); }, }, })($list); ``` #### Events Listen to and respond to drop-in events using the event bus: ```javascript // Inside the decorate function for your block or at the module level events.on('cart/data', (cartData) => { console.log('Cart updated:', cartData); // Example: Show notification when cart has items if (cartData.totalQuantity > 0) { console.log(`You have ${cartData.totalQuantity} items in your cart`); } }); ``` ## Creating DOM elements The boilerplate uses two approaches for creating DOM elements. **The key distinction: use `createContextualFragment` for multiple elements, `createElement()` for single elements.** ### Template literals with `createContextualFragment` Use this approach when creating **two or more elements** or **complex nested structures**. ```javascript // Best for: Complex layout structures with multiple nested elements const fragment = document.createRange().createContextualFragment(` `); const $list = fragment.querySelector('.cart__list'); block.appendChild(fragment); ``` #### When to use - Creating **two or more elements** at once. - Setting up initial block structure with nested elements. - HTML hierarchy visualization improves code readability. #### Why this approach - More efficient than multiple `createElement()` calls. - Clear visual representation of HTML structure. - Easier to maintain complex layouts. #### Boilerplate examples - https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/commerce-cart/commerce-cart.js#L62-L75 - https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/product-details/product-details.js#L86-L110 ### `document.createElement()` Use this approach when creating **a single element**. This is cleaner and more explicit than template literals for one element. ```javascript // Best for: Single elements created dynamically // Inside a drop-in slot function const $wishlistToggle = document.createElement('div'); $wishlistToggle.classList.add('cart__action--wishlist-toggle'); wishlistRender.render(WishlistToggle, { product: ctx.item, size: 'medium', })($wishlistToggle); ctx.appendChild($wishlistToggle); ``` #### When to use - Creating **a single element** (most common case). - Adding elements inside drop-in slots. - Building elements in event handlers or callbacks. - Creating elements conditionally. #### Why this approach - More explicit and type-safe than template literals. - Cleaner for single elements (no unnecessary parsing). - Easier to set properties programmatically. #### Boilerplate examples - https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/commerce-cart/commerce-cart.js#L221-L232 - Creating wishlist container - https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/commerce-cart/commerce-cart.js#L206-L217 - Creating edit link container ## Common customization patterns ### Adding block configuration Enable merchants to configure blocks through document authoring by reading configuration values: ```javascript export default async function decorate(block) { const { 'custom-option': customOption = 'default', 'max-items': maxItems = '10', 'enable-feature': enableFeature = 'false', } = readBlockConfig(block); // Use configuration values if (enableFeature === 'true') { // Enable the feature } } ``` Merchants can then configure the block in their documents: | Commerce Cart | |---------------| | custom-option | premium | | max-items | 5 | | enable-feature | true | ### Customizing empty states Customize what users see when a block has no content: ```javascript // Inside the decorate function for your block const $emptyCart = document.querySelector('.cart__empty-cart'); // Create custom empty state const emptyState = document.createElement('div'); emptyState.className = 'cart__empty-message'; emptyState.innerHTML = ` ### Your cart is empty Start shopping to add items to your cart. [Browse Products](/products) `; $emptyCart.appendChild(emptyState); ``` ### Adding custom analytics Track custom events for analytics: ```javascript // Add to your block file or scripts/analytics.js events.on('cart/data', (cartData) => { // Custom analytics tracking if (window.dataLayer) { window.dataLayer.push({ event: 'cart_updated', cart_total: cartData.totalQuantity, cart_value: cartData.total.includingTax.value, currency: cartData.total.includingTax.currency, }); } }); ``` ### Using multiple drop-ins Combine multiple drop-ins for complex functionality: ```javascript export default async function decorate(block) { // Create container element const $list = document.createElement('div'); $list.className = 'cart__list'; block.appendChild($list); // Render cart with wishlist integration await provider.render(CartSummaryList, { slots: { Footer: (ctx) => { // Add wishlist toggle to each cart item const wishlistContainer = document.createElement('div'); wishlistContainer.className = 'cart__action--wishlist-toggle'; wishlistRender.render(WishlistToggle, { product: ctx.item, size: 'medium', })(wishlistContainer); ctx.appendChild(wishlistContainer); }, }, })($list); } ``` ## Block-specific configuration Some blocks accept configuration options that change their behavior. These are defined in the block README and can be set through document authoring. ### Commerce Cart | Option | Type | Default | Description | |--------|------|---------|-------------| | `hide-heading` | string | `false` | Controls whether the cart heading is hidden | | `max-items` | string | — | Maximum number of items to display in cart | | `hide-attributes` | string | `''` | Comma-separated list of product attributes to hide | | `enable-item-quantity-update` | string | `false` | Enables quantity update controls for cart items | | `enable-item-remove` | string | `true` | Enables remove item functionality | | `enable-estimate-shipping` | string | `false` | Enables shipping estimation functionality | | `start-shopping-url` | string | `''` | URL for "Start Shopping" button when cart is empty | | `checkout-url` | string | `''` | URL for checkout button | | `enable-updating-product` | string | `false` | Enables product editing via mini-PDP modal | | `undo-remove-item` | string | `false` | Enables undo functionality when removing items | ### Commerce Checkout The Checkout block uses events for customization rather than configuration options. #### Example: Add custom validation before checkout ```javascript // Add to the decorate function for your checkout block events.on('checkout/values', (values) => { // Custom validation logic if (values.email && !values.email.includes('@')) { console.error('Invalid email format'); } // Log when payment method is selected if (values.selectedPaymentMethod) { console.log('Payment method:', values.selectedPaymentMethod.code); } }); ``` ### Simple blocks Many blocks (Login, Create Account, Forgot Password, and other account management blocks) are thin wrappers around drop-ins with no document-based configuration options. These blocks do not use `readBlockConfig()` and cannot be configured through document authoring. Customize these blocks by modifying their JavaScript to change drop-in options, or by using CSS and drop-in slots. ## Real-world examples ### Add promotional banner to cart ```javascript // blocks/commerce-cart/commerce-cart.js export default async function decorate(block) { // Read block configuration const { 'hide-heading': hideHeading = 'false', 'max-items': maxItems, 'enable-item-quantity-update': enableUpdateItemQuantity = 'false', 'enable-item-remove': enableRemoveItem = 'true', } = readBlockConfig(block); // Create cart layout with promotional banner const fragment = document.createRange().createContextualFragment(` 🎉 Free shipping on orders over $50! `); const $list = fragment.querySelector('.cart__list'); // Clear block and append new layout block.innerHTML = ''; block.appendChild(fragment); // Helper to create product links const createProductLink = (product) => getProductLink(product.url.urlKey, product.topLevelSku); // Render cart with full configuration await provider.render(CartSummaryList, { hideHeading: hideHeading === 'true', routeProduct: createProductLink, maxItems: parseInt(maxItems, 10) || undefined, enableUpdateItemQuantity: enableUpdateItemQuantity === 'true', enableRemoveItem: enableRemoveItem === 'true', })($list); } ``` ```css /* blocks/commerce-cart/commerce-cart.css */ .cart__promo-banner { background: var(--color-positive-200); padding: var(--spacing-medium); text-align: center; border-radius: 4px; margin-bottom: var(--spacing-medium); } ``` ### Custom checkout success tracking ```javascript // Add to blocks/commerce-checkout/commerce-checkout.js // Place this event listener in your decorate function // Listen for order placement events.on('order/placed', (orderData) => { // Send purchase event to Google Analytics if (window.dataLayer) { window.dataLayer.push({ event: 'purchase', transaction_id: orderData.number, value: orderData.grandTotal.value, currency: orderData.grandTotal.currency, items: orderData.items.map(item => ({ item_id: item.productSku, item_name: item.productName, quantity: item.quantityOrdered, price: item.price.value, })), }); } // Update page title document.title = 'Order Confirmation'; }); ``` ### Customize product gallery images ```javascript // blocks/product-details/product-details.js // Imports (these should already exist at the top of the file) // Inside your decorate function export default async function decorate(block) { // Create container element const $gallery = document.createElement('div'); $gallery.className = 'product-details__gallery'; block.appendChild($gallery); // Define custom image slot with AEM Assets const gallerySlots = { CarouselMainImage: (ctx) => { // Customize main carousel images tryRenderAemAssetsImage(ctx, { alias: ctx.data.sku, imageProps: ctx.defaultImageProps, params: { width: ctx.defaultImageProps.width, height: ctx.defaultImageProps.height, }, }); }, }; // Render gallery with custom slots await pdpRendered.render(ProductGallery, { controls: 'thumbnailsColumn', arrows: true, imageParams: { width: 960, height: 1191, }, slots: gallerySlots, })($gallery); } ``` ## Next steps 1. Browse the https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks to see implementation patterns. 1. Review the [drop-in documentation](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/) for slots, events, and API functions. 1. Check individual https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks for configuration options and behavior details. 1. Test customizations locally before deploying to production. > Start with CSS customization for visual changes, then move to JavaScript behavior modification only when necessary. This keeps your customizations maintainable and easier to upgrade when new boilerplate versions are released. --- # Getting started This page helps you run, examine, and customize a storefront that already uses the Commerce boilerplate (Pre-configured storefront with the components and services you need to get started.). By the end, you'll know where the main files live and which files are safest to customize. > **Boilerplate overview** For conceptual information about what the boilerplate is and why to use it, see the [Boilerplate overview](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/). ## Key terms The boilerplate uses Drop-in components (NPM packages that provide core Commerce storefront features such as cart, checkout, product details, and account flows.) for shopping features such as cart, checkout, product details, and requisition lists when you serve business buyers. It connects those features to document-authored pages with Commerce blocks (JavaScript blocks that integrate drop-in components into Edge Delivery Services pages to power storefront commerce experiences.). For general content and layout, it uses Content blocks (Edge Delivery Services blocks used for non-commerce page content and layout, such as cards, columns, headers, and footers.) such as cards, columns, headers, and footers. For more information about standard Edge Delivery blocks, see the Adobe Experience Manager https://www.aem.live/developer/block-collection. ## Running locally If you still need to create the GitHub repository, connect Commerce, and initialize Document Author content, follow [Create a storefront](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/create-storefront/) first. Run the storefront locally so you can preview changes before you push them. If you already ran `git clone` while following [Create a storefront](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/create-storefront/), start with the `npm install` step below. The create-a-storefront topic links here for `npm install` and `npm start` so those instructions stay in one place. 1. Install the project dependencies. ```bash npm install ``` 1. Start the local development server. ```bash npm start ``` 1. Open `http://localhost:3000` in your browser. Your storefront appears in the browser at `http://localhost:3000`. > If `npm install` fails, confirm that Node.js and npm are installed on your machine. If `npm start` fails, check whether another local server is already using port `3000`. **Optional: **install the `aem` command globally with `npm install -g @adobe/aem-cli` if you want to run the CLI from any directory. The boilerplate already lists `@adobe/aem-cli` as a dev dependency, so after `npm install` the `npm start` script can run without a global install. ### Key runtime packages After `npm install`, `package.json` resolves many dependencies. You do not need to memorize them on day one. Use this table when you trace dependencies or debug install issues. | Package | Purpose | |---------|---------| | https://www.npmjs.com/package/@dropins/tools | Shared utilities for all drop-ins (GraphQL client, event bus, initializers, UI components) | | https://github.com/adobe/adobe-client-data-layer | Standardized data layer for event collection and analytics | | https://www.npmjs.com/package/@adobe/magento-storefront-event-collector | Collects Commerce-specific user interaction events | | https://www.npmjs.com/package/@adobe/magento-storefront-events-sdk | SDK for sending events to Adobe Commerce for Live Search and Product Recommendations | ## Exploring the code The tree below shows the main folders plus important files at the repository root. The table links each folder to GitHub so you can open the matching source tree. ```text aem-boilerplate-commerce/ ├── blocks/ # Commerce blocks, content blocks, and standard AEM blocks ├── scripts/ │ ├── scripts.js # page load orchestration (eager, lazy, delayed) │ ├── commerce.js # Commerce loading, templates, storefront configuration │ └── initializers/ # one initializer file per drop-in (endpoints, labels, and related settings) ├── styles/ # global CSS, tokens, fonts, deferred styles ├── tools/ # dev helpers (for example, PDP metadata tooling) ├── config.json # storefront endpoints, locale, and related settings └── package.json # dependencies, scripts, and drop-in install hooks ``` | Directory | Purpose | |-----------|---------| | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks/ | Contains Commerce blocks, content blocks, and standard AEM blocks such as header, footer, and cards. | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/scripts/ | Contains drop-in initializers, Commerce utilities, and the Edge Delivery Services runtime. | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/styles/ | Contains global styles, design tokens, fonts, and deferred styles for performance. | | https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/tools/ | Contains development tools such as the product detail page (PDP) metadata tool. | ## Understanding the flow To understand how documents become rendered commerce experiences at runtime, see [How a page loads](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/how-a-page-loads/) in [Storefront architecture](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/). This overview helps you decide where to customize and where to leave boilerplate files unchanged. ## Customizing your storefront - Brand styling: edit `styles/styles.css` for design tokens. - Block behavior: modify block decorators in `blocks/`. - Commerce configuration: update initializers in `scripts/initializers/`. - Commerce blocks: use only the blocks your storefront needs. ### Customization strategy Most files in the boilerplate can be modified. The guidance below helps keep your storefront project easy to maintain as the original boilerplate repository evolves. ### Files to keep unchanged - `scripts/aem.js`: The core AEM runtime. This file comes from the original https://github.com/adobe/aem-boilerplate. Local edits can conflict with future boilerplate updates. - `package.json` lifecycle scripts (`postinstall`, `postupdate`, `install:dropins`): These scripts run during `npm install` to install and configure drop-ins. Changing them can break drop-in installation. The table below lists the usual layers you change for branding, blocks, Commerce wiring, page scripts, and build-time queries. Order follows a common path: global styles and blocks first, then initializer and script layers, then `build.mjs` for query shaping. | Layer | Strategy | |-------|----------| | `styles/` | Change colors, fonts, spacing, and layout here. Tokens in `styles/styles.css` apply across the whole site. | | `blocks/` | Edit block appearance and behavior. Keep block folder names unchanged because authors reference them by name in documents. | | `scripts/initializers/` | Configure the API endpoint and UI labels for each drop-in. Each drop-in has one initializer file, such as `cart.js` or `checkout.js`. If you remove a drop-in, also remove its initializer file and all imports of it in the codebase. See [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/). | | `scripts/commerce.js` | Edit this file to add Commerce-specific logic such as consent handling (`getConsent`) or custom page type detection. It reads your [storefront configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) and sets up Commerce API connections on every page. | | `scripts/scripts.js` | Add automatically generated blocks to `buildAutoBlocks` (the boilerplate uses this for the hero block). Load third-party scripts, tag managers, or analytics tools in `scripts/delayed.js`. That file runs 3 seconds after page load so these scripts do not affect the initial page render. | | `build.mjs` | Edit this file to customize what data drop-ins fetch from Commerce. For example, you can add a custom product field to the PDP query or skip query fragments your project doesn't need. Use the existing entries as patterns. Changes take effect after running `npm install`. | ### When to customize When you customize blocks or initializers, put project-specific code in new files when possible. For example, you can add slot customizations to a new `blocks/commerce-cart/slots.js` file and import it at the top of `commerce-cart.js`. Your customizations stay in a file you own. When a boilerplate update arrives, your changes to the original file stay small. ## Deploying your storefront Edge Delivery Services creates preview and production URLs when you push changes: - Preview: `https://----.aem.page` - Production: `https://----.aem.live` You do not run a separate deployment build command for Edge Delivery. The Edge Delivery pipeline handles page delivery and optimization after you push. ## Keeping your storefront current Track boilerplate changes in the https://github.com/hlxsites/aem-boilerplate-commerce/issues?q=label%3Achangelog+is%3Aclosed and [Release notes](https://experienceleague.adobe.com/developer/commerce/storefront/releases/). For guidance on upgrading drop-in components, applying updates, and handling breaking changes, see the [Updates](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/updates/) page. ## Related resources | Resource | Description | |----------|-------------| | [Boilerplate reference](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/) | Technical documentation for blocks, configuration options, and file structure. | | [Drop-in components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/introduction/) | API reference for each drop-in component. | | [Commerce blocks page](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/) | Business user page for using blocks. | --- # Overview This overview describes the Commerce boilerplate repository and how it fits Edge Delivery Services and drop-ins. Start with the definition below, then follow First steps when you are ready to clone, run locally, or dig into architecture. ## What is the Commerce boilerplate? The Commerce boilerplate is Adobe's supported starter/reference codebase for storefronts on Edge Delivery Services (Adobe's hosting and delivery infrastructure that turns authored documents into fast HTML pages served from servers close to the shopper. You push code to GitHub; Edge Delivery Services builds and publishes automatically.) (EDS). You clone it from GitHub, connect your Commerce backend, and customize blocks, scripts, and styles in that repository instead of tying together every Commerce integration yourself. The boilerplate repository lives at https://github.com/hlxsites/aem-boilerplate-commerce. EDS hosts and publishes your site from that repo. The boilerplate also bundles Drop-in components (NPM packages that provide core Commerce storefront features such as cart, checkout, product details, and account flows.) for cart, checkout, product listings, and related Commerce flows so you theme and configure shipped UI rather than building those screens from scratch. > **One supported boilerplate** https://github.com/hlxsites/aem-boilerplate-commerce is the only Adobe-supported Commerce starter/reference storefront. Demo storefronts (for example Citisignal) are forks for events or showcases. They often lag behind this repository and are not a safe starting point for your own project. ## First steps 1. If you still need a site, follow [Create a storefront](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/create-storefront/). When you already have a repo, open [Boilerplate getting started](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/getting-started/) to run `npm install` and `npm start`, and explore `blocks/`, `scripts/`, and `styles/`. 1. Read [Storefront Architecture](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/) when you need the full picture of how authoring, blocks, drop-ins, and Commerce APIs connect. ## Other topics in this section Use these when you already have a project open on disk: - [Configuration](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/configuration/) — Commerce backend endpoints, headers, and storefront settings. - [Blocks reference](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/blocks-reference/) and [Blocks customization](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/customizing-blocks/) — Block behavior and layout. - [Universal Editor](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/universal-editor/) — Optional authoring path alongside Document Authoring. - [Boilerplate updates](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/updates/) — Staying current (npm drop-ins first; suite tags are tested snapshots, not fork upgrade targets). - [Boilerplate skills](https://experienceleague.adobe.com/developer/commerce/storefront/ai/boilerplate-skills/) — Optional skills for coding agents. ## Related areas outside this section - [Commerce configuration](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/commerce-configuration/) — Wire the storefront to your Commerce backend after the boilerplate runs. - [Commerce blocks (merchants)](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/) — How merchants work with blocks in authoring tools. - [Drop-ins introduction](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/introduction/) — Technical reference for each drop-in. ## Packages and block wiring When you trace `npm` dependencies or debug install issues, open [Getting started](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/getting-started/) for the key runtime packages the boilerplate adds. When you need each Commerce block’s drop-ins, GitHub source folder, and merchant-facing topic in one place, open [Blocks reference](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/blocks-reference/). --- # Universal Editor for developers The Adobe Commerce boilerplate supports the Universal Editor (UE) for content and Commerce blocks. For setup, instrumentation syntax, and custom block workflow, see the DA.live documentation below. > **Document Authoring content source** This page covers Universal Editor setup for the **Commerce boilerplate**, which uses **Document Authoring (DA.live)** as the content source. If your project uses **AEM Sites** as the content source, the instrumentation approach differs — follow the https://www.aem.live/developer/universal-editor-blocks on aem.live instead. ## Commerce boilerplate structure Commerce differs from the standard DA.live approach (centralized `ue/models/blocks/` in https://github.com/aemsites/da-block-collection): **UE instrumentation JSON files live next to each block** at `blocks/{block-name}/_{block-name}.json`. A pre-commit hook composes these into the root-level `component-*.json` files the UE consumes. ```text aem-boilerplate-commerce/ ├── component-definition.json ← generated (UE consumes) ├── component-filters.json ← generated (UE consumes) ├── component-models.json ← generated (UE consumes) ├── models/ │ └── _section.json ← add new blocks to component list └── blocks/ ├── accordion/ │ └── _accordion.json ← UE instrumentation ├── hero/ │ └── _hero.json ├── product-recommendations/ │ └── _product-recommendations.json ├── commerce-cart/ │ └── _commerce-cart.json └── ... ← and other blocks with _*.json ``` :::tip While you are developing, open a page in the Universal Editor. Blocks without instrumentation appear as **(no definition)** in the content tree, which is a quick visual signal that a definition file is needed. ::: ## Documentation - https://docs.da.live/developers/guides/setup-universal-editor — **First-time setup:** `editor.path` config, requirements, and behavior - https://docs.da.live/developers/reference/universal-editor — full instrumentation reference - https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/models/_section.json#L130-L168 — add new blocks to this component list - [Using the Universal Editor](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/quick-start/universal-editor/) — for content authors; workflow and authoring docs --- # Boilerplate updates Upgrade your Adobe Commerce Storefront to get the latest features, security updates, and performance improvements from the Commerce Boilerplate. > **Two distinct update paths** You can keep your storefront current in two ways: - **Updating drop-in npm packages** (`@dropins/*`, `@adobe/*`) — This is the primary update path for every Commerce boilerplate storefront. Drop-in packages follow semantic versioning: minor and patch releases stay non-breaking by contract. Run `npm outdated` to see available updates, or enable the `update-dropins.yml` GitHub Actions workflow included in the boilerplate to receive automatic weekly pull requests when newer stable versions are available. - **Merging the upstream Commerce boilerplate into your repository** — This path is optional and advanced. Most teams start from a new GitHub repository generated from the Commerce boilerplate template, then clone that repository locally. You are under no obligation to pull upstream boilerplate changes into your project. Merging upstream can bring in new integration patterns or bug fixes, but expect manual conflict resolution and a careful review of your customizations. Reserve this path when a specific upstream improvement is worth the effort. Suite releases are tagged snapshots that validate a specific combination of drop-in versions and boilerplate code. They work best as starting points for new implementations, not as recurring upgrade targets for existing storefronts. ## Overview When you initially scaffold your Commerce storefront from the Commerce Boilerplate, you create a snapshot of the boilerplate at that point in time. As Adobe continues to improve the boilerplate with new features, bug fixes, and performance optimizations, your existing project won't automatically receive these updates. This page shows you how to upgrade your storefront while preserving your customizations. ## Understanding the upgrade landscape Upgrading is ongoing, not a one-time task. Regular, small updates are generally safer and easier to manage than infrequent, large upgrades. ### What gets updated Most of your storefront's critical Commerce logic is now delivered through npm packages, making upgrades significantly easier and more reliable. Upgrades typically involve these main areas: **Drop-in components (NPM packages that provide core Commerce storefront features such as cart, checkout, product details, and account flows.):** npm packages containing the core Commerce functionality, including the following: - `@dropins/storefront-account` - `@dropins/storefront-auth` - `@dropins/storefront-cart` - `@dropins/storefront-checkout` - `@dropins/storefront-order` - `@dropins/storefront-payment-services` - `@dropins/storefront-pdp` - `@dropins/storefront-personalization` - `@dropins/storefront-product-discovery` - `@dropins/storefront-recommendations` - `@dropins/storefront-wishlist` - `@dropins/tools` If your project serves business buyers, install and upgrade these business-to-business (B2B) packages too: - `@dropins/storefront-company-management` - `@dropins/storefront-company-switcher` - `@dropins/storefront-purchase-order` - `@dropins/storefront-quote-management` - `@dropins/storefront-quick-order` - `@dropins/storefront-requisition-list` The `*` in `npm list @dropins/storefront-*` and `npm update @dropins/storefront-*` is a wildcard: npm matches every installed package whose name starts with `@dropins/storefront-`, including these B2B packages when they are in your project. For what each drop-in does and how to connect it in your storefront, see [B2B drop-ins overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/). On Adobe Commerce as a Cloud Service or Adobe Commerce Optimizer, Adobe manages Storefront Compatibility packages on Commerce for you, so upgrades on this page are mainly npm packages and boilerplate changes—not a separate compatibility install checklist. > **Commerce on Cloud or on-premises** Coordinate Commerce compatibility package upgrades with npm and boilerplate changes on this page. For B2B drop-ins, see [Storefront Compatibility B2B Package](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/storefront-compatibility/b2b/) and [Manual installation](https://experienceleague.adobe.com/developer/commerce/storefront/setup/configuration/storefront-compatibility/install/#install-the-b2b-package). **Boilerplate integration layer (Storefront-level files that connect drop-ins to your site, including block implementations and initialization scripts.):** storefront-level files that integrate drop-ins | File | Purpose | Update Frequency | Typical Changes | |------|---------|------------------|-----------------| | `blocks/*` | Block integration folders | Frequent | Drop-in integration JS and CSS. Rare updates to non-drop-in block examples (cards, accordion, and so on) sourced from other boilerplates | | `scripts/aem.js` | Core AEM functionality | Very Rare | Core platform improvements | | `scripts/commerce.js` | Commerce-specific integration and utilities | Rare | Integration improvements, new utility functions | | `scripts/initializers/*` | Drop-in initialization scripts | Moderate | New drop-in integrations, configuration updates | | `scripts/scripts.js` | Page loading, block decoration, and global site functionality | Very Rare | Performance optimizations, new global features | ### Why upgrades are now easier The Commerce Boilerplate team moved most of the complex Commerce logic into npm-distributed drop-in packages, providing key advantages: - **Most logic is packaged**: Core Commerce functionality, business rules, and complex state management are delivered through npm packages - **Simplified storefront code**: Your project primarily contains integration code, styling, and configuration - **Standard npm upgrades**: Most updates can be applied using standard `npm install` commands - **Reduced conflicts**: Fewer files in your project means fewer merge conflicts when updating - **Consistent behavior**: All storefronts benefit from the same tested, optimized Commerce logic ## Upgrade strategies ### npm package updates > **Recommended for most cases** Most Commerce logic lives in npm packages, so this approach handles upgrades with minimal risk. 1. Update drop-in components Most of your Commerce functionality improvements come through these package updates: Check your current versions: ```bash npm list @dropins/storefront-* ``` Update individual components: ```bash npm install @dropins/storefront-cart@latest npm install @dropins/storefront-checkout@latest # Repeat for other components as needed ``` Or update all drop-in components at once: ```bash npm update @dropins/storefront-* ``` 1. Test your storefront Since most logic changes are packaged, testing focuses on integration points: Start your local development environment: ```bash npm start ``` Test all Commerce functionality: - Product detail pages - Shopping cart operations - Checkout flow - User account features - Search and navigation - B2B flows you ship (for example, company profiles and roles, negotiable quotes, purchase orders, requisition lists, quick order, or company switching), when those packages are in your project Review the release notes for any integration changes that may affect your implementation. > **For Major Version Updates** When upgrading drop-in packages across major versions (for example, from 1.x to 2.x), perform more thorough testing as these updates may include breaking changes (Updates that require code or configuration changes in your project before everything works correctly again.). Pay particular attention to: - Any custom integrations you've built around the drop-in components - API interfaces you're using - Configuration settings that may have changed - Custom styling or event handlers you've added Test edge cases and error scenarios in addition to the standard user flows. To help automate this testing process, consider implementing end-to-end testing in your project using tools like Cypress. These tools can simulate real user interactions across your Commerce flows, automatically catching regressions that manual testing might miss. Focus your automated tests on critical user journeys like product browsing, cart management, and checkout completion. ### Selective boilerplate updates When you need integration layer improvements or new features that require changes to your storefront's integration code, use selective updates (A workflow where you review upstream changes and merge only the files or commits that are relevant to your project.) to merge changes from the upstream (The original source repository your project forks from, used as the canonical source when reviewing and pulling updates.) boilerplate repository. 1. **Set up upstream remote** Add the boilerplate repository as an upstream remote: ```bash git remote add upstream https://github.com/hlxsites/aem-boilerplate-commerce.git git fetch upstream ``` 1. **Review available updates** Compare your current branch with the latest boilerplate to understand what changes are available. For detailed instructions on reviewing upstream changes, see https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork and https://docs.github.com/en/pull-requests/committing-changes-to-your-project/viewing-and-comparing-commits/comparing-commits. A storefront repo created from the Commerce boilerplate template is not a GitHub fork of the boilerplate, but you can still add an `upstream` remote and merge or cherry-pick using the same Git operations. Focus your review on key files like `scripts/commerce.js` and the `scripts/initializers/` directory where most Commerce-specific updates occur. 1. **Identify changes to include** Focus on these integration layer improvements: **Commerce Integration Layer (`scripts/commerce.js`)** - Enhanced drop-in initialization patterns - New configuration utilities - Performance optimizations - Bug fixes in integration logic **Initializers (`scripts/initializers/`)** - Updated drop-in initialization patterns - New feature integrations - Improved error handling **Block Integration Updates (`blocks/` directory)** - Enhanced drop-in integration patterns - Accessibility improvements - Bug fixes in block-level integration > Each Commerce block now includes a README file that describes the block's purpose and functionality. These README files are particularly helpful when diffing changes, as they provide a summary of what has changed in the block before you examine the code. If you use B2B Commerce blocks and patterns from the Commerce Boilerplate, compare your branch with the upstream repository's `b2b` branch as well as `main`. The `b2b` branch holds B2B-oriented block and integration updates. Adopt the same selective merge or cherry-pick approach so you do not overwrite your customizations. 1. **Apply selected changes** Create a branch for your upgrade work to isolate changes and test before merging to main. Use a descriptive name indicating the upgrade with a timeframe or version (for example, `upgrade-august-2025` or `upgrade-cart-v2.1.0`). Once you're on your upgrade branch, you have several options for applying changes from the upstream boilerplate: - **Selectively merge** individual files or directories that have updates you want to incorporate. This approach works well when you want to adopt specific improvements without bringing in changes that might conflict with your customizations. - **Cherry-pick** specific commits from the upstream repository if you want to apply only particular features or fixes. This gives you granular control over what changes to include and helps avoid introducing unwanted modifications. For each change you apply, commit it with a clear message describing what upstream improvement you're incorporating. This creates a clean history that makes it easy to understand what was updated and why, which will be valuable for future upgrades and troubleshooting. 1. **Resolve integration points and test** - Resolve any conflicts in integration code, preserving your customizations - Test thoroughly in your development environment - Run your test suite if available - Deploy to a staging environment for comprehensive testing ## Special considerations for early-adopter projects If you scaffolded your project before mid-2025, you may have encountered a major architectural change: Commerce logic moved from `scripts.js` to `scripts/commerce.js`. This shift affects your update scope but uses the same methods above. ### Understanding the architectural change The major refactoring (PR #567) reorganized the boilerplate architecture with two significant improvements: 1. Separation of AEM and Commerce logic: - `scripts/scripts.js` - Core AEM functionality, aligned with the upstream AEM Boilerplate - `scripts/commerce.js` - Commerce-specific integration and utility logic 1. Core Commerce logic migration to npm packages: - Complex Commerce functionality moved to `@dropins/tools` and other drop-in packages - Functions previously embedded in your integration code are now abstracted to reusable, versioned, and upgradable libraries - The Commerce Boilerplate is now focused on pure integration code that you own **Before the refactoring:** In early versions of the Commerce Boilerplate, all logic lived in `scripts/scripts.js` where Commerce business logic was mixed with integration code. Core Commerce functions were embedded directly in your project files, making it difficult to upgrade Commerce functionality without encountering code conflicts during merges. **After the refactoring:** The architecture now separates concerns more effectively. Core Commerce logic lives in npm packages that are easily upgradable, while the boilerplate contains minimal integration code that results in fewer merge conflicts. This creates clear boundaries between platform logic and your customizations, allowing future updates to focus on integration patterns rather than business logic. This architectural shift means that complex Commerce operations, utility functions, and business rules that you may have seen in your early boilerplate files are now delivered through npm packages, making them upgradable without touching your project code. ### Adapting your update approach If you have an early-adopter project, your updates will need a broader scope to account for architectural changes. When reviewing updates, pay attention to the main scripts file (`scripts/scripts.js`) and the Commerce integration layer (`scripts/commerce.js`). Both have changed significantly. Be sure to preserve your custom business logic while adopting the improved structural foundation that the upstream changes provide. The migration process requires more careful consideration than typical updates since you're not just incorporating new features, but potentially restructuring how your existing customizations integrate with the boilerplate's core functionality. During Step 4 (apply selective changes), you may need to migrate your customizations: **Custom functions in the "old" `scripts/scripts.js`** - Commerce-related customizations should move to `scripts/commerce.js` - General AEM functionality should remain in `scripts/scripts.js` - Many custom functions may now be available as utilities in `@dropins/tools` **Block import updates** - Update imports from `../../scripts/scripts.js` to `../../scripts/commerce.js` for Commerce utilities - Verify that custom utilities are still available or replace with package utilities **Integration pattern updates** - Review if custom functions can be replaced with utilities from `@dropins/tools` package - Update integrations to use new patterns for Commerce-specific functionality During Step 5 (testing), pay special attention to: - Custom blocks that import Commerce utilities - Third-party integrations that depend on Commerce functions - Custom analytics implementations that hook into Commerce events - Any custom business logic that was mixed with platform code ### Benefits of architectural alignment While early-adopter projects require more comprehensive selective updates, aligning with the new architecture provides significant long-term benefits: - **Future updates simplified**: Once aligned, future updates will be much easier due to reduced code surface area - **Better separation of concerns**: Clear boundaries between platform logic and your customizations - **Reduced merge conflicts**: Commerce functionality updates through npm packages instead of file merges - **Access to latest features**: Full compatibility with new drop-in components and features - **Enhanced PDP flexibility**: Container-based PDP architecture provides unprecedented customization control ## Product details page architectural evolution ### Previous architecture (slot-based customization) Older `@dropins/storefront-pdp` releases used the monolithic `ProductDetails` container (deprecated) with predefined slots. That pattern is deprecated and not a part of the Commerce boilerplate. It should not be used for new work. In that legacy model, teams could only: - Create custom slot implementations for different sections of the page - Work within the constraints of a single-container structure - Rearrange or replace PDP sections with limited flexibility - Rely on complex styling workarounds to reach custom layouts ### New architecture (container-based composition) The latest version breaks down the PDP into independent containers you can compose, style, and replace: - **ProductHeader** - Product title, SKU, and basic information - **ProductPrice** - Pricing display and special price handling - **ProductGallery** - Image carousel with flexible configuration - **ProductOptions** - Product variants and configuration - **ProductQuantity** - Quantity selector - **ProductDescription** - Product descriptions and content - **ProductAttributes** - Product specifications and metadata For detailed information about the container-based architecture and implementation patterns, see the [Product Details Drop-in Documentation](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/). You can also reference the current product-details block implementation in the Commerce Boilerplate for practical examples of the container-based approach. ### Migration benefits for PDP blocks - **Complete layout control**: Arrange containers in any order within your block structure - **Individual container replacement**: Replace entire sections (like gallery or pricing) with custom implementations - **Simplified styling**: Style containers independently without complex CSS overrides - **Enhanced functionality**: Each container can be configured independently with specific features - **Eliminated slot workarounds**: UI customizations that required appending, prepending, or replacing slot content can now be handled directly in the layout structure or by completely replacing containers - **Direct data access**: Product data previously available through slot context is now accessible via the event bus (`pdp/data` and `pdp/values`) - **Future-proof architecture**: New PDP features delivered as new containers rather than slot modifications ### PDP block migration process When updating your PDP block from slot-based to container-based: 1. Update dependencies ```javascript import { render as pdpRendered } from '@dropins/storefront-pdp/render.js'; // Before: Single container with slots import { ProductDetails } from '@dropins/storefront-pdp/containers/ProductDetails.js'; // (deprecated) // After: multiple independent containers import ProductHeader from '@dropins/storefront-pdp/containers/ProductHeader.js'; import ProductPrice from '@dropins/storefront-pdp/containers/ProductPrice.js'; import ProductGallery from '@dropins/storefront-pdp/containers/ProductGallery.js'; // ... other containers ``` 1. Replace slot implementations You can often remove custom slot implementations entirely. The new containers provide better configuration. 1. Update block structure Replace the single container render with multiple container renders: ```javascript // Before: Single render with slot configuration await pdpRendered.render( ProductDetails, // (deprecated) { slots: { /* custom slot implementations */ }, }, )($pdpRoot); // After: multiple container renders to layout regions await pdpRendered.render(ProductHeader, {})($header); await pdpRendered.render(ProductPrice, {})($price); await pdpRendered.render(ProductGallery, { controls: 'thumbnailsColumn' })($gallery); // ... other containers ``` 1. Layout restructuring Create HTML structure that positions containers where you need them: ```html ``` 1. Styling migration - Remove complex CSS overrides targeting internal slot structures - Style containers directly with cleaner, more maintainable CSS - Review any potential class name changes between architectures - Handle breakpoints and responsive layout at the block level, as this is now the block's responsibility ## Tracking updates ### Monitor boilerplate changes Stay informed about boilerplate updates: - **GitHub watch**: Watch the https://github.com/hlxsites/aem-boilerplate-commerce for releases and important changes - **Changelog tracking**: Follow issues tagged with the changelog label - **Release notes**: Review drop-in component release notes at the [Release Information](https://experienceleague.adobe.com/developer/commerce/storefront/releases/) page ### Version management best practices **Document your customizations** - Document all customizations - Write clear commit messages - Tag storefront releases **Establish update cadence** - Review updates monthly or quarterly - Schedule major upgrades for low-traffic periods - Test in staging ## Troubleshooting common issues ### Post-install scripts (Scripts that run automatically after package installation to copy and prepare required Commerce integration files.) failures If post-install fails: - Check Node.js and npm version compatibility - Clear the npm cache: `npm cache clean --force` - Remove and reinstall: `rm -rf node_modules package-lock.json && npm install` - Check file permissions ### Merge conflicts When merging updates: - **Understand the context**: Review both versions - **Preserve functionality**: Keep your customizations working - **Test thoroughly**: Conflicts signal significant changes - **Be selective**: Only merge valuable updates; avoid bringing in features you won't use ### Breaking changes When drop-in updates introduce breaking changes: - **Review migration pages in release notes**: Check package release notes for migration instructions - **Update integrations**: Modify custom code that depends on changed APIs - **Test edge cases**: Breaking changes often affect less common use cases - **Plan rollback**: Have a rollback plan ready before applying breaking changes ## Best practices ### Development workflow **Branch strategy** - Create upgrade branches with descriptive names (for example, `upgrade-cart-v2.1.0`) - Merge upgrades through pull requests with thorough reviews **Testing approach** - Cover custom functionality with tests - Automate testing where possible - Test across devices and browsers - Use real product data **Deployment process** - Use staging environments that mirror production - Deploy during low-traffic periods - Monitor key metrics post-deployment - Have rollback procedures ready ### Long-term maintenance **Stay current** - Update regularly, don't let them pile up - Apply security patches promptly - Plan major upgrades ahead well in advance **Customize thoughtfully** - Minimize modifications to core boilerplate files - Use configuration and theming approaches when possible - Document the business justification for customizations ## Resources - [Storefront Developer Tutorial](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/create-storefront/): Comprehensive storefront setup page - [Storefront Architecture](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/): Understanding the technical foundation - [Drop-in Components Documentation](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/introduction/): Detailed component reference pages - [B2B drop-ins overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/): B2B packages, containers, and initialization - https://www.aem.live/docs/: Core platform documentation - https://github.com/hlxsites/aem-boilerplate-commerce: Source code and latest updates - [Release Notes](https://experienceleague.adobe.com/developer/commerce/storefront/releases/): Track major changes and updates - https://github.com/hlxsites/aem-boilerplate-commerce/releases: GitHub releases and version history --- # AcceptInvitation Container Processes company invitation acceptance from email links and displays the result to the user. Version: 1.3.0 ## Configuration The `AcceptInvitation` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `routeMyAccount` | `function` | No | Returns the URL for the customer account page. Used for the 'Go to My Account' button after successful invitation acceptance. | | `routeLogin` | `function` | No | Returns the URL for the login page. Used when the user is not authenticated and needs to sign in. | | `isAuthenticated` | `boolean` | No | Indicates the current authentication status. When true, the container renders the acceptance flow; when false, it prompts the user to log in first. | | `labels` | `object` | No | Optional labels for overriding the default text. Supports keys: `title`, `loadingText`, `successTitle`, `successMessage`, `errorTitle`, `myAccountButton`, `loginButton`. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `AcceptInvitation` container: ```js await provider.render(AcceptInvitation, { routeMyAccount: () => `/customer/account`, routeLogin: () => `/customer/login`, isAuthenticated })(block); ``` --- # CompanyCredit Container Displays company credit information including credit limit, outstanding balance, and available credit for B2B customers. Version: 1.3.0 ## Configuration The `CompanyCredit` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `creditHistoryParams` | `GetCompanyCreditHistoryParams` | No | Provides optional parameters for filtering and paginating credit history data. Use to control date ranges, page size, or apply custom filters when displaying company credit transactions. | | `showCreditHistory` | `boolean` | No | Controls whether to display the credit history section. Set to false to hide historical transactions and show only current credit information, useful for simplified views or when history is displayed elsewhere. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `CompanyCredit` container: ```js await provider.render(CompanyCredit, { showCreditHistory: shouldShowHistory, creditHistoryParams: shouldShowHistory ? { pageSize: 10, currentPage: 1, } : undefined })(block); ``` --- # CompanyHierarchy Container Part of the https://github.com/adobe-commerce/storefront-company-management drop-in. Displays and manages the company organizational hierarchy for company administrators, with an interactive tree view that supports drag-and-drop reorganization of parent-child company relationships. The hierarchy supports only a single-level structure: parent companies and their direct children. Nested hierarchies are not supported. The following shows the tree view rendered in `SHOW_STRUCTURE` mode. ![CompanyHierarchy container showing an interactive tree view of parent and child companies](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins-b2b/CH.png) *CompanyHierarchy container showing an interactive tree view of parent and child companies* > Only users with the Company Administrator role can view and interact with this container. Users without that role see an access-denied message. Company features must also be enabled in the Admin before hierarchy data can load — see [Admin configuration](#admin-configuration). ## View modes The container renders one of five view modes depending on data state and user permissions. | Mode | When it appears | |---|---| | `IS_LOADING` | During the initial admin check and data fetch | | `NO_ACCESS` | When the authenticated user lacks Company Administrator privileges | | `IS_EMPTY` | When no companies exist in the hierarchy | | `IS_ERROR` | When an API call fails or a network error occurs | | `SHOW_STRUCTURE` | When data loads successfully, rendering the full interactive tree | ## Displayed information When the container renders in `SHOW_STRUCTURE` mode, the tree view includes: - Company names in a hierarchical tree structure - Visual distinction between root companies (no parent) and child companies nested under a parent - Optional "Admin" badges next to companies where `is_admin` is `true` (controlled by `showAdminBadge`) - Drag handles on child companies for reorganizing the structure - Expand and collapse indicators for companies that have children - Company icons that distinguish root-level entries (full opacity) from child-level entries - Selection highlighting with a blue border on the clicked company row - A toolbar with Expand All and Collapse All action buttons ## Available actions | Action | How to trigger | |---|---| | Expand or collapse a single node | Click the chevron icon on a company row | | Expand all nodes | Click the Expand All toolbar button | | Collapse all nodes | Click the Collapse All toolbar button (all nodes collapse except the virtual root) | | Select a company | Click any company row | | Drag a company | Drag a child company row to a new position in the tree | | Drop a company | Drop a dragged company onto a root-level company or the virtual root to reassign the relationship | | Auto-refresh | Triggered automatically after a successful assign or unassign operation | ## GraphQL operations The container orchestrates the following operations automatically. | Operation | Type | Description | |---|---|---| | `isCompanyAdmin` | Query | Validates that the authenticated user has Company Administrator role. The check passes when `role.id === '0'` or `role.name === 'Company Administrator'`. | | `getCompanyHierarchy` | Query | Fetches the complete hierarchy including parent-child relationships via the `customer.company_hierarchy` field. | | `assignChildCompany` | Mutation | Assigns a child company to a parent company. | | `unassignChildCompany` | Mutation | Removes a child company from its parent, making it a root-level company. | ## Configuration The `CompanyHierarchy` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `withHeader` | `boolean` | No | Renders a "Company Hierarchy" title and divider above the tree. Set to `true` for standalone page sections; leave `false` (default) when the surrounding layout already provides a title. Use `true` for full-page company management dashboards; use `false` for sidebar widgets or embedded views. | | `className` | `string` | No | Adds a custom CSS class to the root container element, merged with the default `account-company-hierarchy` class. Use to apply brand styles, integrate with a utility CSS framework (Tailwind, Bootstrap, etc.), add responsive layout classes for mobile or tablet layouts, or override default spacing, colors, or typography. | | `defaultExpanded` | `boolean` | No | Controls whether all tree nodes are expanded on first render. Defaults to `true`. Use `true` for small hierarchies (fewer than 10 companies) where immediate full visibility helps; use `false` for large hierarchies (20 or more companies) to reduce visual overload, or for progressive disclosure in compact or mobile layouts. | | `showAdminBadge` | `boolean` | No | Shows an "Admin" badge next to companies where the authenticated user has admin privileges (`is_admin: true`). Defaults to `false`. Enable in multi-company environments where users manage more than one organization, or in company selection interfaces where quick visual identification of administrative access rights is important. Disable for single-company users or when permissions are irrelevant to the task. | | `slots` | `object` | No | Replaces or extends the default toolbar above the tree. Use to swap in custom-styled Expand All / Collapse All controls, add extra actions (such as Export to PDF, Print Structure, or Share Hierarchy), implement filtering or search above the tree, display company count badges or hierarchy statistics, or integrate analytics tracking for expand and collapse events. See [Slots](#slots) for context properties. | ## Slots The `CompanyHierarchy` container exposes one slot for customizing the toolbar. | Slot | Type | Required | Description | |------|------|----------|-------------| | `Actions` | `SlotProps` | No | Replaces the default "Expand All" / "Collapse All" toolbar buttons. Receives context with tree controls and state. | ### Actions slot context The `Actions` slot receives a context object with the following properties: | Property | Type | Description | |----------|------|-------------| | `expandAll` | `() => void` | Expands all tree nodes. | | `collapseAll` | `() => void` | Collapses all nodes except the virtual root. | | `expandedIds` | `Set` | The set of currently expanded node IDs. | | `setExpandedIds` | `(ids: Set) => void` | Sets the expanded nodes programmatically. | | `treeItemsCount` | `number` | Total number of companies in the hierarchy. | ## Usage The following examples show common ways to render the `CompanyHierarchy` container. ### Basic ```js await provider.render(CompanyHierarchy, {})(block); ``` ### With header ```js await provider.render(CompanyHierarchy, { withHeader: true, })(block); ``` ### With header and admin badges ```js await provider.render(CompanyHierarchy, { withHeader: true, defaultExpanded: false, showAdminBadge: true, })(block); ``` ### With a custom Actions slot ```jsx await provider.render(CompanyHierarchy, { className: 'custom-hierarchy-styling', withHeader: true, defaultExpanded: true, showAdminBadge: true, slots: { Actions: ({ expandAll, collapseAll, treeItemsCount }) => ( ), }, })(block); ``` ## Admin configuration The `CompanyHierarchy` container has no dedicated Admin panel settings of its own, but the following configurations must be in place for it to function. ### Store settings Path: **Admin** > **Stores** > **Settings** > **Configuration** > **General** > **B2B Features** > **Company** | Setting | Required value | |---|---| | Enable Company | Yes | | Enable Company Registration from the Storefront | Yes | ### Company settings Path: **Admin** > **Customers** > **Companies** > **Edit Company** > **Advanced Settings** - The parent company must exist and have Active status. - Each child company must have a valid company record before it can be assigned to a parent. ### Permissions There are no dedicated ACL resources that control access to Company Hierarchy. Access is determined entirely by the Company Administrator role check performed via the `isCompanyAdmin()` API. Non-admin users see the access-denied view regardless of Admin panel settings. --- # CompanyProfile Container Manages company profile information including legal name, VAT/Tax ID, contact details, and `payment/shipping` configurations. Version: 1.3.0 ## Configuration The `CompanyProfile` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `className` | `string` | No | Adds custom CSS classes to the container element. Use to override default styles, integrate with existing design systems, or apply conditional styling based on application state. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container exposes the following slots for customization: | Slot | Type | Required | Description | |------|------|----------|-------------| | `CompanyData` | `SlotProps` | No | Customize company profile information display. | ## Usage The following example demonstrates how to use the `CompanyProfile` container: ```js await provider.render(CompanyProfile, { className: "Example Name", initialData: {}, slots: { // Add custom slot implementations here } })(block); ``` --- # CompanyRegistration Container Provides a company registration form for new B2B customers to create a company account. Version: 1.3.0 ## Configuration The `CompanyRegistration` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `isAuthenticated` | `boolean` | No | Indicates authentication status. Use to conditionally show registration or redirect to account. | | `onRedirectLogin` | `function` | No | Callback to redirect to login. Use for custom login routing. | | `onRedirectAccount` | `function` | No | Callback to redirect to account after registration. Use for custom navigation. | | `onSuccess` | `function` | No | Callback function triggered on successful completion. Use to implement custom success handling, navigation, or notifications. | | `onError` | `function` | No | Callback function triggered when an error occurs. Use to implement custom error handling, logging, or user notifications. | | `className` | `string` | No | Adds custom CSS classes to the container element. Use to override default styles, integrate with existing design systems, or apply conditional styling based on application state. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `CompanyRegistration` container: ```js await provider.render(CompanyRegistration, { isAuthenticated: true, onRedirectLogin: (redirectLogin) => console.log('RedirectLogin', redirectLogin), onRedirectAccount: (redirectAccount) => console.log('RedirectAccount', redirectAccount), })(block); ``` --- # CompanyStructure Container Displays and manages the company organizational hierarchy with teams and user assignments. Version: 1.3.0 ## Configuration The `CompanyStructure` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `className` | `string` | No | Adds custom CSS classes to the container element. Use to override default styles, integrate with existing design systems, or apply conditional styling based on application state. | | `withHeader` | `boolean` | No | Controls whether to render the container header section. Set to false when embedding the container within a layout that already provides its own header to avoid duplicate navigation elements. | | `isAuthenticated` | `boolean` | No | Indicates authentication status. Use to conditionally render content or trigger login. | | `onRedirectLogin` | `function` | No | Callback to redirect to login. Use for custom login routing. | | `onRedirectAccount` | `function` | No | Callback to redirect to account. Use for custom navigation. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container exposes the following slots for customization: | Slot | Type | Required | Description | |------|------|----------|-------------| | `StructureData` | `SlotProps` | No | Customize company structure hierarchy display. | ## Usage The following example demonstrates how to use the `CompanyStructure` container: ```js await provider.render(CompanyStructure, { className: "Example Name", withHeader: true, isAuthenticated: true, slots: { // Add custom slot implementations here } })(block); ``` --- # CompanyUsers Container Manages company users including adding, editing, removing users, and controlling user status (Active/Inactive). Version: 1.3.0 ## Configuration The `CompanyUsers` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | No configurations | - | - | - | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `CompanyUsers` container: ```js await provider.render(CompanyUsers, {})(block); ``` --- # CustomerCompanyInfo Container Displays basic company information for the currently authenticated customer. Version: 1.3.0 ## Configuration The `CustomerCompanyInfo` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `className` | `string` | No | Adds custom CSS classes to the container element. Use to override default styles, integrate with existing design systems, or apply conditional styling based on application state. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `CustomerCompanyInfo` container: ```js await provider.render(CustomerCompanyInfo, { className: "Example Name", initialData: {}, })(block); ``` --- # Company Management Containers The **Company Management** drop-in provides pre-built container components for integrating into your storefront. Version: 1.3.0 ## What are Containers? Containers are pre-built UI components that combine functionality, state management, and presentation. They provide a complete solution for specific features and can be customized through props, slots, and CSS. ## Available Containers | Container | Description | | --------- | ----------- | | [AcceptInvitation](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/containers/accept-invitation/) | Processes company invitation acceptance from email links and displays the result to the user. | | [CompanyCredit](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/containers/company-credit/) | Displays company credit information including credit limit, outstanding balance, and available credit for B2B customers. | | [CompanyHierarchy](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/containers/company-hierarchy/) | Displays and manages parent-child company relationships through an interactive tree view with drag-and-drop reorganization. | | [CompanyProfile](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/containers/company-profile/) | Manages company profile information including legal name, VAT/Tax ID, contact details, and `p`ayment/shippin`g` configurations. | | [CompanyRegistration](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/containers/company-registration/) | Provides a company registration form for new B2B customers to create a company account. | | [CompanyStructure](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/containers/company-structure/) | Displays and manages the company organizational hierarchy with teams and user assignments. | | [CompanyUsers](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/containers/company-users/) | Manages company users including adding, editing, removing users, and controlling user status (Active/Inactive). | | [CustomerCompanyInfo](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/containers/customer-company-info/) | Displays basic company information for the currently authenticated customer. | | [RolesAndPermissions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/containers/roles-and-permissions/) | Manages company roles and permission assignments for role-based access control. | > Each container is designed to work independently but can be composed together to create comprehensive user experiences. --- # RolesAndPermissions Container Manages company roles and permission assignments for role-based access control. Version: 1.3.0 ## Configuration The `RolesAndPermissions` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `className` | `string` | No | Adds custom CSS classes to the container element. Use to override default styles, integrate with existing design systems, or apply conditional styling based on application state. | | `withHeader` | `boolean` | No | Controls whether to render the container header section. Set to false when embedding the container within a layout that already provides its own header to avoid duplicate navigation elements. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `RolesAndPermissions` container: ```js await provider.render(RolesAndPermissions, { className: "Example Name", withHeader: true, initialData: {}, })(block); ``` --- # Company Management Dictionary The **Company Management dictionary** contains all user-facing text, labels, and messages displayed by this drop-in. Customize the dictionary to: - **Localize** the drop-in for different languages and regions - **Customize** labels and messages to match your brand voice - **Override** default text without modifying source code for the drop-in Dictionaries use the **i18n (internationalization)** pattern, where each text string is identified by a unique key path. Version: 1.2.0 ## How to customize Override dictionary values during drop-in initialization. The drop-in deep-merges your custom values with the defaults. ```javascript await initialize({ langDefinitions: { en_US: { "Company": { "shared": { "fields": { "companyName": "Custom value", "companyEmail": "Custom value" } } } } } }); ``` You only need to include the keys you want to change. For multi-language support and advanced patterns, see the [Dictionary customization guide](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/dictionaries/). ## Default keys and values Below are the default English (`en_US`) strings provided by the **Company Management** drop-in: ```json title="en_US.json" { "Company": { "shared": { "fields": { "companyName": "Company Name", "companyEmail": "Company Email", "email": "Email", "legalName": "Legal Name", "vatTaxId": "VAT/Tax ID", "resellerId": "Reseller ID", "accountInformation": "Account Information", "legalAddress": "Legal Address", "streetAddress": "Street Address", "city": "City", "country": "Country", "stateProvince": "State/Province", "zipPostalCode": "ZIP/Postal Code", "phoneNumber": "Phone Number", "status": "Status", "region": "Region", "postalCode": "Postal Code", "jobTitle": "Job Title", "workPhoneNumber": "Work Phone Number", "userRole": "User Role", "title": "New Company", "companyInformation": "Company Information", "street": "Street Address", "streetLine2": "Street Address Line 2", "postcode": "ZIP/Postal Code", "telephone": "Phone Number", "companyAdmin": "Company Administrator", "adminJobTitle": "Job Title", "adminWorkTelephone": "Work Phone Number", "adminEmail": "Email", "adminFirstname": "First Name", "adminLastname": "Last Name", "adminGender": "Gender", "address": "Address", "submit": "Register Company", "submitting": "Registering...", "required": "Required", "createCompanyError": "Failed to create company. Please try again.", "unexpectedError": "An unexpected error occurred. Please try again." }, "buttons": { "edit": "Edit", "cancel": "Cancel", "save": "Save Changes", "saving": "Saving...", "close": "Close", "confirm": "Confirm" }, "validation": { "required": "This field is required", "invalidEmail": "Please enter a valid email address", "companyNameRequired": "Company name is required", "emailRequired": "Email is required", "emailNotAvailable": "This email is already used by another company", "phoneInvalid": "Please enter a valid phone number", "postalCodeInvalid": "Please enter a valid postal code", "companyNameLengthError": "Company name must not exceed 40 characters", "legalNameLengthError": "Legal name must not exceed 80 characters", "vatTaxIdLengthError": "VAT/Tax ID must not exceed 40 characters", "resellerIdLengthError": "Reseller ID must not exceed 40 characters", "roleNameRequired": "This is a required field.", "roleNameExists": "User role with this name already exists. Enter a different name to save this role." }, "messages": { "loading": "Loading...", "noData": "No data available", "error": "An error occurred", "success": "Operation completed successfully" }, "loading": "Loading...", "ariaLabels": { "editButton": "Edit company profile", "cancelButton": "Cancel editing", "saveButton": "Save company profile changes", "closeButton": "Close dialog" } }, "CompanyProfile": { "containerTitle": "Company Profile", "editCompanyProfile": { "containerTitle": "Edit Company Profile", "companySuccess": "Company profile updated successfully", "companyError": "Failed to update company profile", "buttonSecondary": "Cancel", "buttonPrimary": "Save Changes" }, "companyProfileCard": { "noDataMessage": "Company profile not available. Please contact your administrator.", "contacts": "Contacts", "companyAdministrator": "Company Administrator", "salesRepresentative": "Sales Representative", "paymentInformation": "Payment Information", "availablePaymentMethods": "Available Payment Methods", "shippingInformation": "Shipping Information", "availableShippingMethods": "Available Shipping Methods", "noPaymentMethods": "This company has no payment methods. Please contact store administrator.", "noShippingMethods": "This company has no shipping methods. Please contact store administrator.", "companyDetails": "Company Details", "addressInformation": "Address Information" }, "messages": { "loadError": "Failed to load company profile", "updateError": "Failed to update company profile", "loadingProfile": "Loading company profile...", "savingProfile": "Saving company profile...", "noDataToUpdate": "No data to update" } }, "CompanyStructure": { "containerTitle": "Company Structure", "shared": { "buttons": { "addUser": "Add User", "addTeam": "Add Team", "editSelected": "Edit", "remove": "Remove", "ok": "OK", "cancel": "Cancel", "close": "Close", "save": "Save", "deleting": "Deleting…", "removing": "Removing…", "expandAll": "Expand All", "collapseAll": "Collapse All" }, "titles": { "addUser": "Add User", "editUser": "Edit User", "addTeam": "Add Team", "editTeam": "Edit Team" }, "fields": { "jobTitle": "Job Title", "userRole": "User Role", "firstName": "First Name", "lastName": "Last Name", "email": "Email", "workPhoneNumber": "Work Phone Number", "status": "Status", "teamTitle": "Team Title", "description": "Description" }, "options": { "selectRole": "Select role…", "active": "Active", "inactive": "Inactive", "companyAdministrator": "Company Administrator", "delete": "Delete", "expand": "Expand", "collapse": "Collapse" }, "ariaLabels": { "addUser": "Add user", "addTeam": "Add team", "editSelected": "Edit selected", "removeSelected": "Remove selected", "showDescription": "Show description", "companyStructureActions": "Company structure actions", "expandAllNodes": "Expand all nodes", "collapseAllNodes": "Collapse all nodes" }, "messages": { "processing": "Processing…", "teamDescription": "Team description" }, "validation": { "firstNameRequired": "First name is required", "lastNameRequired": "Last name is required", "emailRequired": "Email is required", "emailInvalid": "Enter a valid email", "jobTitleRequired": "Job title is required", "workPhoneRequired": "Work phone number is required", "selectRole": "Select a role", "teamTitleRequired": "Team title is required", "firstNameMaxLength": "First name must not exceed 255 characters", "lastNameMaxLength": "Last name must not exceed 255 characters", "emailMaxLength": "Email must not exceed 254 characters", "jobTitleMaxLength": "Job title must not exceed 255 characters", "telephoneMaxLength": "Phone number must not exceed 20 characters", "teamNameMaxLength": "Team title must not exceed 39 characters", "teamDescriptionMaxLength": "Team description must not exceed 1000 characters", "firstNameInvalidChars": "First name contains invalid characters. Only letters, numbers, spaces, and ,-._'`& are allowed", "lastNameInvalidChars": "Last name contains invalid characters. Only letters, numbers, spaces, and ,-._'`& are allowed", "telephoneInvalidChars": "Phone number contains invalid characters. Only 0-9, +, -, (, ), and spaces are allowed" } }, "messages": { "loadError": "Failed to load company structure", "updateError": "Failed to update company structure", "noStructureData": "No structure data.", "cannotDeleteUser": "Cannot Delete User", "cannotDeleteTeam": "Cannot Delete This Team", "removeUserConfirm": "Remove this user from Company structure?", "deleteTeamConfirm": "Delete this team?", "removeItemsConfirm": "Remove {count} item(s)?", "removeUserMessage": "Removing a user changes the account status to Inactive. The user's content is still available to the Company administrator, but the user cannot log in.", "cannotDeleteUserMessage": "This user has active users or teams assigned to it and cannot be deleted. Please unassign the users or teams first.", "cannotDeleteTeamMessage": "This team has active users or teams assigned to it and cannot be deleted. Please unassign the users or teams first.", "removeItemsMessage": "This action will remove the selected items from the company structure.", "deleteTeamMessage": "This action cannot be undone. Are you sure you want to delete this team?", "failedToMoveItem": "Failed to move item", "createUserError": "Failed to create user. You may not have permission to perform this action.", "createTeamError": "Failed to create team. You may not have permission to perform this action.", "saveUserError": "An error occurred while saving the user.", "saveTeamError": "An error occurred while saving the team.", "createUserSuccess": "The customer was successfully created.", "updateUserSuccess": "The customer was successfully updated.", "createTeamSuccess": "The team was successfully created.", "updateTeamSuccess": "The team was successfully updated.", "removeUserSuccess": "User was successfully removed from company structure.", "deleteTeamSuccess": "Team was successfully deleted.", "removeMultipleSuccess": "{count} item(s) were successfully removed.", "moveUserSuccess": "User was successfully moved.", "moveTeamSuccess": "Team was successfully moved.", "loadRolesError": "Failed to load roles", "fetchPermissionsError": "Failed to fetch permissions" } }, "CompanyUsers": { "filters": { "showAll": "Show All Users", "showActive": "Show Active Users", "showInactive": "Show Inactive Users" }, "columns": { "id": "ID", "name": "Name", "email": "Email", "role": "Role", "team": "Team", "status": "Status", "actions": "Actions" }, "status": { "active": "Active", "inactive": "Inactive" }, "emptyTeam": "-", "pagination": { "itemsRange": "Items {start}-{end} of {total}", "itemsPerPage": "Items per page:", "show": "Show", "perPage": "per page", "previous": "Previous", "next": "Next", "pageInfo": "Page {current} of {total}" }, "emptyActions": "", "noUsersFound": "No users found.", "actions": { "manage": "Manage", "edit": "Edit", "addNewUser": "Add New User" }, "ariaLabels": { "loadingUsers": "Loading company users", "usersTable": "Company users table", "filterOptions": "User filter options", "paginationNav": "Pagination navigation", "pageNavigation": "Page navigation", "pageSizeSelector": "Items per page selector", "previousPageFull": "Go to previous page, current page {current}", "nextPageFull": "Go to next page, current page {current}", "currentPage": "Current page {current} of {total}", "showingUsers": "Showing {count} users", "dataLoaded": "Loaded {count} users", "dataError": "Failed to load users.", "manageUser": "Manage user {name}", "editUser": "Edit user {name}" }, "managementModal": { "title": "Manage user", "setActiveText": "Reactivate the user's account by selecting \"Set as Active\".", "setInactiveText": "Temporarily lock the user's account by selecting \"Set as Inactive\".", "deleteText": "Permanently delete the user's account and all associated content by selecting \"Delete\". This action cannot be reverted.", "setActiveButton": "Set as Active", "setInactiveButton": "Set as Inactive", "settingActiveButton": "Setting Active...", "settingInactiveButton": "Setting Inactive...", "deleteButton": "Delete", "deletingButton": "Deleting...", "cancelButton": "Cancel", "setActiveErrorGeneric": "An unexpected error occurred while setting user as active.", "setActiveErrorSpecific": "Failed to set user as active.", "setInactiveErrorGeneric": "An unexpected error occurred while setting user as inactive.", "setInactiveErrorSpecific": "Failed to set user as inactive.", "deleteErrorGeneric": "An unexpected error occurred.", "deleteErrorSpecific": "Failed to delete user.", "setActiveSuccess": "User was successfully activated.", "setInactiveSuccess": "User was successfully deactivated.", "deleteSuccess": "User was successfully deleted.", "ariaLabels": { "closeModal": "Close modal", "modalDescription": "User management options including setting as inactive or deleting the user account" } } }, "CompanyRegistration": { "success": { "pendingApproval": "Thank you! We're reviewing your request and will contact you soon.", "companyDetails": "Company Information" } }, "CustomerCompanyInfo": { "individualUserMessage": "You don't have a company account yet.", "createAccountCta": "Create a Company Account" }, "CompanyCredit": { "title": "Company Credit", "creditAvailable": "Available Credit", "creditLimit": "Credit Limit", "outstandingBalance": "Outstanding Balance", "messages": { "loadError": "Failed to load company credit" }, "emptyState": { "title": "No Credit Information", "message": "There is no credit information to display." } }, "CompanyCreditHistory": { "title": "Credit History", "columns": { "date": "Date", "operation": "Operation", "amount": "Amount", "outstandingBalance": "Outstanding Balance", "availableCredit": "Available Credit", "creditLimit": "Credit Limit", "customReference": "Custom Reference #", "updatedBy": "Updated By" }, "pagination": { "itemsRange": "Items {start}-{end} of {total}", "show": "Show" }, "emptyState": { "title": "No Credit History", "message": "There is no credit history to display." }, "ariaLabels": { "dataLoaded": "Loaded {count} credit history entries", "dataError": "Failed to load credit history entries. Please try again.", "historyTable": "Credit history table", "paginationNav": "Pagination navigation", "pageSizeSelector": "Items per page selector", "showingHistory": "Showing {count} credit history entries" } }, "EditRoleAndPermission": { "createTitle": "Add New Role", "editTitle": "Edit Role", "roleInformation": "Role Information", "roleName": "Role Name", "rolePermissions": "Role Permissions", "permissionsDescription": "Granting permissions does not affect which features are available for your company account. The merchant must enable features to make them available for your account.", "expandAll": "Expand All", "collapseAll": "Collapse All", "saveRole": "Save Role" }, "FormText": { "requiredFieldError": "This is a required field.", "numericError": "Only numeric values are allowed.", "alphaNumWithSpacesError": "Only alphanumeric characters and spaces are allowed.", "alphaNumericError": "Only alphanumeric characters are allowed.", "alphaError": "Only alphabetic characters are allowed.", "emailError": "Please enter a valid email address.", "phoneError": "Please enter a valid phone number.", "postalCodeError": "Please enter a valid postal code.", "lengthTextError": "Text length must be between {min} and {max} characters.", "urlError": "Please enter a valid URL", "nameError": "Please enter a valid name", "selectCountry": "Please select a country", "selectRegion": "Please select a region, state or province", "selectCountryFirst": "Please select a country first", "companyNameLengthError": "Company name must be between {min} and {max} characters.", "loading": "Loading...", "submitting": "Registering your company..." }, "AcceptInvitation": { "title": "Accept Company Invitation", "loadingText": "Processing your invitation...", "successMessage": "You have successfully accepted the invitation to the company.", "myAccountButton": "My Account", "loginButton": "Go to Login", "invalidLinkError": "Invalid invitation link. Please check the URL and try again.", "companyDisabledError": "Company functionality is not enabled. Please contact the store administrator.", "expiredLinkError": "This invitation link has expired or is no longer valid.", "genericError": "An error occurred while processing your invitation. Please try again." }, "RolesAndPermissions": { "containerTitle": "Company Roles & Permissions", "noAccess": { "title": "Access Restricted", "message": "You do not have permission to view roles and permissions. Please contact your company administrator." }, "error": { "title": "Error Loading Roles", "message": "An error occurred while loading roles and permissions. Please try again." }, "deleteModal": { "title": "Delete This Role?", "message": "This action cannot be undone. Are you sure you want to delete this role?", "confirm": "Delete", "cancel": "Cancel" }, "cannotDeleteModal": { "title": "Cannot Delete Role", "message": "This role cannot be deleted because users are assigned to it. Reassign the users to another role to continue.", "ok": "OK" }, "alerts": { "createSuccess": "Role \"{roleName}\" created successfully!", "createError": "Failed to create role. Please try again.", "createErrorPermissions": "Failed to create role. Please check your permissions and try again.", "updateSuccess": "Role \"{roleName}\" updated successfully!", "updateError": "Failed to update role. Please try again.", "updateErrorPermissions": "Failed to update role. Please check your permissions and try again.", "deleteError": "Failed to delete role. Please try again." } }, "RoleAndPermissionTable": { "addNewRole": "Add New Role", "columnId": "ID", "columnRole": "Role", "columnUsers": "Users", "columnActions": "Actions", "editButton": "Edit", "duplicateButton": "Duplicate", "deleteButton": "Delete", "viewOnlyLabel": "View Only", "systemRoleLabel": "System Role", "itemCount": "Item(s)", "itemsRange": "Items {start}-{end} of {total}", "show": "Show", "perPage": "per page", "deleteRole": { "success": "You have deleted role \"{roleName}\"." } }, "CompanyHierarchy": { "containerTitle": "Company Hierarchy", "messages": { "noCompaniesData": "No companies data available.", "loading": "Loading company hierarchy...", "loadError": "Failed to load company hierarchy. Please try again.", "moveError": "Failed to update company hierarchy. Your changes were not saved." }, "noAccess": { "title": "Access Denied", "message": "You do not have permission to view company hierarchy." } }, "Table": { "sortedAscending": "Sorted ascending by {label}", "sortedDescending": "Sorted descending by {label}", "sortBy": "Sort by {label}" } } } ``` --- # Company Management Events and Data The **Company Management** drop-in uses the [event bus](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/events/) to emit and listen to events for communication between drop-ins and external integrations. Version: 1.3.0 ## Events reference {/* EVENTS_TABLE_START */} | Event | Direction | Description | |-------|-----------|-------------| | [company/updated](#companyupdated-emits) | Emits | Emitted when the component state is updated. | | [companyStructure/updated](#companystructureupdated-emits) | Emits | Emitted when the component state is updated. | | [error](#error-emits) | Emits | Emitted when a network error occurs during any API call. | | [companyContext/changed](#companycontextchanged-listens) | Listens | Fired by Company Context (`companyContext`) when a change occurs. | {/* EVENTS_TABLE_END */} ## Event details The following sections provide detailed information about each event, including its direction, event payload, and usage examples. ### `company/updated` (emits) Emitted when company information is updated. This event fires after successful company profile updates, legal address changes, contact information modifications, or sales representative information updates. #### Event payload ```typescript { data?: { id?: string; companyName?: string; email?: string; telephone?: string; }; message?: string; error?: Error; } ``` #### When triggered - After successful company profile update - After updating company legal address - After updating company contact information - After updating sales representative information #### Example 1: Basic company update handler ```js // Listen for company updates events.on('company/updated', (payload) => { console.log('Company updated:', payload.data); // Update UI or trigger other actions refreshCompanyDisplay(); }); ``` #### Example 2: Update with notification and error handling ```js async function updateCompanyProfile(updates) { try { // Show loading state showLoadingIndicator('Updating company profile...'); // Update the company await updateCompany(updates); // Listen for successful update events.once('company/updated', (payload) => { hideLoadingIndicator(); showSuccessNotification('Company profile updated successfully'); // Update the displayed company information document.querySelector('.company-name').textContent = payload.data.companyName; document.querySelector('.company-email').textContent = payload.data.email; // Track the update in analytics trackEvent('company_profile_updated', { companyId: payload.data.id, fieldsUpdated: Object.keys(updates) }); }); } catch (error) { hideLoadingIndicator(); showErrorNotification('Failed to update company profile: ' + error.message); console.error('Company update error:', error); } } // Usage updateCompanyProfile({ companyName: 'Acme Corporation', email: 'info@acme.com', telephone: '+1-555-0123' }); ``` #### Example 3: Real-time multi-component sync ```js // Central company data manager class CompanyDataManager { constructor() { this.subscribers = []; // Listen for company updates events.on('company/updated', this.handleCompanyUpdate.bind(this)); } handleCompanyUpdate(payload) { const companyData = payload.data; // Update all subscribed components this.subscribers.forEach(callback => { try { callback(companyData); } catch (error) { console.error('Error updating subscriber:', error); } }); // Update local storage for offline support localStorage.setItem('companyData', JSON.stringify(companyData)); // Sync with external CRM this.syncWithCRM(companyData); } subscribe(callback) { this.subscribers.push(callback); } async syncWithCRM(companyData) { try { await fetch('/api/crm/update-company', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(companyData) }); } catch (error) { console.error('CRM sync failed:', error); } } } // Initialize manager const companyManager = new CompanyDataManager(); // Subscribe components companyManager.subscribe((data) => { // Update header component document.querySelector('.header-company-name').textContent = data.companyName; }); companyManager.subscribe((data) => { // Update sidebar widget updateCompanySidebarWidget(data); }); ``` #### Usage scenarios - Refresh company profile display after edits. - Trigger analytics tracking for profile changes. - Update related UI components (headers, sidebars, widgets). - Sync company data with external systems (CRM, ERP). - Show success notifications to users. - Update cached data and local storage. - Refresh company-dependent permissions. - Update breadcrumbs and navigation with company name. --- ### `error` (emits) Emitted when a network error occurs during any Company Management API call. Does not fire for intentional user cancellations (`AbortError`). #### Event payload ```typescript { source: 'company'; type: 'network'; error: Error; } ``` #### When triggered - When any API mutation or query fails due to a network error. - Does **not** fire when the request is deliberately aborted. #### Example ```js events.on('error', ({ source, type, error }) => { if (source === 'company') { console.error('Company Management network error:', error.message); } }); ``` --- ### `companyContext/changed` (listens) Fired by Company Context (`companyContext`) when a change occurs. #### Event payload ```typescript string | null | undefined ``` #### Example ```js events.on('companyContext/changed', (payload) => { console.log('companyContext/changed event received:', payload); // Add your custom logic here }); ``` ### `companyStructure/updated` (emits) Emitted when the company organizational structure changes. This event fires after creating or updating teams, deleting teams, creating or updating users, moving users between teams, or changing team hierarchy. #### Event payload ```typescript { message?: string; action?: 'move' | 'remove' | 'add'; nodeId?: string; newParentId?: string; nodeIds?: string[]; nodes?: unknown[]; error?: unknown; } ``` #### When triggered - After creating a new team - After updating team information - After deleting a team - After creating a new user - After updating user details - After moving users between teams - After changing team hierarchy #### Example 1: Interactive structure tree with live updates ```js class CompanyStructureTree { constructor(containerElement) { this.container = containerElement; this.structureData = null; // Listen for structure updates events.on('companyStructure/updated', this.handleUpdate.bind(this)); // Initial load this.loadStructure(); } async loadStructure() { try { this.showLoading(); this.structureData = await getCompanyStructure(); this.render(); } catch (error) { this.showError('Failed to load company structure'); console.error(error); } } async handleUpdate(payload) { console.log('Structure updated:', payload.data); // Highlight the updated section const updatedNodeId = payload.data.updatedNodeId; if (updatedNodeId) { this.highlightNode(updatedNodeId); } // Reload the full structure await this.loadStructure(); // Show success message this.showNotification('Organization structure updated', 'success'); // Refresh permissions for all users in the tree await this.refreshPermissions(); } highlightNode(nodeId) { const nodeElement = this.container.querySelector(`[data-node-id="${nodeId}"]`); if (nodeElement) { nodeElement.classList.add('highlight-update'); setTimeout(() => nodeElement.classList.remove('highlight-update'), 2000); } } render() { // Render the structure tree this.container.innerHTML = this.buildTreeHTML(this.structureData); this.attachEventListeners(); } buildTreeHTML(structure) { // Build hierarchical HTML for the structure return `...`; } async refreshPermissions() { // Refresh permissions after structure change events.emit('permissions/refresh-needed'); } showLoading() { this.container.innerHTML = 'Loading structure...'; } showError(message) { this.container.innerHTML = `${message}`; } showNotification(message, type) { // Show toast notification const notification = document.createElement('div'); notification.className = `notification notification-${type}`; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => notification.remove(), 3000); } attachEventListeners() { // Add drag-and-drop, expand/collapse, etc. } } // Initialize the tree const tree = new CompanyStructureTree(document.querySelector('#company-structure')); ``` #### Example 2: Team-based notification system ```js // Track structure changes and notify affected users events.on('companyStructure/updated', async (payload) => { const { data } = payload; // Determine what changed const changeType = determineChangeType(data); switch (changeType) { case 'team-created': notifyTeamCreation(data.newTeam); break; case 'team-deleted': notifyTeamDeletion(data.deletedTeam); break; case 'user-moved': notifyUserReassignment(data.user, data.oldTeam, data.newTeam); break; case 'hierarchy-changed': notifyHierarchyChange(data.affectedTeams); break; } // Update all team-based UI components updateTeamSelectors(); updateUserFilters(); refreshTeamDashboards(); // Log for audit trail logStructureChange({ timestamp: new Date(), changeType, userId: getCurrentUserId(), details: data }); }); function determineChangeType(data) { // Logic to determine what type of change occurred if (data.newTeam) return 'team-created'; if (data.deletedTeam) return 'team-deleted'; if (data.userMoved) return 'user-moved'; return 'hierarchy-changed'; } async function notifyUserReassignment(user, oldTeam, newTeam) { const message = `${user.name} has been moved from ${oldTeam.name} to ${newTeam.name}`; // Notify team managers await sendNotification([oldTeam.managerId, newTeam.managerId], message); // Notify the user await sendNotification([user.id], `You have been assigned to ${newTeam.name}`); // Update UI showToast(message, 'info'); } function logStructureChange(logEntry) { // Send to audit log fetch('/api/audit/log', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(logEntry) }); } ``` #### Usage scenarios - Refresh the company structure tree display in real-time. - Update user access controls based on new hierarchy. - Trigger notifications to affected team members. - Log organizational changes for audit and compliance. - Update cached structure data and local storage. - Refresh team-based dropdowns and filters. - Update permission matrices after reassignments. - Highlight changes in the structure visualization. - Trigger workflow updates (approval chains, and so on). - Sync organizational structure with external HR systems. --- ## Listening to events All Company Management events are emitted through the centralized event bus. Subscribe to events using the `events.on()` method: ```js // Single event listener events.on('company/updated', (payload) => { // Handle company update }); // Multiple event listeners events.on('company/updated', handleCompanyUpdate); events.on('companyStructure/updated', handleStructureUpdate); // Remove listeners when no longer needed events.off('company/updated', handleCompanyUpdate); ``` > Event listeners remain active until explicitly removed with `events.off()`. Clean up listeners when components unmount to prevent memory leaks. ## Related documentation - [Functions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/functions/) - API functions that emit these events - [Containers](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/containers/) - UI components that respond to events - [Event bus documentation](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/events/) - Learn more about the event system {/* This documentation is manually curated based on: https://github.com/adobe-commerce/storefront-company-management */} --- # Company Management Functions The Company Management drop-in provides **29 API functions** for managing company structures, users, roles, permissions, and credit, enabling complete B2B company administration workflows. Version: 1.3.0 | Function | Description | | --- | --- | | [`acceptCompanyInvitation`](#acceptcompanyinvitation) | Accepts a company invitation using the invitation code and user details from an email link. | | [`allowCompanyRegistration`](#allowcompanyregistration) | Returns whether the backend allows company self-registration per store configuration. | | [`assignChildCompany`](#assignchildcompany) | Assigns a child company to a parent company in the company hierarchy. | | [`buildPermissionTree`](#buildpermissiontree) | Filters a complete ACL resource tree down to only the resources matching the provided permission IDs. | | [`checkCompanyCreditEnabled`](#checkcompanycreditenabled) | Checks whether the Company Credit functionality ("Payment on Account") is enabled for the logged-in customer's company. | | [`companyEnabled`](#companyenabled) | Returns whether the Company feature is enabled in store configuration. | | [`createCompany`](#createcompany) | Registers a new B2B company with complete business information including company details, legal address, and administrator account. | | [`createCompanyRole`](#createcompanyrole) | Creates a new company role with specified permissions and assigns it to users. | | [`createCompanyTeam`](#createcompanyteam) | Creates a new company team under an optional target structure node. | | [`createCompanyUser`](#createcompanyuser) | Creates a new company user and optionally places them under a target structure node. | | [`deleteCompanyRole`](#deletecompanyrole) | Deletes a company role by ID and unassigns users from the deleted role. | | [`deleteCompanyTeam`](#deletecompanyteam) | Deletes a company team by entity ID. | | [`deleteCompanyUser`](#deletecompanyuser) | Unassigns the user from the company (the user is not removed from the Company Structure tree). | | [`fetchUserPermissions`](#fetchuserpermissions) | Retrieves the current user's role permissions and returns both the flattened permission IDs and the raw role response. | | [`flattenPermissionIds`](#flattenpermissionids) | Flattens a nested ACL resource tree into a flat array of permission ID strings. | | [`getCompany`](#getcompany) | Retrieves complete information about the current company including name, structure, settings, and metadata. | | [`getCompanyAclResources`](#getcompanyaclresources) | Retrieves the available ACL (Access Control List) resources for company role permissions. | | [`getCompanyHierarchy`](#getcompanyhierarchy) | Retrieves the hierarchy of companies related to the current company. | | [`getCompanyCredit`](#getcompanycredit) | Retrieves the company's credit information including available credit, credit limit, outstanding balance, and currency. | | [`getCompanyCreditHistory`](#getcompanycredithistory) | Retrieves the company's credit transaction history with pagination support. | | [`getCompanyRole`](#getcompanyrole) | Retrieves details for a single company role including permissions and assigned users. | | [`getCompanyRoles`](#getcompanyroles) | Retrieves all company roles with their permissions and user assignments. | | [`getCompanyStructure`](#getcompanystructure) | Retrieves the hierarchical organization structure of the company including all teams, divisions, and reporting relationships. | | [`getCompanyTeam`](#getcompanyteam) | Fetches details for a single company team by entity ID. | | [`getCompanyUser`](#getcompanyuser) | Fetches details for a single company user by entity ID. | | [`getCompanyUsers`](#getcompanyusers) | Fetches the list of company users with their roles and team information, supporting pagination and status filtering. | | [`getCountries`](#getcountries) | Retrieves available countries and regions for address forms. | | [`getCustomerCompany`](#getcustomercompany) | Fetches simplified customer company information for display on the customer account information page. | | [`getStoreConfig`](#getstoreconfig) | Retrieves store configuration settings relevant to company management. | | `initialize` | Initializes the Company drop-in with optional language definitions and data model metadata. | | [`isCompanyAdmin`](#iscompanyadmin) | Checks if the current authenticated customer is a company administrator in any company. | | [`isCompanyRoleNameAvailable`](#iscompanyrolenameavailable) | Checks if a role name is available for use in the company. | | [`isCompanyUser`](#iscompanyuser) | Checks if the current authenticated customer belongs to any company. | | [`isCompanyUserEmailAvailable`](#iscompanyuseremailavailable) | Checks if an email address is available for a new company user. | | [`unassignChildCompany`](#unassignchildcompany) | Removes a child company from its parent in the company hierarchy. | | [`updateCompany`](#updatecompany) | Updates company profile information with permission-aware field filtering based on user's edit permissions. | | [`updateCompanyRole`](#updatecompanyrole) | Updates an existing company role's name, permissions, or assigned users. | | [`updateCompanyStructure`](#updatecompanystructure) | Moves a structure node under a new parent in the company structure tree. | | [`updateCompanyTeam`](#updatecompanyteam) | Updates a company team's name and/or description. | | [`updateCompanyUser`](#updatecompanyuser) | Updates company user fields such as name, email, telephone, role, and status. | | [`updateCompanyUserStatus`](#updatecompanyuserstatus) | Updates a company user's status between Active and Inactive with automatic base64 encoding. | | [`validateCompanyEmail`](#validatecompanyemail) | Validates if a company email is available. | ## acceptCompanyInvitation Accepts a company invitation using the invitation code and user details from an email link. ```ts const acceptCompanyInvitation = async ( input: AcceptCompanyInvitationInput ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | `AcceptCompanyInvitationInput` | Yes | Input parameters for the operation. | ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## allowCompanyRegistration Returns whether the backend allows company self-registration per store configuration. ```ts const allowCompanyRegistration = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns `boolean`. ## assignChildCompany Assigns a child company to a parent company in the company hierarchy. Returns the updated hierarchy as a flat array of `Company` objects. ```ts const assignChildCompany = async ( parentCompanyId: string, childCompanyId: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `parentCompanyId` | `string` | Yes | The unique ID of the parent company. | | `childCompanyId` | `string` | Yes | The unique ID of the child company to assign. | ### Events Does not emit any drop-in events. ### Returns Returns `Company[]` — the updated company hierarchy after the assignment. ## checkCompanyCreditEnabled Checks whether the Company Credit functionality ("Payment on Account") is enabled for the logged-in customer's company. ```ts const checkCompanyCreditEnabled = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns [`CheckCompanyCreditEnabledResponse`](#checkcompanycreditenabledresponse). ## companyEnabled Returns whether the Company feature is enabled in store configuration. ```ts const companyEnabled = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns `boolean`. ## createCompany Registers a new B2B company with complete business information. This function handles the entire company registration workflow including: - Company details validation (name, email, legal name, tax IDs) - Legal address validation with `country/region` support - Company administrator account creation - Email uniqueness validation ```ts const createCompany = async ( formData: any ): Promise<{ success: boolean; company?: CompanyRegistrationModel; errors?: string[] }> ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `formData` | `any` | Yes | Company registration form data containing company info, legal address, and admin details. Includes: `company_name`, `company_email`, `legal_name`, `vat_tax_id`, `reseller_id`, `legal_address` (with street, city, region, postcode, country_id, telephone), and `company_admin` (with email, firstname, lastname, job_title, telephone, gender, custom_attributes). | ### Events Does not emit any drop-in events. ### Returns ```ts { success: boolean; company?: CompanyRegistrationModel; errors?: string[] } ``` See [`CompanyRegistrationModel`](#companyregistrationmodel). ## createCompanyRole Creates a new company role with specified name and permissions. The role name must be unique within the company. Use `isCompanyRoleNameAvailable` to validate name uniqueness before calling this function. **Permissions Required:** - `Magento_Company::roles_edit` - User must have role management permission ```ts const createCompanyRole = async ( input: CompanyRoleCreateInputModel ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | `CompanyRoleCreateInputModel` | Yes | Role creation data including name and permission IDs. | ### Events Does not emit any drop-in events. ### Returns Returns [`CompanyRoleModel`](#companyrolemodel). ## createCompanyTeam Creates a new company team under an optional target structure node. ```ts const createCompanyTeam = async ( input: CreateCompanyTeamInput ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | `CreateCompanyTeamInput` | Yes | Input parameters for the operation. | ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## createCompanyUser Creates a new company user and optionally places them under a target structure node. ```ts const createCompanyUser = async ( input: CreateCompanyUserInput ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | `CreateCompanyUserInput` | Yes | Input parameters for the operation. | ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## deleteCompanyRole Permanently deletes a company role. > Restrictions: - Cannot delete roles with assigned users. You must reassign users first - Cannot delete default system roles, such as "Default User" - This operation cannot be undone - Role configuration and permission settings are permanently lost **Permissions Required:** - `Magento_Company::roles_edit` - User must have role management permission ```ts const deleteCompanyRole = async ( variables: DeleteCompanyRoleVariables ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `variables` | `DeleteCompanyRoleVariables` | Yes | Delete operation parameters containing the role ID. | ### Events Does not emit any drop-in events. ### Returns Returns `boolean`. ## deleteCompanyTeam Deletes a company team by entity ID. ```ts const deleteCompanyTeam = async ( id: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `id` | `string` | Yes | The unique identifier for the company team (structure node) to delete. This removes the team and may reassign team members depending on your company's configuration. | ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## deleteCompanyUser > This function **unassigns the user from the company** and should **NOT** be used for removing users from the Company Structure tree. ```ts const deleteCompanyUser = async ( params: DeleteCompanyUserParams ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `params` | `DeleteCompanyUserParams` | Yes | An object of type `DeleteCompanyUserParams` containing the user ID to delete and any additional parameters required for user deletion. | ### Events Does not emit any drop-in events. ### Returns Returns [`DeleteCompanyUserResponse`](#deletecompanyuserresponse). ## fetchUserPermissions Retrieves the current user's role permissions and returns both the flattened permission IDs and the raw role response. This function is used internally by other API functions to determine what data the user can access. ```ts const fetchUserPermissions = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## getCompany Retrieves complete information about the current company including name, structure, settings, and metadata. Returns the full company profile for the authenticated user's company context. ```ts const getCompany = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## getCompanyAclResources Retrieves the available ACL (Access Control List) resources for company role permissions. ```ts const getCompanyAclResources = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns an array of [`CompanyAclResourceModel`](#companyaclresourcemodel) objects. ## getCompanyHierarchy Retrieves the hierarchy of companies related to the current company, including parent and child company relationships. Returns the full hierarchy as a flat array of `Company` objects. ```ts const getCompanyHierarchy = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns `Company[]` — the full company hierarchy for the authenticated user's company context. ## getCompanyCredit Retrieves the company's credit information including available credit, credit limit, outstanding balance, and currency. This is used to display company credit status and validate purchase limits. ```ts const getCompanyCredit = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns [`CompanyCreditInfo`](#companycreditinfo) or `null`. ## getCompanyCreditHistory Retrieves the company's credit transaction history with pagination support. ```ts const getCompanyCreditHistory = async ( params: GetCompanyCreditHistoryParams = {} ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `params` | `GetCompanyCreditHistoryParams` | No | An optional object of type `GetCompanyCreditHistoryParams` containing pagination parameters (currentPage, pageSize) and optional filters. Omit to retrieve history with default pagination. | ### Events Does not emit any drop-in events. ### Returns Returns [`CompanyCreditHistory`](#companycredithistory) or `null`. ## getCompanyRole Retrieves complete details for a specific company role by ID. Returns role name, assigned user count, and the complete permission tree structure with this role's granted permissions. Used when editing an existing role or viewing role details. **Permissions Required:** - `Magento_Company::roles_view` (minimum) - To view role details - `Magento_Company::roles_edit` - To modify the role (additional permission) ```ts const getCompanyRole = async ( variables: GetCompanyRoleVariables ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `variables` | `GetCompanyRoleVariables` | Yes | Query parameters containing the role ID. | ### Events Does not emit any drop-in events. ### Returns Returns [`CompanyRoleModel`](#companyrolemodel). ## getCompanyRoles Retrieves a paginated list of all company roles with basic information. Returns roles with their names, assigned user counts, and IDs. Supports server-side pagination and filtering by role name. **Permissions Required:** - `Magento_Company::roles_view` - User must have permission to view company roles ```ts const getCompanyRoles = async ( variables: GetCompanyRolesVariables = {} ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `variables` | `GetCompanyRolesVariables` | No | Optional query parameters for pagination and filtering. | ### Events Does not emit any drop-in events. ### Returns Returns [`CompanyRolesResponseModel`](#companyrolesresponsemodel). ## getCompanyStructure Retrieves the hierarchical organization structure of the company including all teams, divisions, and reporting relationships. Returns the complete company tree structure. ```ts const getCompanyStructure = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## getCompanyTeam Fetches details for a single company team by entity ID. ```ts const getCompanyTeam = async ( id: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `id` | `string` | Yes | The unique identifier for the company team to retrieve. Returns detailed information about the team including its name, members, and position in the company hierarchy. | ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## getCompanyUser Fetches details for a single company user by entity ID. ```ts const getCompanyUser = async ( id: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `id` | `string` | Yes | The unique identifier for the company user to retrieve. Returns complete user profile including role, team assignment, and permissions. | ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## getCompanyUsers Fetches the list of company users with their roles and team information, supporting pagination and status filtering. ```ts const getCompanyUsers = async ( params: CompanyUsersParams = {} ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `params` | `CompanyUsersParams` | No | An optional object of type `CompanyUsersParams` containing pagination and filter criteria (currentPage, pageSize, filter). Omit to retrieve all users with default pagination. | ### Events Does not emit any drop-in events. ### Returns Returns [`CompanyUsersResponse`](#companyusersresponse). ## getCountries Retrieves available countries and regions for address forms. ```ts const getCountries = async (): Promise<{ availableCountries: Country[] | []; countriesWithRequiredRegion: string[]; optionalZipCountries: string[]; }> ``` ### Events Does not emit any drop-in events. ### Returns ```ts Promise<{ availableCountries: Country[] | []; countriesWithRequiredRegion: string[]; optionalZipCountries: string[]; }> ``` See [`Country`](#country). ## getCustomerCompany Fetches simplified customer company information for display on the customer account information page. This is a lightweight API that only returns essential company details without requiring full company management permissions. ```ts const getCustomerCompany = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## getStoreConfig Retrieves store configuration settings relevant to company management. ```ts const getStoreConfig = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns [`StoreConfigModel`](#storeconfigmodel). ## isCompanyAdmin Checks if the current authenticated customer is a company administrator in any company. ```ts const isCompanyAdmin = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns `boolean`. ## isCompanyRoleNameAvailable Validates whether a role name is available for use (not already taken). Used for real-time validation during role creation and editing to prevent duplicate role names within a company. Role names are case-sensitive. > Role names must be unique within a company. Different companies can have roles with the same name. ```ts const isCompanyRoleNameAvailable = async ( variables: IsCompanyRoleNameAvailableVariables ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `variables` | `IsCompanyRoleNameAvailableVariables` | Yes | Validation parameters containing the role name to check. | ### Events Does not emit any drop-in events. ### Returns Returns `boolean`. ## isCompanyUser Checks if the current authenticated customer belongs to any company. ```ts const isCompanyUser = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns `boolean`. ## isCompanyUserEmailAvailable Checks if an email address is available for a new company user. ```ts const isCompanyUserEmailAvailable = async ( email: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `email` | `string` | Yes | The email address. | ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## unassignChildCompany Removes a child company from its parent in the company hierarchy. Returns the updated hierarchy as a flat array of `Company` objects. ```ts const unassignChildCompany = async ( childCompanyId: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `childCompanyId` | `string` | Yes | The unique ID of the child company to unassign from its parent. | ### Events Does not emit any drop-in events. ### Returns Returns `Company[]` — the updated company hierarchy after the child company is removed. ## updateCompany Updates company profile information with permission-aware field filtering. This function dynamically builds the `GraphQL` mutation based on user permissions: - Only requests fields the user can view in the response - Only sends fields the user can edit in the mutation **Permissions Required:** - `Magento_Company::edit_account` - To update name, email, legal name, VAT/Tax ID, Reseller ID - `Magento_Company::edit_address` - To update legal address fields > The drop-in UI gates which fields are editable. If neither permission is granted, the submit button is disabled and this function should not be called. ```ts const updateCompany = async ( input: UpdateCompanyDto ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | `UpdateCompanyDto` | Yes | Partial company data to update (only changed fields). Can include: `name`, `email`, `legalName`, `vatTaxId`, `resellerId`, and `legalAddress` (with street, city, region, countryCode, postcode, telephone). | ### Events Does not emit any drop-in events. ### Returns Returns `CompanyModel`. ## updateCompanyRole Updates an existing company role's name `and/or` permissions. The role name must be unique within the company (excluding the current role). Use `isCompanyRoleNameAvailable` to validate name uniqueness if changing the name. **Permissions Required:** - `Magento_Company::roles_edit` - User must have role management permission ```ts const updateCompanyRole = async ( input: CompanyRoleUpdateInputModel ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | `CompanyRoleUpdateInputModel` | Yes | Role update data including ID, new name, and/or new permission IDs. | ### Events Does not emit any drop-in events. ### Returns Returns [`CompanyRoleModel`](#companyrolemodel). ## updateCompanyStructure Moves a structure node under a new parent in the company structure tree. ```ts const updateCompanyStructure = async ( input: UpdateCompanyStructureInput ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | `UpdateCompanyStructureInput` | Yes | Input parameters for the operation. | ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## updateCompanyTeam Updates a company team's name and description. ```ts const updateCompanyTeam = async ( input: UpdateCompanyTeamInput ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | `UpdateCompanyTeamInput` | Yes | Input parameters for the operation. | ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## updateCompanyUser Updates company user fields such as name, email, telephone, role, and status. ```ts const updateCompanyUser = async ( input: UpdateCompanyUserInput ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | `UpdateCompanyUserInput` | Yes | Input parameters for the operation. | ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## updateCompanyUserStatus Updates a company user's status between Active and Inactive with automatic base64 encoding. ```ts const updateCompanyUserStatus = async ( params: UpdateCompanyUserStatusParams ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `params` | `UpdateCompanyUserStatusParams` | Yes | An object of type `UpdateCompanyUserStatusParams` containing the user ID and the new status value (active, inactive). Used to enable or disable user access to company resources. | ### Events Does not emit any drop-in events. ### Returns Returns [`UpdateCompanyUserStatusResponse`](#updatecompanyuserstatusresponse). ## validateCompanyEmail Validates if a company email is available. ```ts const validateCompanyEmail = async ( email: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `email` | `string` | Yes | The email address. | ### Events Does not emit any drop-in events. ### Returns Returns [`ValidateCompanyEmailResponse`](#validatecompanyemailresponse). ## Utility Functions The following utility functions are exported from the public API and support working with company ACL (Access Control List) data. ## buildPermissionTree The `buildPermissionTree` function filters a complete ACL resource tree down to only the nodes that match a given set of permission IDs. Nodes without matching descendants are excluded. Used when displaying or saving role permissions. ```ts const buildPermissionTree = ( allResources: CompanyAclResourceModel[], selectedIds: string[] ): CompanyAclResourceModel[] ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `allResources` | `CompanyAclResourceModel[]` | Yes | The full, unfiltered tree of ACL resources as returned by `getCompanyAclResources`. | | `selectedIds` | `string[]` | Yes | An array of permission ID strings to keep. Nodes whose ID is in this array, or that have a descendant in this array, are included in the result. | ### Events Does not emit any drop-in events. ### Returns Returns a filtered `CompanyAclResourceModel[]` containing only nodes that match the provided permission IDs (or have matching descendants). ## flattenPermissionIds The `flattenPermissionIds` function traverses a nested ACL resource tree and returns a flat array of all permission ID strings. Useful for initializing checkboxes, validating permission sets, or building the `selectedIds` input for `buildPermissionTree`. ```ts const flattenPermissionIds = ( resources: CompanyAclResourceModel[] ): string[] ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `resources` | `CompanyAclResourceModel[]` | Yes | The nested ACL resource tree to flatten. | ### Events Does not emit any drop-in events. ### Returns Returns a `string[]` containing every permission ID found in the tree (including nested children). ### Company The `Company` object is returned by the following functions: [`assignChildCompany`](#assignchildcompany), [`getCompanyHierarchy`](#getcompanyhierarchy), [`unassignChildCompany`](#unassignchildcompany). ```ts interface Company { id: string; name: string; is_admin: boolean; parent_company: { id: string; name: string } | null; child_companies: Company[]; } ``` ## Data Models The following data models are used by functions in this drop-in. ### CheckCompanyCreditEnabledResponse The `CheckCompanyCreditEnabledResponse` object is returned by the following functions: [`checkCompanyCreditEnabled`](#checkcompanycreditenabled). ```ts interface CheckCompanyCreditEnabledResponse { creditEnabled: boolean; error?: string; } ``` ### CompanyAclResourceModel The `CompanyAclResourceModel` object is returned by the following functions: [`getCompanyAclResources`](#getcompanyaclresources). ```ts interface CompanyAclResourceModel { id: string; text: string; sortOrder: number; children?: CompanyAclResourceModel[]; } ``` ### CompanyCreditHistory The `CompanyCreditHistory` object is returned by the following functions: [`getCompanyCreditHistory`](#getcompanycredithistory). ```ts interface CompanyCreditHistory { items: CompanyCreditHistoryItem[]; pageInfo: CompanyCreditHistoryPageInfo; totalCount: number; } ``` ### CompanyCreditInfo The `CompanyCreditInfo` object is returned by the following functions: [`getCompanyCredit`](#getcompanycredit). ```ts interface CompanyCreditInfo { credit: { available_credit: { currency: string; value: number; }; credit_limit: { currency: string; value: number; }; outstanding_balance: { currency: string; value: number; }; }; } ``` ### CompanyRegistrationModel The `CompanyRegistrationModel` object is returned by the following functions: [`createCompany`](#createcompany). ```ts interface CompanyRegistrationModel { id: string; name: string; email: string; legalName?: string; vatTaxId?: string; resellerId?: string; legalAddress: { street: string[]; city: string; region: { regionCode: string; region?: string; regionId?: number; }; postcode: string; countryCode: string; telephone?: string; }; companyAdmin: { id: string; firstname: string; lastname: string; email: string; jobTitle?: string; telephone?: string; }; } ``` ### CompanyRoleModel The `CompanyRoleModel` object is returned by the following functions: [`createCompanyRole`](#createcompanyrole), [`getCompanyRole`](#getcompanyrole), [`updateCompanyRole`](#updatecompanyrole). ```ts interface CompanyRoleModel { id: string; name: string; usersCount: number; permissions: CompanyAclResourceModel[]; } ``` ### CompanyRolesResponseModel The `CompanyRolesResponseModel` object is returned by the following functions: [`getCompanyRoles`](#getcompanyroles). ```ts interface CompanyRolesResponseModel { items: CompanyRoleModel[]; totalCount: number; pageInfo: PageInfoModel; } ``` ### CompanyUsersResponse The `CompanyUsersResponse` object is returned by the following functions: [`getCompanyUsers`](#getcompanyusers). ```ts interface CompanyUsersResponse { users: CompanyUser[]; pageInfo: CompanyUsersPageInfo; totalCount?: number; } ``` ### Country The `Country` object is returned by the following functions: [`getCountries`](#getcountries). ```ts type Country = { value: string; text: string; availableRegions?: { id: number; code: string; name: string; }[]; }; ``` ### DeleteCompanyUserResponse The `DeleteCompanyUserResponse` object is returned by the following functions: [`deleteCompanyUser`](#deletecompanyuser). ```ts interface DeleteCompanyUserResponse { success: boolean; } ``` ### StoreConfigModel The `StoreConfigModel` object is returned by the following functions: [`getStoreConfig`](#getstoreconfig). ```ts interface StoreConfigModel { defaultCountry: string; storeCode: string; } ``` ### UpdateCompanyUserStatusResponse The `UpdateCompanyUserStatusResponse` object is returned by the following functions: [`updateCompanyUserStatus`](#updatecompanyuserstatus). ```ts interface UpdateCompanyUserStatusResponse { success: boolean; user?: { id: string; status: CompanyUserStatus; }; } ``` ### ValidateCompanyEmailResponse The `ValidateCompanyEmailResponse` object is returned by the following functions: [`validateCompanyEmail`](#validatecompanyemail). ```ts interface ValidateCompanyEmailResponse { isValid: boolean; error?: string; } ``` {/* This documentation is auto-generated from the drop-in source repository: REPO_URL */} --- # Company Management overview The Company Management drop-in enables company profile management and role-based permissions for Adobe Commerce storefronts. It also supports legal address management and company contact information. ## Supported Commerce features The following table provides an overview of the Adobe Commerce features that the Company Management drop-in supports: | Feature | Status | | ------- | ------ | | Company profile management | Supported | | Role-based permissions | Supported | | Legal address management | Supported | | Company contact information | Supported | | Payment methods configuration | Supported | | Shipping methods configuration | Supported | | Multi-language support | Supported | | Custom regions for international addresses | Supported | | Email validation | Supported | | GraphQL API integration | Supported | | Company hierarchy management | Supported | | Advanced user role management | Supported | --- # Company Management initialization The **Company Management initializer** configures the drop-in for managing company accounts, organizational structures, user roles, and permissions. Use initialization to customize how company data is displayed and enable internationalization for multi-language B2B storefronts. Version: 1.4.0 ## Basic initialization Initialize the drop-in with default settings: ```javascript title="scripts/initializers/company-management.js" await initializers.mountImmediately(initialize, {}); ``` > **Standard options** You can customize text and labels using the standard `langDefinitions` option. See other drop-in initialization pages for examples. --- # Company Management Quick Start Get started with the Company Management drop-in to enable self-service company administration in your B2B storefront. Version: 1.3.0 ## Quick example The Company Management drop-in is included in the https://github.com/hlxsites/aem-boilerplate-commerce. This example shows the basic pattern: ```js // 1. Import initializer (handles all setup) // 2. Import the container you need // 3. Import the provider // 4. Render in your block export default async function decorate(block) { await provider.render(AcceptInvitation, { // Configuration options - see Containers page })(block); } ``` **New to drop-ins?** See the [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/) guide for complete step-by-step instructions. ## Quick reference **Import paths:** - Initializer: `import '../../scripts/initializers/company-management.js'` - Containers: `import ContainerName from '@dropins/storefront-company-management/containers/ContainerName.js'` - Provider: `import { render } from '@dropins/storefront-company-management/render.js'` **Package:** `@dropins/storefront-company-management` **Version:** 1.2.0 (verify compatibility with your Commerce instance) **Example container:** `AcceptInvitation` ## Learn more - [Containers](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/containers/) - Available UI components and configuration options - [Initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/initialization/) - Customize initializer settings and data models - [Functions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/functions/) - Control drop-in behavior programmatically - [Events](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/events/) - Listen to and respond to drop-in state changes - [Slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/slots/) - Extend containers with custom content --- # Company Management Slots The Company Management drop-in exposes slots for customizing specific UI sections. Use slots to replace or extend container components. For default properties available to all slots, see [Extending drop-in components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/extending/). Version: 1.3.0 | Container | Slots | |-----------|-------| | [`CompanyHierarchy`](#companyhierarchy-slots) | `Actions` | | [`CompanyProfile`](#companyprofile-slots) | `CompanyData` | | [`CompanyStructure`](#companystructure-slots) | `StructureData` | ## CompanyHierarchy slots The slots for the `CompanyHierarchy` container allow you to customize its appearance and behavior. ```typescript interface CompanyHierarchyProps { slots?: { Actions?: SlotProps; }; } ``` ### Actions slot The `Actions` slot allows you to add custom actions to the `CompanyHierarchy` container. #### Example ```js await provider.render(CompanyHierarchy, { slots: { Actions: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Actions'; ctx.appendChild(element); } } })(block); ``` ## CompanyProfile slots The slots for the `CompanyProfile` container allow you to customize its appearance and behavior. ```typescript interface CompanyProfileProps { slots?: { CompanyData?: SlotProps; }; } ``` ### CompanyData slot The `CompanyData` slot allows you to customize the company data section of the `CompanyProfile` container. #### Example ```js await provider.render(CompanyProfile, { slots: { CompanyData: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom CompanyData'; ctx.appendChild(element); } } })(block); ``` ## CompanyStructure slots The slots for the `CompanyStructure` container allow you to customize its appearance and behavior. ```typescript interface CompanyStructureProps { slots?: { StructureData?: SlotProps; }; } ``` ### StructureData slot The `StructureData` slot allows you to customize the structure data section of the `CompanyStructure` container. #### Example ```js await provider.render(CompanyStructure, { slots: { StructureData: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom StructureData'; ctx.appendChild(element); } } })(block); ``` --- # Company Management styles Customize the Company Management drop-in using CSS classes and design tokens. This page covers the Company Management-specific container classes and customization examples. For comprehensive information about design tokens, responsive breakpoints, and styling best practices, see [Styling Drop-In Components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/). Version: 1.3.0 ## Customization example Add this to the CSS file of the specific https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/ where you're using the Company Management drop-in. For a complete list of available design tokens (colors, spacing, typography, and more), see the [Design tokens reference](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/#design-tokens-reference). ```css title="styles/styles.css" del={2-2} ins={3-3} .company-registration-success { max-width: 600px; max-width: 900px; } ``` ## Container classes The Company Management drop-in uses BEM-style class naming. Use the browser DevTools to inspect elements and find specific class names. ```css /* AcceptInvitationForm */ .company-accept-invitation-loading {} .company-accept-invitation-wrapper {} .company-accept-invitation-wrapper__buttons {} .company-accept-invitation-wrapper__submit {} /* CompanyCreditDisplay */ .company-management-company-credit-display {} .company-management-company-credit-empty {} .company-management-company-credit-grid {} .company-management-company-credit-negative {} /* CompanyCreditHistoryDisplay */ .company-management-company-credit-history-display {} .company-management-company-credit-history-table {} .company-management-credit-history-empty {} .company-management-credit-history-price {} .dropin-picker__button {} .dropin-picker__option {} .item-count {} .page-size-loading {} .page-size-picker {} .pagination-controls {} .pagination-label {} .sr-only {} .table-footer {} .table-footer-center {} .table-footer-left {} .table-footer-right {} /* CompanyLoaders */ .company-company-loaders--card-loader {} .company-company-loaders--picker-loader {} .company-credit-skeleton-loader {} /* CompanyProfileCard */ .account-company-profile-card {} .account-company-profile-card-short {} .account-company-profile-card__actions {} .account-company-profile-card__content {} .account-company-profile-card__no-data {} .account-company-profile-card__wrapper {} .company-contact {} .company-contacts {} .company-legal-address {} .company-payment-methods {} .company-profile__title {} .dropin-card__content {} /* CompanyRegistrationForm */ .company-form-section {} .company-form-section__title {} .company-form-wrapper {} .company-form-wrapper__buttons {} .company-form-wrapper__errors {} .company-form-wrapper__notification {} .company-form-wrapper__submit {} .error-message {} /* Form */ .company-form {} .company-form--submitting {} .company-form-container {} .company-form-loader {} .company-form-loader__text {} .company-form__submitting-overlay {} .company-form__submitting-text {} .company-registration-form__inputs {} .company-registration-form__section {} /* CompanyRegistrationSuccess */ .company-registration-success {} .company-registration-success__details {} .company-registration-success__details-title {} .company-registration-success__grid {} .company-registration-success__header {} .company-registration-success__item {} .company-registration-success__label {} .company-registration-success__pending {} .company-registration-success__section-header {} .company-registration-success__subtitle {} .company-registration-success__title {} .company-registration-success__value {} /* CompanyStructureCard */ .acm-structure-chevron {} .acm-structure-count {} .acm-structure-description {} .acm-structure-description-button {} .acm-structure-expander {} .acm-structure-expander--placeholder {} .acm-structure-expander-button {} .acm-structure-expander-wrapper {} .acm-structure-handle {} .acm-structure-icon {} .acm-structure-info {} .acm-structure-label {} .acm-structure-message-card {} .acm-structure-modal {} .acm-structure-modal-actions {} .acm-structure-modal-content {} .acm-structure-modal-title {} .acm-structure-modal__actions {} .acm-structure-modal__backdrop {} .acm-structure-modal__body {} .acm-structure-modal__title {} .acm-structure-panel {} .acm-structure-panel__title {} .acm-structure-row {} .acm-structure-toolbar {} .acm-structure-toolbar-card {} .acm-structure-toolbar-card--spaced {} .acm-structure-tree-card {} .acm-structure-tree-content {} .acm-structure-tree-overlay {} .acm-structure-working {} .acm-tree {} .acm-tree-root {} .acm-tree__group {} .acm-tree__item {} .css {} .is-expanded {} .is-root {} .is-team {} .is-user {} .is-working {} .req {} .secondary {} .svg {} /* CompanyStructureEmpty */ .company-management-company-structure-card {} .company-management-company-structure-card__alert {} .company-management-company-structure-card__cta {} .dropin-button {} /* CompanyTeamForm */ .company-team-form__card {} .company-team-form__content {} .company-team-form__overlay {} .dropin-field {} .dropin-field__label {} .is-working {} /* CompanyUserForm */ .company-user-form__card {} .company-user-form__content {} .company-user-form__overlay {} .dropin-field {} .dropin-field__label {} .is-working {} /* CompanyUsersManagementModal */ .company-management-company-users-management-modal {} .company-management-company-users-management-modal-overlay {} .company-management-company-users-management-modal__actions {} .company-management-company-users-management-modal__alert {} .company-management-company-users-management-modal__button-cancel {} .company-management-company-users-management-modal__button-delete {} .company-management-company-users-management-modal__button-primary {} .company-management-company-users-management-modal__close {} .company-management-company-users-management-modal__content {} .company-management-company-users-management-modal__header {} .company-management-company-users-management-modal__text {} .company-management-company-users-management-modal__title {} /* CustomerCompanyInfoCard */ .customer-company-info-card {} .customer-company-info-card__content {} .dropin-card__content {} /* DeleteRoleModal */ .delete-role-modal {} .delete-role-modal__actions {} .delete-role-modal__cancel-btn {} .delete-role-modal__confirm-btn {} .delete-role-modal__content {} .delete-role-modal__ok-btn {} /* EditCompanyProfile */ .account-edit-company-profile {} .account-edit-company-profile-form {} .account-edit-company-profile-form__field {} .account-edit-company-profile-form__section {} .account-edit-company-profile-form__section-title {} .account-edit-company-profile__actions {} .account-edit-company-profile__loading-overlay {} .account-edit-company-profile__loading-text {} .account-edit-company-profile__notification {} .account-edit-company-profile__title {} .dropin-card__content {} /* EditRoleAndPermission */ .acm-tree__group {} .acm-tree__item {} .dropin-field__label {} .edit-role-and-permission {} .edit-role-and-permission-form {} .edit-role-and-permission__actions {} .edit-role-and-permission__loading-overlay {} .edit-role-and-permission__loading-text {} .edit-role-and-permission__notification {} .edit-role-and-permission__permissions-description {} .edit-role-and-permission__section {} .edit-role-and-permission__section-title {} .edit-role-and-permission__title {} .edit-role-and-permission__tree-container {} .edit-role-and-permission__tree-controls {} .edit-role-and-permission__tree-expander {} .edit-role-and-permission__tree-label {} .edit-role-and-permission__tree-loading {} .edit-role-and-permission__tree-node {} .edit-role-and-permission__tree-spacer {} .edit-role-and-permission__validation-spinner {} /* RoleAndPermissionTable */ .add-role-section {} .company-management-role-and-permission-table {} .dropin-header-container__divider {} .dropin-picker__button {} .dropin-picker__option {} .dropin-table__body__cell {} .dropin-table__body__row {} .dropin-table__header {} .dropin-table__header__cell {} .dropin-table__header__row {} .dropin-table__table {} .item-count {} .no-actions {} .page-actions {} .page-content {} .page-footer {} .page-header {} .page-size-loading {} .page-size-picker {} .pagination-controls {} .pagination-label {} .pagination-section {} .role-action-button {} .role-action-wrapper {} .role-actions {} .role-actions-container {} .roles-actions {} .roles-and-permissions-card {} .roles-and-permissions-page {} .roles-table-container {} .table-footer {} .table-footer-center {} .table-footer-left {} .table-footer-right {} /* Tree */ .acm-structure-label {} .acm-structure-row {} .acm-tree {} .acm-tree-root {} .acm-tree__group {} .acm-tree__item {} .acm-tree__label {} .acm-tree__row {} /* CompanyStructure */ .account-company-structure {} .company-structure__title {} /* CompanyUsers */ .addUserButtonContainer {} .companyUsersTable {} .companyUsersTable__empty {} .edit-user-button {} .filterButtons {} .loadingContainer {} .manage-user-button {} .pageSizeSelector {} .paginationButtons {} .paginationContainer {} .sr-only {} .user-actions {} ``` For the source CSS files, see the https://github.com/adobe-commerce/storefront-company-management/tree/main/src. --- # CompanySwitcher Container Allows users to switch between multiple companies they have access to using a dropdown selector. Version: 1.2.0 ## Configuration The `CompanySwitcher` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `ariaLabel` | `string` | No | Sets a custom aria-label for the company picker dropdown. Use to improve accessibility by providing descriptive text for screen readers, especially when the picker is embedded in contexts where the default label may not be clear. | | `onCompanyChange` | `function` | No | Callback when the user selects a different company. Use to refresh page data, update application state, or trigger navigation when the company context changes. | | `size` | `number` | No | Maximum number of companies to fetch for the picker. Defaults to `100`. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `CompanySwitcher` container: ```js await provider.render(CompanySwitcher, { onCompanyChange: () => { const redirect = Object.entries(redirections).find(([pattern]) => { const [pathname, search] = pattern.split('?'); return window.location.pathname.includes(pathname) && (!search || window.location.search.includes(search)); }); if (redirect) { const [, redirectUrl] = redirect; window.location.href = redirectUrl; } else { window.location.reload(); } } })(block); ``` --- # Company Switcher Containers The **Company Switcher** drop-in provides pre-built container components for integrating into your storefront. Version: 1.2.0 ## What are Containers? Containers are pre-built UI components that combine functionality, state management, and presentation. They provide a complete solution for specific features and can be customized through props, slots, and CSS. ## Available Containers | Container | Description | | --------- | ----------- | | [CompanySwitcher](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-switcher/containers/company-switcher/) | Allows users to switch between multiple companies they have access to using a dropdown selector. | > Each container is designed to work independently but can be composed together to create comprehensive user experiences. --- # Company Switcher Dictionary The **Company Switcher dictionary** contains all user-facing text, labels, and messages displayed by this drop-in. Customize the dictionary to: - **Localize** the drop-in for different languages and regions - **Customize** labels and messages to match your brand voice - **Override** default text without modifying source code for the drop-in Dictionaries use the **i18n (internationalization)** pattern, where each text string is identified by a unique key path. Version: 1.2.0 ## How to customize Override dictionary values during drop-in initialization. The drop-in deep-merges your custom values with the defaults. ```javascript await initialize({ langDefinitions: { en_US: { "Company Switcher": { "Component": { "heading": "My Custom Heading", "buttonText": "Click Me" } } } } }); ``` You only need to include the keys you want to change. For multi-language support and advanced patterns, see the [Dictionary customization guide](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/dictionaries/). ## Default keys and values Below are the default English (`en_US`) strings provided by the **Company Switcher** drop-in: ```json title="en_US.json" { "": {} } ``` --- # Company Switcher Events and Data The **Company Switcher** drop-in uses the [event bus](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/events/) to emit and listen to events for communication between drop-ins and external integrations. Version: 1.2.0 ## Events reference {/* EVENTS_TABLE_START */} | Event | Direction | Description | |-------|-----------|-------------| | [checkout/initialized](#checkoutinitialized-listens) | Listens | Fired by Checkout (`checkout`) when the component completes initialization. | | [checkout/updated](#checkoutupdated-listens) | Listens | Fired by Checkout (`checkout`) when the component state is updated. | | [company/updated](#companyupdated-listens) | Listens | Fired by Company (`company`) when the component state is updated. | | [companyContext/changed](#companycontextchanged-emits-and-listens) | Emits and listens | Emitted when a change occurs. | {/* EVENTS_TABLE_END */} ## Event details The following sections provide detailed information about each event, including its direction, event payload, and usage examples. ### `checkout/initialized` (listens) Fired by Checkout (`checkout`) when the component completes initialization. #### Event payload #### Example ```js events.on('checkout/initialized', (payload) => { console.log('checkout/initialized event received:', payload); // Add your custom logic here }); ``` ### `checkout/updated` (listens) Fired by Checkout (`checkout`) when the component state is updated. #### Event payload #### Example ```js events.on('checkout/updated', (payload) => { console.log('checkout/updated event received:', payload); // Add your custom logic here }); ``` ### `company/updated` (listens) Fired by Company (`company`) when the component state is updated. #### Event payload ```typescript { company: { id: string; name: string; email: string; legalAddress: { street: string[]; city: string; region: { region: string; regionCode: string; regionId: number; } countryCode: string; postcode: string; telephone: string; } companyAdmin: { id: string; firstname: string; lastname: string; email: string; } salesRepresentative: { firstname: string; lastname: string; email: string; } availablePaymentMethods: Array<{ code: string; title: string; }>; availableShippingMethods: Array<{ code: string; title: string; }>; canEditAccount: boolean; canEditAddress: boolean; permissionsFlags: { canViewAccount: boolean; canEditAccount: boolean; canViewAddress: boolean; canEditAddress: boolean; canViewContacts: boolean; canViewPaymentInformation: boolean; canViewShippingInformation: boolean; } customerRole: { id: string; name: string; permissions: any[]; } customerStatus: string; } } ``` #### Example ```js events.on('company/updated', (payload) => { console.log('company/updated event received:', payload); // Add your custom logic here }); ``` ### `companyContext/changed` (emits and listens) Emitted when a change occurs. #### Event payload ```typescript string | null ``` #### Example ```js events.on('companyContext/changed', (payload) => { console.log('companyContext/changed event received:', payload); // Add your custom logic here }); ``` --- # Company Switcher Functions The Company Switcher drop-in provides API functions for managing company context and headers in multi-company B2B scenarios. Version: 1.2.0 | Function | Description | | --- | --- | | [`getCompanyHeaderManager`](#getcompanyheadermanager) | Returns the singleton `CompanyHeaderManager` instance that manages company-specific headers for all configured `GraphQL` modules. | | [`getCustomerCompanyInfo`](#getcustomercompanyinfo) | Retrieves the customer's current company context information including the active company ID, company name, and list of available companies for the user. | | [`getGroupHeaderManager`](#getgroupheadermanager) | Returns the singleton `GroupHeaderManager` instance that manages customer group headers for all configured `GraphQL` modules. | | [`updateCustomerGroup`](#updatecustomergroup) | Updates the customer group context for the current shopper. | ## getCompanyHeaderManager Returns the singleton CompanyHeaderManager instance that manages company-specific headers for all configured GraphQL modules. Use the returned manager to set, remove, or check company headers. ```typescript function getCompanyHeaderManager(): any ``` > After calling `manager.setCompanyHeaders()`, all subsequent GraphQL requests will operate in the context of the specified company. Ensure you refresh all company-dependent data after switching companies. > Passing `null` to `setCompanyHeaders()` removes the company context headers from all GraphQL requests. This is useful when logging out or switching to a non-company user context. > The `CompanyHeaderManager` is a singleton. Calling `getCompanyHeaderManager()` multiple times returns the same instance, ensuring consistent header management across your application. ### Usage scenarios - Switch between companies for multi-company users. - Set company context after user selection. - Initialize company context on page load. - Change active company from a dropdown selector. - Restore company context from session storage. - Remove company context by passing `null`. - Check the current company header state. - Configure custom header keys dynamically. ### Events The manager's `setCompanyHeaders()` method emits the [`companyContext/changed`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-switcher/events/#companycontextchanged-emits-and-listens) event after successfully setting or removing the company headers. ### Returns Returns a `CompanyHeaderManager` instance with the following methods: ```typescript { setCompanyHeaders(companyId: string | null): void; removeCompanyHeaders(): void; isCompanyHeaderSet(): boolean; setHeaderKey(headerKey: string): void; setFetchGraphQlModules(modules: FetchGraphQL[]): void; } ``` ### Example ```js // Get the manager instance const manager = getCompanyHeaderManager(); // Switch to a specific company manager.setCompanyHeaders('company-123'); // Remove company headers (switch to no-company context) manager.setCompanyHeaders(null); // Check if headers are set if (manager.isCompanyHeaderSet()) { console.log('Company context is active'); } // Listen for context changes events.on('companyContext/changed', (companyId) => { if (companyId) { console.log('Switched to company:', companyId); } else { console.log('Company context removed'); } // Refresh all company-dependent data refreshCompanyData(); }); ``` ## getCustomerCompanyInfo Retrieves the customer's current company context information including the active company ID, company name, and list of available companies for the user. ```typescript function getCustomerCompanyInfo(): Promise ``` ### Usage scenarios - Determine which company is currently active. - Load company-specific data on page load. - Check if user has company access. - Display current company information. - Conditional rendering based on company context. - Populate company dropdown selector with available companies. ### Events Does not emit any drop-in events. ### Returns Returns a Promise that resolves to a `CustomerCompanyInfo` object containing: ```typescript { currentCompany: { companyId: string; companyName: string; }; customerCompanies: Array<{ value: string; // Company ID text: string; // Company name }>; } ``` ### Example ```js // Get current company context const info = await getCustomerCompanyInfo(); console.log('Active company:', info.currentCompany.companyName); console.log('Company ID:', info.currentCompany.companyId); console.log('Available companies:', info.customerCompanies.length); // Use context to load company-specific data if (info.currentCompany.companyId) { loadCompanyData(info.currentCompany.companyId); } ``` ## getGroupHeaderManager Returns the singleton GroupHeaderManager instance that manages customer group headers for all configured GraphQL modules. Use the returned manager to set, remove, or check group headers for proper pricing and catalog visibility. ```typescript function getGroupHeaderManager(): any ``` > Customer group changes typically happen automatically based on the company context. You usually only need to call `manager.setGroupHeaders()` directly in advanced scenarios like admin impersonation or testing. > The `GroupHeaderManager` is a singleton. Calling `getGroupHeaderManager()` multiple times returns the same instance, ensuring consistent header management across your application. ### Usage scenarios - Set the customer group context for proper pricing. - Apply group-specific catalog rules. - Initialize the group context on login. - Switch groups for testing or admin purposes. - Coordinate with company context changes. - Check current group header state. - Configure custom header keys dynamically. ### Events Does not emit any drop-in events. ### Returns Returns a `GroupHeaderManager` instance with the following methods: ```typescript { setGroupHeaders(groupId: string | null): void; removeGroupHeaders(): void; isGroupHeaderSet(): boolean; setHeaderKey(headerKey: string): void; setFetchGraphQlModules(modules: FetchGraphQL[]): void; } ``` ### Example ```js // Get the manager instance const manager = getGroupHeaderManager(); // Set customer group for pricing manager.setGroupHeaders('wholesale-group-id'); // Subsequent requests will use this group context // Prices and catalog visibility will reflect group settings await loadProducts(); // Products will show group-specific prices // Remove group headers manager.setGroupHeaders(null); // Check if headers are set if (manager.isGroupHeaderSet()) { console.log('Group context is active'); } ``` ## updateCustomerGroup The `updateCustomerGroup` function updates the customer group context for the current shopper (for example, after company or role changes). ```ts const updateCustomerGroup = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns `string | null`. ## Data models The following data models are used by functions in this drop-in. ### CustomerCompanyInfo The `CustomerCompanyInfo` object is returned by the following functions: [`getCustomerCompanyInfo`](#getcustomercompanyinfo). ```ts interface CustomerCompanyInfo { currentCompany: Company; customerCompanies: CompanyOption[]; customerGroupId: string; } ``` ## Integration with company context The Company Switcher functions work together to manage the complete company and group context: ```js // Get manager instances const companyManager = getCompanyHeaderManager(); const groupManager = getGroupHeaderManager(); // Complete company switch workflow async function switchCompany(companyId, groupId) { // 1. Set the company headers companyManager.setCompanyHeaders(companyId); // 2. Set the group headers if (groupId) { groupManager.setGroupHeaders(groupId); } // 3. Verify the context const info = await getCustomerCompanyInfo(); console.log('Switched to:', info.currentCompany.companyName); // 4. Refresh all company-dependent data await Promise.all([ refreshPurchaseOrders(), refreshQuotes(), refreshRequisitionLists(), refreshCompanyUsers() ]); } ``` {/* This documentation is auto-generated from the drop-in source repository: REPO_URL */} --- # Company Switcher overview The Company Switcher drop-in enables multi-company user access and company context switching for Adobe Commerce storefronts. It also supports company context retrieval and automatic GraphQL header management. ## Supported Commerce features The following table provides an overview of the Adobe Commerce features that the Company Switcher drop-in supports: | Feature | Status | | ------- | ------ | | Multi-company user access | Supported | | Company context switching | Supported | | Company context retrieval | Supported | | Automatic GraphQL header management | Supported | | Customer group header management | Supported | | Real-time context change events | Supported | | Data isolation across companies | Supported | | Permission-based access control | Supported | | Session persistence | Supported | | GraphQL API integration | Supported | --- # Company Switcher initialization The **Company Switcher initializer** configures the drop-in for managing multi-company contexts in B2B storefronts. Use initialization to customize company context management, header injection, session persistence, and GraphQL module integration. Version: 1.2.1 ## Configuration options The following table describes the configuration options available for the **Company Switcher** initializer: | Parameter | Type | Req? | Description | |---|---|---|---| | `langDefinitions` | [`LangDefinitions`](#langdefinitions) | No | Language definitions for internationalization (i18n). Override dictionary keys for localization or branding. | | `companyHeader` | `string` | No | HTTP header name for company identification. Defaults to `X-Adobe-Company`. | | `customerGroupHeader` | `string` | No | HTTP header name for customer group identification. Defaults to `Magento-Customer-Group`. | | `companySessionStorageKey` | `string` | No | Session storage key for persisting company context. Defaults to `DROPIN__COMPANYSWITCHER__COMPANY__CONTEXT`. | | `groupSessionStorageKey` | `string` | No | Session storage key for persisting group context. Defaults to `DROPIN__COMPANYSWITCHER__GROUP__CONTEXT`. | | `fetchGraphQlModules` | `FetchGraphQL[]` | No | `GraphQL` modules that will have company headers applied. Defaults to `\[\]`. | | `groupGraphQlModules` | `FetchGraphQL[]` | No | `GraphQL` modules that will have group headers applied. Defaults to `\[\]`. | ## Default configuration The initializer runs with these defaults when no configuration is provided: ```javascript title="scripts/initializers/company-switcher.js" // All configuration options are optional await initializers.mountImmediately(initialize, { langDefinitions: {}, // Uses built-in English strings models: {}, // Uses default data models // Drop-in-specific defaults: // companyHeader: undefined // See configuration options below // customerGroupHeader: undefined // See configuration options below // companySessionStorageKey: undefined // See configuration options below // groupSessionStorageKey: undefined // See configuration options below // fetchGraphQlModules: undefined // See configuration options below // groupGraphQlModules: undefined // See configuration options below }); ``` ## Language definitions Override dictionary keys for localization or branding. The `langDefinitions` object maps locale keys to custom strings that override default text for the drop-in. ```javascript title="scripts/initializers/company-switcher.js" const customStrings = { 'AddToCart': 'Add to Bag', 'Checkout': 'Complete Purchase', 'Price': 'Cost', }; const langDefinitions = { default: customStrings, }; await initializers.mountImmediately(initialize, { langDefinitions }); ``` > For complete dictionary customization including all available keys and multi-language support, see the [Company Switcher Dictionary](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-switcher/dictionary/) page. ## Customizing data models Extend or transform data models by providing custom transformer functions. Use the `models` option to add custom fields or modify existing data structures returned from the backend. ### Available models The following models can be customized through the `models` configuration option: > No customizable models are available for this drop-in. The following example shows how to customize the `CustomModel` model for the **Company Switcher** drop-in: ```javascript title="scripts/initializers/company-switcher.js" const models = { CustomModel: { transformer: (data) => ({ // Add custom fields from backend data customField: data?.custom_field, promotionBadge: data?.promotion?.label, // Transform existing fields displayPrice: data?.price?.value ? `${data.price.value}` : 'N/A', }), }, }; await initializers.mountImmediately(initialize, { models }); ``` ## Drop-in configuration The **Company Switcher initializer** configures the drop-in for managing multi-company contexts in B2B storefronts. Use initialization to customize company context management, header injection, session persistence, and GraphQL module integration. ```javascript title="scripts/initializers/company-switcher.js" await initializers.mountImmediately(initialize, { langDefinitions: {}, companyHeader: 'X-Custom-Header', customerGroupHeader: 'X-Custom-Header', companySessionStorageKey: 'customKey', groupSessionStorageKey: 'customKey', fetchGraphQlModules: [], groupGraphQlModules: [], }); ``` > Refer to the [Configuration options](#configuration-options) table for detailed descriptions of each option. ## Configuration types The following TypeScript definitions show the structure of each configuration object: ### langDefinitions Maps locale identifiers to dictionaries of key-value pairs. The `default` locale is used as the fallback when no specific locale matches. Each dictionary key corresponds to a text string used in the drop-in UI. ```typescript langDefinitions?: { [locale: string]: { [key: string]: string; }; }; ``` --- # Company Switcher Quick Start Get started with the Company Switcher drop-in to enable multi-company context switching for B2B users. Version: 1.2.0 ## Quick example The Company Switcher drop-in is included in the https://github.com/hlxsites/aem-boilerplate-commerce. This example shows the basic pattern: ```js // 1. Import initializer (handles all setup) // 2. Import the container you need // 3. Import the provider // 4. Render in your block export default async function decorate(block) { await provider.render(CompanySwitcher, { // Configuration options - see Containers page })(block); } ``` **New to drop-ins?** See the [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/) guide for complete step-by-step instructions. ## Quick reference **Import paths:** - Initializer: `import '../../scripts/initializers/company-switcher.js'` - Containers: `import ContainerName from '@dropins/storefront-company-switcher/containers/ContainerName.js'` - Provider: `import { render } from '@dropins/storefront-company-switcher/render.js'` **Package:** `@dropins/storefront-company-switcher` **Version:** 1.2.0 (verify compatibility with your Commerce instance) **Example container:** `CompanySwitcher` ## Learn more - [Containers](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-switcher/containers/) - Available UI components and configuration options - [Initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-switcher/initialization/) - Customize initializer settings and data models - [Functions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-switcher/functions/) - Control drop-in behavior programmatically - [Events](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-switcher/events/) - Listen to and respond to drop-in state changes - [Slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-switcher/slots/) - Extend containers with custom content --- # Company Switcher Slots The Company Switcher drop-in does not expose any slots for customization. ## Why no slots? This drop-in provides functionality through API methods and configuration options rather than UI customization points. Slots may be added in future versions as the feature set for the drop-in expands. Version: 1.2.0 --- # Company Switcher styles Customize the Company Switcher drop-in using CSS classes and design tokens. This page covers the Company Switcher-specific container classes and customization examples. For comprehensive information about design tokens, responsive breakpoints, and styling best practices, see [Styling Drop-In Components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/). Version: 1.2.0 ## Customization example Add this to the CSS file of the specific https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/ where you're using the Company Switcher drop-in. For a complete list of available design tokens (colors, spacing, typography, and more), see the [Design tokens reference](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/#design-tokens-reference). ```css title="styles/styles.css" /* Target Company Switcher containers */ .company-switcher-container { /* Use the browser DevTools to find the specific classes you need */ } ``` ## Container classes The Company Switcher drop-in uses BEM-style class naming. Use the browser DevTools to inspect elements and find specific class names. --- # B2B drop-ins overview B2B drop-ins are pre-built, customizable UI components that provide complete B2B commerce functionality for your storefront. Each drop-in handles a specific aspect of the business-to-business shopping experience, from company management to purchase order workflows. ## Available B2B drop-ins | Drop-in | Description | | ------- | ----------- | | [Company Management](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/) | Enables company profile management and role-based permissions for Adobe Commerce storefronts. | | [Company Switcher](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-switcher/) | Provides a UI component for users to switch between multiple companies they are associated with. | | [Purchase Order](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/) | Manages purchase order workflows, approval rules, and purchase order history for B2B transactions. | | [Quote Management](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/) | Enables negotiable quotes for B2B customers with quote request, negotiation, and approval workflows. | | [Quick Order](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/) | Bulk ordering by SKU, search, and CSV upload; Quick Order page and Grid Ordering for configurable products on PDP. | | [Requisition List](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/) | Provides tools for creating and managing requisition lists for repeat purchases and bulk ordering. | --- # ApprovalRuleDetails Container Displays detailed information for a specific purchase order approval rule including conditions and approvers. Version: 1.2.0 ## Configuration The `ApprovalRuleDetails` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `withHeader` | `boolean` | No | When true, displays the header section. Set to false when embedding the container within a layout that provides its own header. | | `withWrapper` | `boolean` | No | When true, wraps the container in a styled wrapper. Set to false for custom styling or when the container is embedded within another styled component. | | `className` | `string` | No | Additional CSS classes to apply to the container for custom styling. | | `approvalRuleID` | `string` | No | The unique identifier for the approval rule to display or manage. Required to fetch the correct data from the backend. | | `routeApprovalRulesList` | `function` | Yes | Function to generate the URL for navigating to the approval rules list. Use this to implement custom routing logic or add query parameters. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `ApprovalRuleDetails` container: ```js await provider.render(ApprovalRuleDetails, { routeApprovalRulesList: routeApprovalRulesList, withHeader: true, withWrapper: true, })(block); ``` --- # ApprovalRuleForm Container Provides a form for creating or editing purchase order approval rules with validation and submission handling. Version: 1.2.0 ## Configuration The `ApprovalRuleForm` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `withHeader` | `boolean` | No | When true, displays the header section. Set to false when embedding the container within a layout that provides its own header. | | `withWrapper` | `boolean` | No | When true, wraps the container in a styled wrapper. Set to false for custom styling or when the container is embedded within another styled component. | | `className` | `string` | No | Additional CSS classes to apply to the container for custom styling. | | `approvalRuleID` | `string` | No | The unique identifier for the approval rule to display or manage. Required to fetch the correct data from the backend. | | `routeApprovalRulesList` | `function` | Yes | Function to generate the URL for navigating to the approval rules list. Use this to implement custom routing logic or add query parameters. | | `onSubmit` | `function` | No | Callback triggered when form is submitted. Use for custom success handling or navigation. | | `onChange` | `function` | No | Callback triggered when form values change. Use for real-time validation or tracking. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `ApprovalRuleForm` container: ```js await provider.render(ApprovalRuleForm, { routeApprovalRulesList: routeApprovalRulesList, withHeader: true, withWrapper: true, })(block); ``` --- # ApprovalRulesList Container Displays a list of purchase order approval rules with options to create, edit, and view rule details. Version: 1.2.0 ## Configuration The `ApprovalRulesList` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `initialPageSize` | `PageSizeListProps[]` | No | The initial number of items to display per page in the approval rules table. Use this to control default pagination based on screen size or user preferences. | | `routeCreateApprovalRule` | `function` | No | Function to generate the URL for creating a new approval rule. Use this to implement custom routing or add context parameters for rule creation. | | `routeEditApprovalRule` | `function` | No | Function to generate the URL for editing an approval rule. Receives the rule ID. Use this to implement custom routing or add context parameters. | | `routeApprovalRuleDetails` | `function` | No | Function to generate the URL for viewing approval rule details. Receives the rule ID. Use this to implement custom routing or add context parameters. | | `setColumns` | `function` | No | Function to customize the table columns displayed. Receives default columns and returns modified columns. Use this to show/hide columns, reorder them, or add custom column definitions based on user roles or preferences. | | `setRowsData` | `function` | No | Function to transform or filter the row data before display. Receives default rows and returns modified rows. Use this to add custom data processing, formatting, or filtering logic. | | `className` | `string` | No | Additional CSS classes to apply to the container for custom styling. | | `withHeader` | `boolean` | No | When true, displays the header section. Set to false when embedding the container within a layout that provides its own header. | | `withWrapper` | `boolean` | No | When true, wraps the container in a styled wrapper. Set to false for custom styling or when the container is embedded within another styled component. | | `skeletonRowCount` | `number` | No | Number of skeleton rows to display while loading data. Use this to provide visual feedback during data fetching and improve perceived performance. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `ApprovalRulesList` container: ```js await provider.render(ApprovalRulesList, { initialPageSize: [], routeCreateApprovalRule: routeCreateApprovalRule, routeEditApprovalRule: routeEditApprovalRule, })(block); ``` --- # CompanyPurchaseOrders Container Displays all purchase orders for the entire company with filtering, sorting, and pagination capabilities. Version: 1.2.0 ## Configuration The `CompanyPurchaseOrders` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `initialPageSize` | `PageSizeListProps[]` | Yes | The initial number of items to display per page in the approval rules table. Use this to control default pagination based on screen size or user preferences. | | `routePurchaseOrderDetails` | `function` | No | Function to generate the URL for navigating to the purchase order details. Use this to implement custom routing logic or add query parameters. | | `setColumns` | `function` | No | Function to customize the table columns displayed. Receives default columns and returns modified columns. Use this to show/hide columns, reorder them, or add custom column definitions based on user roles or preferences. | | `setRowsData` | `function` | No | Function to transform or filter the row data before display. Receives default rows and returns modified rows. Use this to add custom data processing, formatting, or filtering logic. | | `className` | `string` | No | Additional CSS classes to apply to the container for custom styling. | | `withHeader` | `boolean` | No | When true, displays the header section. Set to false when embedding the container within a layout that provides its own header. | | `withWrapper` | `boolean` | No | When true, wraps the container in a styled wrapper. Set to false for custom styling or when the container is embedded within another styled component. | | `skeletonRowCount` | `number` | No | Number of skeleton rows to display while loading data. Use this to provide visual feedback during data fetching and improve perceived performance. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `CompanyPurchaseOrders` container: ```js await provider.render(CompanyPurchaseOrders, { initialPageSize: [], routePurchaseOrderDetails: routePurchaseOrderDetails, setColumns: setColumns, })(block); ``` --- # CustomerPurchaseOrders Container Displays purchase orders created by the currently authenticated customer with management controls. Version: 1.2.0 ## Configuration The `CustomerPurchaseOrders` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `initialPageSize` | `PageSizeListProps[]` | Yes | The initial number of items to display per page in the approval rules table. Use this to control default pagination based on screen size or user preferences. | | `routePurchaseOrderDetails` | `function` | No | Function to generate the URL for navigating to the purchase order details. Use this to implement custom routing logic or add query parameters. | | `setColumns` | `function` | No | Function to customize the table columns displayed. Receives default columns and returns modified columns. Use this to show/hide columns, reorder them, or add custom column definitions based on user roles or preferences. | | `setRowsData` | `function` | No | Function to transform or filter the row data before display. Receives default rows and returns modified rows. Use this to add custom data processing, formatting, or filtering logic. | | `className` | `string` | No | Additional CSS classes to apply to the container for custom styling. | | `withHeader` | `boolean` | No | When true, displays the header section. Set to false when embedding the container within a layout that provides its own header. | | `withWrapper` | `boolean` | No | When true, wraps the container in a styled wrapper. Set to false for custom styling or when the container is embedded within another styled component. | | `skeletonRowCount` | `number` | No | Number of skeleton rows to display while loading data. Use this to provide visual feedback during data fetching and improve perceived performance. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `CustomerPurchaseOrders` container: ```js await provider.render(CustomerPurchaseOrders, { initialPageSize: [], routePurchaseOrderDetails: routePurchaseOrderDetails, setColumns: setColumns, })(block); ``` --- # Purchase Order Containers The **Purchase Order** drop-in provides pre-built container components for integrating into your storefront. Version: 1.2.0 ## What are Containers? Containers are pre-built UI components that combine functionality, state management, and presentation. They provide a complete solution for specific features and can be customized through props, slots, and CSS. ## Available Containers | Container | Description | | --------- | ----------- | | [ApprovalRuleDetails](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/containers/approval-rule-details/) | Displays detailed information for a specific purchase order approval rule including conditions and approvers. | | [ApprovalRuleForm](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/containers/approval-rule-form/) | Provides a form for creating or editing purchase order approval rules with validation and submission handling. | | [ApprovalRulesList](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/containers/approval-rules-list/) | Displays a list of purchase order approval rules with options to create, edit, and view rule details. | | [CompanyPurchaseOrders](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/containers/company-purchase-orders/) | Displays all purchase orders for the entire company with filtering, sorting, and pagination capabilities. | | [CustomerPurchaseOrders](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/containers/customer-purchase-orders/) | Displays purchase orders created by the currently authenticated customer with management controls. | | [PurchaseOrderApprovalFlow](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/containers/purchase-order-approval-flow/) | Manages the approval workflow for a purchase order including approval actions and status updates. | | [PurchaseOrderCommentForm](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/containers/purchase-order-comment-form/) | Provides a form for adding comments to a purchase order with validation and submission handling. | | [PurchaseOrderCommentsList](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/containers/purchase-order-comments-list/) | Displays the list of comments associated with a purchase order in chronological order. | | [PurchaseOrderConfirmation](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/containers/purchase-order-confirmation/) | Displays confirmation details after a purchase order is successfully created or approved. | | [PurchaseOrderHistoryLog](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/containers/purchase-order-history-log/) | Displays the chronological history of actions and status changes for a purchase order. | | [PurchaseOrderStatus](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/containers/purchase-order-status/) | Displays the current status and detailed information for a specific purchase order. | | [RequireApprovalPurchaseOrders](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/containers/require-approval-purchase-orders/) | Displays purchase orders that require approval from the currently authenticated user. | > Each container is designed to work independently but can be composed together to create comprehensive user experiences. --- # PurchaseOrderApprovalFlow Container Manages the approval workflow for a purchase order including approval actions and status updates. Version: 1.2.0 ## Configuration The `PurchaseOrderApprovalFlow` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `className` | `string` | No | Additional CSS classes to apply to the container for custom styling. | | `withHeader` | `boolean` | No | When true, displays the header section. Set to false when embedding the container within a layout that provides its own header. | | `withWrapper` | `boolean` | No | When true, wraps the container in a styled wrapper. Set to false for custom styling or when the container is embedded within another styled component. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `PurchaseOrderApprovalFlow` container: ```js await provider.render(PurchaseOrderApprovalFlow, { className: "Example Name", withHeader: true, withWrapper: true, })(block); ``` --- # PurchaseOrderCommentForm Container Provides a form for adding comments to a purchase order with validation and submission handling. Version: 1.2.0 ## Configuration The `PurchaseOrderCommentForm` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `withHeader` | `boolean` | No | When true, displays the header section. Set to false when embedding the container within a layout that provides its own header. | | `withWrapper` | `boolean` | No | When true, wraps the container in a styled wrapper. Set to false for custom styling or when the container is embedded within another styled component. | | `className` | `string` | No | Additional CSS classes to apply to the container for custom styling. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `PurchaseOrderCommentForm` container: ```js await provider.render(PurchaseOrderCommentForm, { withHeader: true, withWrapper: true, className: "Example Name", })(block); ``` --- # PurchaseOrderCommentsList Container Displays the list of comments associated with a purchase order in chronological order. Version: 1.2.0 ## Configuration The `PurchaseOrderCommentsList` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `withHeader` | `boolean` | No | When true, displays the header section. Set to false when embedding the container within a layout that provides its own header. | | `withWrapper` | `boolean` | No | When true, wraps the container in a styled wrapper. Set to false for custom styling or when the container is embedded within another styled component. | | `visibleRecordsLimit` | `number` | No | Maximum number of comments to display initially. Additional comments can be revealed with a 'Show More' action. Use this to prevent overwhelming users with long comment threads. | | `className` | `string` | No | Additional CSS classes to apply to the container for custom styling. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `PurchaseOrderCommentsList` container: ```js await provider.render(PurchaseOrderCommentsList, { withHeader: true, withWrapper: true, visibleRecordsLimit: 0, })(block); ``` --- # PurchaseOrderConfirmation Container Displays confirmation details after a purchase order is successfully created or approved. Version: 1.2.0 ## Configuration The `PurchaseOrderConfirmation` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `purchaseOrderNumber` | `string \| number` | Yes | | | `routePurchaseOrderDetails` | `function` | Yes | Function to generate the URL for navigating to the purchase order details. Use this to implement custom routing logic or add query parameters. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `PurchaseOrderConfirmation` container: ```js await provider.render(PurchaseOrderConfirmation, { purchaseOrderNumber: "example", routePurchaseOrderDetails: routePurchaseOrderDetails, })(block); ``` --- # PurchaseOrderHistoryLog Container Displays the chronological history of actions and status changes for a purchase order. Version: 1.2.0 ## Configuration The `PurchaseOrderHistoryLog` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `visibleRecordsLimit` | `number` | No | Maximum number of history entries to display initially. Additional entries can be revealed with a 'Show More' action. Use this to prevent overwhelming users with long audit trails. | | `withHeader` | `boolean` | No | When true, displays the header section. Set to false when embedding the container within a layout that provides its own header. | | `withWrapper` | `boolean` | No | When true, wraps the container in a styled wrapper. Set to false for custom styling or when the container is embedded within another styled component. | | `className` | `string` | No | Additional CSS classes to apply to the container for custom styling. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `PurchaseOrderHistoryLog` container: ```js await provider.render(PurchaseOrderHistoryLog, { visibleRecordsLimit: 0, withHeader: true, withWrapper: true, })(block); ``` --- # PurchaseOrderStatus Container Displays the current status and detailed information for a specific purchase order. Version: 1.2.0 ## Configuration The `PurchaseOrderStatus` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `className` | `string` | No | Additional CSS classes to apply to the container for custom styling. | | `withHeader` | `boolean` | No | When true, displays the header section. Set to false when embedding the container within a layout that provides its own header. | | `withWrapper` | `boolean` | No | When true, wraps the container in a styled wrapper. Set to false for custom styling or when the container is embedded within another styled component. | ## Slots This container exposes the following slots for customization: | Slot | Type | Required | Description | |------|------|----------|-------------| | `PurchaseOrderActions` | `SlotProps` | Yes | Customize action buttons for purchase order operations (approve, reject, cancel, place order). | ## Usage The following example demonstrates how to use the `PurchaseOrderStatus` container: ```js await provider.render(PurchaseOrderStatus, { className: "Example Name", withHeader: true, withWrapper: true, slots: { // Add custom slot implementations here } })(block); ``` --- # RequireApprovalPurchaseOrders Container Displays purchase orders that require approval from the currently authenticated user. Version: 1.2.0 ## Configuration The `RequireApprovalPurchaseOrders` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `initialPageSize` | `PageSizeListProps[]` | Yes | The initial number of items to display per page in the approval rules table. Use this to control default pagination based on screen size or user preferences. | | `routePurchaseOrderDetails` | `function` | No | Function to generate the URL for navigating to the purchase order details. Use this to implement custom routing logic or add query parameters. | | `setColumns` | `function` | No | Function to customize the table columns displayed. Receives default columns and returns modified columns. Use this to show/hide columns, reorder them, or add custom column definitions based on user roles or preferences. | | `setRowsData` | `function` | No | Function to transform or filter the row data before display. Receives default rows and returns modified rows. Use this to add custom data processing, formatting, or filtering logic. | | `className` | `string` | No | Additional CSS classes to apply to the container for custom styling. | | `withHeader` | `boolean` | No | When true, displays the header section. Set to false when embedding the container within a layout that provides its own header. | | `withWrapper` | `boolean` | No | When true, wraps the container in a styled wrapper. Set to false for custom styling or when the container is embedded within another styled component. | | `skeletonRowCount` | `number` | No | Number of skeleton rows to display while loading data. Use this to provide visual feedback during data fetching and improve perceived performance. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `RequireApprovalPurchaseOrders` container: ```js await provider.render(RequireApprovalPurchaseOrders, { initialPageSize: [], routePurchaseOrderDetails: routePurchaseOrderDetails, setColumns: setColumns, })(block); ``` --- # Purchase Order Dictionary The **Purchase Order dictionary** contains all user-facing text, labels, and messages displayed by this drop-in. Customize the dictionary to: - **Localize** the drop-in for different languages and regions - **Customize** labels and messages to match your brand voice - **Override** default text without modifying source code for the drop-in Dictionaries use the **i18n (internationalization)** pattern, where each text string is identified by a unique key path. Version: 1.2.0 ## How to customize Override dictionary values during drop-in initialization. The drop-in deep-merges your custom values with the defaults. ```javascript await initialize({ langDefinitions: { en_US: { "PurchaseOrders": { "customerPurchaseOrders": { "containerTitle": "My Custom Title", "noPurchaseOrders": "No items found" } } } } }); ``` You only need to include the keys you want to change. For multi-language support and advanced patterns, see the [Dictionary customization guide](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/dictionaries/). ## Default keys and values Below are the default English (`en_US`) strings provided by the **Purchase Order** drop-in: ```json title="en_US.json" { "PurchaseOrders": { "customerPurchaseOrders": { "containerTitle": "My purchase orders", "noPurchaseOrders": "No purchase orders found." }, "companyPurchaseOrders": { "containerTitle": "Company purchase orders", "noPurchaseOrders": "No company purchase orders found." }, "requireApprovalPurchaseOrders": { "containerTitle": "Requires my approval", "noPurchaseOrders": "No purchase orders requiring my approval found." }, "approvalRulesList": { "containerTitle": "Approval rules", "emptyTitle": "No approval rules found", "ariaLabel": { "editRule": "Edit approval rule {{ruleName}}", "deleteRule": "Delete approval rule {{ruleName}}", "viewRule": "View approval rule {{ruleName}}" }, "buttons": { "newRule": "Add New Rule" } }, "alertMessages": { "header": { "approve": "Approve Purchase Orders", "reject": "Reject Purchase Orders", "error": "Error" }, "description": { "approve": "The selected purchase orders were approved successfully.", "reject": "The selected purchase orders were rejected successfully.", "error": "An error occurred while processing your request." } }, "purchaseOrdersTable": { "noPurchaseOrders": { "default": "No purchase orders found." }, "pagination": { "status": "Items {{from}}-{{to}} of {{total}}", "pageSizeLabel": { "start": "Show" } }, "loading": "Loading purchase orders...", "actionView": "View", "actionEdit": "Edit", "actionDelete": "Delete", "rulesStatus": { "enabled": "Enabled", "disabled": "Disabled" }, "ruleTypes": { "grand_total": "Grand Total", "number_of_skus": "Number of SKUs", "any_item": "Any Item", "all_items": "All Items" }, "buttons": { "expandedHidden": "Hide", "expandedShow": "Show" }, "appliesToAll": "All", "statusOrder": { "order_placed": "Order placed", "order_failed": "Order failed", "pending": "Pending", "approved": "Approved", "rejected": "Rejected", "canceled": "Canceled", "order_in_progress": "Order in progress", "approval_required": "Approval required", "approved_pending_payment": "Approved pending Payment" }, "expandedRowLabels": { "orderNumber": "Order Number:", "createdDate": "Created Date:", "updatedDate": "Updated Date:", "total": "Total:", "ruleType": "Rule Type:", "appliesTo": "Applies To:", "approver": "Approver:" }, "tableColumns": { "poNumber": "PO #", "orderNumber": "Order #", "createdDate": "Created", "updatedDate": "Updated", "createdBy": "Created By", "status": "Status", "total": "Total", "action": "Action", "ruleName": "Rule Name", "selectAllAriaLabel": "Select all not approved purchase orders" } }, "purchaseOrderConfirmation": { "title": "Your Purchase Order has been submitted for approval.", "messagePrefix": "Your Purchase Order request number is", "messageSuffix": "A copy of this Purchase Order will be emailed to you shortly." }, "purchaseOrderStatus": { "headerText": "Status", "emptyText": "No actions available for this purchase order.", "status": { "pending": { "title": "Pending approval", "message": "Purchase order is awaiting approval." }, "approval_required": { "title": "Approval required", "message": "Purchase order requires approval before it can be processed." }, "approved": { "title": "Order approved", "message": "Purchase order has been approved." }, "order_in_progress": { "title": "Processing in progress", "message": "Purchase order is currently being processed." }, "order_placed": { "title": "Order placed", "message": "Order has been placed successfully." }, "order_failed": { "title": "Order failed", "message": "Order placing has failed." }, "rejected": { "title": "Order rejected", "message": "Purchase order has been rejected." }, "canceled": { "title": "Order canceled", "message": "Purchase order has been canceled." }, "approved_pending_payment": { "title": "Order approved - pending payment", "message": "Purchase order has been approved and is awaiting payment." } }, "alertMessages": { "success": { "approval": "The purchase order was approved successfully.", "reject": "The purchase order was rejected successfully.", "cancel": "The purchase order was canceled successfully.", "placeOrder": "The sales order was placed successfully." }, "errors": { "approval": "An error occurred while approving the purchase order. Please try again.", "reject": "An error occurred while rejecting the purchase order. Please try again.", "cancel": "An error occurred while canceling the purchase order. Please try again.", "placeOrder": "An error occurred while placing the sales order. Please try again." } }, "buttons": { "approve": "Approve", "reject": "Reject", "cancel": "Cancel", "placeOrder": "Place Order" } }, "approvalRuleForm": { "headerText": "Purchase order approval rule", "titleAppliesTo": "Applies To", "titleRuleType": "Rule Type", "titleRequiresApprovalRole": "Requires Approval From", "fields": { "enabled": "Rule Enabled", "disabled": "Rule Disabled", "inputRuleName": { "floatingLabel": "Rule Name", "placeholder": "Rule Name" }, "textAreaDescription": { "label": "Rule Description" }, "appliesTo": { "allUsers": "All Users", "specificRoles": "Specific Roles" }, "ruleTypeOptions": { "grandTotal": "Grand Total", "shippingInclTax": "Shipping Cost", "numberOfSkus": "Number of SKUs" }, "conditionOperators": { "moreThan": "is more than", "lessThan": "is less than", "moreThanOrEqualTo": "is more than or equal to", "lessThanOrEqualTo": "is less than or equal to" }, "inputQuantity": { "floatingLabel": "Enter Amount", "placeholder": "Enter Amount" }, "inputAmount": { "floatingLabel": "Enter Amount", "placeholder": "Enter Amount" }, "buttons": { "cancel": "Cancel", "save": "Save" } }, "errorsMessages": { "required": "This field is required.", "quantity": "Quantity must be greater than 0.", "amount": "Amount must be greater than 0.", "approvers": "Please select at least one approver." } }, "approvalRuleDetails": { "containerTitle": "Approval rule details", "buttons": { "back": "Back to Rules List" }, "fields": { "ruleName": "Rule Name:", "status": "Status:", "description": "Description:", "appliesTo": "Applies To:", "requiresApprovalFrom": "Requires Approval From:", "ruleType": "Rule Type:", "amount": { "label": " amount " }, "statusView": { "enabled": "Enabled", "disabled": "Disabled" }, "condition": { "attribute": { "grand_total": "Grand Total", "shipping_incl_tax": "Shipping Cost", "number_of_skus": "Number of SKUs" }, "operator": { "more_than": "Is more than", "less_than": "Is less than", "more_than_or_equal_to": "Is more than or equal to", "less_than_or_equal_to": "Is less than or equal to" } } } }, "historyLog": { "headerText": "Purchase order history log", "statusTitle": "Status Changes", "emptyText": "No history log available.", "status": { "cancel": "Cancelled on {{date}}", "reject": "Rejected on {{date}}", "place_order_fail": "Failed to place order on {{date}}", "apply_rules": "Rule applied on {{date}}", "place_order": "Order placed on {{date}}", "auto_approve": "Auto approved on {{date}}", "approve": "Approved on {{date}}", "submit": "Submitted for approval on {{date}}" }, "buttons": { "viewMore": "View More", "viewLess": "View Less" }, "ariaLabel": { "showMore": "Show more history items", "showLess": "Show fewer history items" } }, "comments": { "view": { "headerText": "Purchase order comments", "emptyText": "No comments available.", "buttons": { "viewMore": "View More", "viewLess": "View Less" }, "ariaLabel": { "showMore": "Show more comments", "showLess": "Show fewer comments" } }, "add": { "headerText": "Add purchase order comment", "placeholder": "Add your comment", "submit": "Add Comment", "errorMessage": "Something went wrong while adding your comment. Please try again." } }, "approvalFlow": { "headerText": "Purchase order approval flow", "emptyText": "No approval flow is available for this purchase order.", "ariaLabels": { "icons": { "approved": "Status approved", "rejected": "Status rejected", "pending": "Status pending approval" } } } } } ``` --- # Purchase Order Data & Events The **Purchase Order** drop-in uses the [event bus](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/events/) to emit and listen to events for communication between drop-ins and external integrations. Version: 1.2.0 ## Events reference {/* EVENTS_TABLE_START */} | Event | Direction | Description | |-------|-----------|-------------| | [order/data](#orderdata-emits) | Emits | Emitted when data is available or changes. | | [purchase-order/error](#purchase-ordererror-emits) | Emits | Emitted when an error occurs. | | [purchase-order/placed](#purchase-orderplaced-emits) | Emits | Emitted when an order is placed. | | [auth/permissions](#authpermissions-listens) | Listens | Fired by Auth (`auth`) when permissions are updated. | | [purchase-order/data](#purchase-orderdata-emits-and-listens) | Emits and listens | Emitted when data is available or changes. | | [purchase-order/refresh](#purchase-orderrefresh-emits-and-listens) | Emits and listens | Emitted and consumed for internal and external communication. | {/* EVENTS_TABLE_END */} ## Event details The following sections provide detailed information about each event, including its direction, event payload, and usage examples. ### `auth/permissions` (listens) Fired by Auth (`auth`) when permissions are updated. #### Event payload ```typescript { admin?: boolean; [key: string]: boolean | undefined; } ``` #### Example ```js events.on('auth/permissions', (payload) => { console.log('auth/permissions event received:', payload); // Add your custom logic here }); ``` ### `order/data` (emits) Emitted when data is available or changes. #### Event payload ```typescript PurchaseOrderModel['quote'] ``` See [`PurchaseOrderModel`](#purchaseordermodel) for full type definition. #### When triggered - When the Purchase Order Details page loads with a poRef parameter - After purchase-order/refresh event (emitted together with purchase-order/data) #### Example: Display purchase order details ```js events.on('order/data', (payload) => { console.log('Purchase order data for Order containers:', payload.data); // Initialize Order Drop-In containers with PO data displayPurchaseOrderDetailsInOrderContainers(payload.data); // Extract purchase order information const { number, status, items } = payload.data; }); ``` #### Usage scenarios - Initialize Order Drop-In containers with purchase order data. - Display purchase order details on the PO Details page. - Sync PO data after refresh events. - Update UI when PO data loads. --- ### `purchase-order/data` (emits and listens) Triggered when data is available or changes. #### Event payload ```typescript PurchaseOrderModel ``` See [`PurchaseOrderModel`](#purchaseordermodel) for full type definition. #### When triggered - After loading a purchase order - After approving a purchase order - After rejecting a purchase order - After canceling a purchase order - After adding comments to a purchase order - After updating purchase order status #### Example: Example ```js events.on('purchase-order/data', (payload) => { const po = payload.data; console.log('Purchase order updated:', po.number, po.status); // Update the UI to reflect current status updatePurchaseOrderStatus(po.status); // Show approval flow if needed if (po.requiresApproval) { displayApprovalFlow(po.approvalFlow); } }); ``` #### Usage scenarios - Refresh the purchase order details view. - Update purchase order lists. - Display the approval flow progress. - Show status-specific actions. - Update cached purchase order data. --- ### `purchase-order/error` (emits) Emitted when an error occurs. #### Event payload #### Example ```js events.on('purchase-order/error', (payload) => { console.log('purchase-order/error event received:', payload); // Add your custom logic here }); ``` ### `purchase-order/placed` (emits) Emitted when a purchase order is placed (submitted). #### Event payload ```typescript PurchaseOrderModel ``` See [`PurchaseOrderModel`](#purchaseordermodel) for full type definition. #### When triggered - After successfully calling `placePurchaseOrder()` - After converting a cart to a purchase order #### Example 1: Basic purchase order placement ```js events.on('purchase-order/placed', (payload) => { const { number: purchaseOrderNumber, status } = payload.data; const requiresApproval = status !== "APPROVED"; console.log(`Purchase order ${purchaseOrderNumber} placed with status: ${status}`); // Show appropriate confirmation message if (requiresApproval) { showMessage('Purchase order submitted for approval'); redirectToApprovalStatus(purchaseOrderNumber); } else { showMessage('Purchase order placed successfully'); redirectToOrderConfirmation(purchaseOrderNumber); } // Track analytics trackPurchaseOrderPlacement(purchaseOrderNumber, requiresApproval); }); ``` #### Example 2: Complete checkout workflow with notifications ```js async function completePurchaseOrderCheckout(cartId) { try { // Show checkout processing showCheckoutModal('Processing your purchase order...'); // Place the purchase order await placePurchaseOrder(cartId); // Listen for successful placement events.once('purchase-order/placed', async (payload) => { const { number: purchaseOrderNumber, status } = payload.data; const requiresApproval = status !== "APPROVED"; // Close processing modal hideCheckoutModal(); // Clear cart UI clearCartDisplay(); // Show success modal with details if (requiresApproval) { showSuccessModal({ title: 'Purchase Order Submitted', message: `Your purchase order #${purchaseOrderNumber} has been submitted for approval.`, details: [ `Status: Pending Approval`, `You will be notified when it's reviewed.`, `Track your order in the Purchase Orders section.` ], primaryAction: { label: 'View Purchase Order', onClick: () => window.location.href = `/purchase-orders/${purchaseOrderNumber}` }, secondaryAction: { label: 'Continue Shopping', onClick: () => window.location.href = '/products' } }); // Send notification email await sendNotification({ type: 'purchase-order-submitted', purchaseOrderNumber, approvers: payload.data.approvers }); } else { showSuccessModal({ title: 'Order Placed Successfully', message: `Your purchase order #${purchaseOrderNumber} has been placed.`, details: [ `Status: ${status}`, `You will receive a confirmation email shortly.` ], primaryAction: { label: 'View Order', onClick: () => window.location.href = `/orders/${purchaseOrderNumber}` } }); } // Track conversion trackConversion({ type: 'purchase-order', orderNumber: purchaseOrderNumber, requiresApproval, value: payload.data.total, currency: payload.data.currency }); // Update user's PO history count incrementPurchaseOrderCount(); }); } catch (error) { hideCheckoutModal(); showErrorModal({ title: 'Failed to Place Purchase Order', message: error.message || 'An error occurred while processing your order.', action: { label: 'Try Again', onClick: () => completePurchaseOrderCheckout(cartId) } }); console.error('Purchase order placement error:', error); } } ``` #### Example 3: Multi-approval workflow dashboard ```js // Dashboard for tracking all purchase orders class PurchaseOrderDashboard { constructor() { this.pendingOrders = []; this.completedOrders = []; // Listen for new purchase orders events.on('purchase-order/placed', this.handleNewOrder.bind(this)); events.on('purchase-order/data', this.handleOrderUpdate.bind(this)); this.init(); } async init() { await this.loadExistingOrders(); this.render(); } handleNewOrder(payload) { const { number: purchaseOrderNumber, status } = payload.data; const requiresApproval = status !== "APPROVED"; const order = { number: purchaseOrderNumber, status: status, requiresApproval, placedAt: new Date(), ...payload.data }; if (requiresApproval) { this.pendingOrders.unshift(order); // Show real-time notification this.showNotificationBanner({ type: 'info', message: `New PO #${purchaseOrderNumber} awaiting approval`, action: () => this.viewOrder(purchaseOrderNumber) }); // Play notification sound this.playNotificationSound(); // Update pending count badge this.updatePendingBadge(this.pendingOrders.length); } else { this.completedOrders.unshift(order); } // Refresh dashboard display this.render(); } handleOrderUpdate(payload) { const updatedOrder = payload.data; // Remove from pending if approved/rejected if (['approved', 'rejected', 'canceled'].includes(updatedOrder.status)) { this.pendingOrders = this.pendingOrders.filter( o => o.number !== updatedOrder.number ); this.completedOrders.unshift(updatedOrder); this.updatePendingBadge(this.pendingOrders.length); } this.render(); } render() { document.querySelector('#pending-orders').innerHTML = this.renderOrderList(this.pendingOrders, 'pending'); document.querySelector('#completed-orders').innerHTML = this.renderOrderList(this.completedOrders, 'completed'); } renderOrderList(orders, type) { if (orders.length === 0) { return `No ${type} purchase orders`; } return orders.map(order => ` #${order.number} ${order.status} Total: ${order.total} Date: ${formatDate(order.placedAt)} `).join(''); } showNotificationBanner(options) { // Show slide-in notification const banner = document.createElement('div'); banner.className = `notification-banner notification-${options.type}`; banner.innerHTML = ` ${options.message} `; if (options.action) { banner.onclick = options.action; } document.body.appendChild(banner); setTimeout(() => banner.remove(), 5000); } playNotificationSound() { const audio = new Audio('/sounds/notification.mp3'); audio.play().catch(() => {}); } updatePendingBadge(count) { const badge = document.querySelector('.pending-count-badge'); if (badge) { badge.textContent = count; badge.style.display = count > 0 ? 'block' : 'none'; } } viewOrder(orderNumber) { window.location.href = `/purchase-orders/${orderNumber}`; } async loadExistingOrders() { // Load existing orders from API const { pendingOrders, completedOrders } = await fetchPurchaseOrders(); this.pendingOrders = pendingOrders; this.completedOrders = completedOrders; } } // Initialize dashboard const dashboard = new PurchaseOrderDashboard(); ``` #### Usage scenarios - Display success confirmation with order details. - Redirect to the success page (approval or direct order). - Clear shopping cart after successful placement. - Send email notifications to approvers. - Track analytics and conversion events. - Update purchase order history and counts. - Show real-time notifications for new orders. - Update dashboard widgets and badges. - Trigger approval workflow notifications. - Log purchase order creation for audit. - Update budget tracking systems. - Sync with ERP/accounting systems. --- ### `purchase-order/refresh` (emits and listens) Emitted and consumed for internal and external communication. #### Event payload ```typescript Boolean ``` #### When triggered - After approval rule changes - After permission updates - On user request (manual refresh) - After significant state changes #### Example: Example ```js // Emit refresh event events.emit('purchase-order/refresh', { purchaseOrderId: 'PO123456' }); // Listen for refresh requests events.on('purchase-order/refresh', async (payload) => { if (payload.data.purchaseOrderId) { // Refresh specific purchase order await refreshPurchaseOrder(payload.data.purchaseOrderId); } else { // Refresh all purchase orders await refreshAllPurchaseOrders(); } }); ``` #### Usage scenarios - Force reload after external updates. - Implement pull-to-refresh functionality. - Sync data after background changes. - Refresh after approval rule modifications. --- ## Listening to events All Purchase Order events are emitted through the centralized event bus. Subscribe to events using the `events.on()` method: ```js // Listen to purchase order lifecycle events events.on('purchase-order/placed', handlePurchaseOrderPlaced); events.on('purchase-order/data', handlePurchaseOrderData); events.on('purchase-order/refresh', handleRefreshRequest); // Remove listeners when done events.off('purchase-order/placed', handlePurchaseOrderPlaced); ``` > Event listeners remain active until explicitly removed with `events.off()`. Clean up listeners when components unmount to prevent memory leaks. > Purchase order events integrate with the standard Order drop-in events. A purchase order that completes the approval process will emit both `purchase-order/data` and `order/data` events. ## Related documentation - [Functions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/functions/) - API functions that emit these events - [Containers](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/containers/) - UI components that respond to events - [Event bus documentation](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/events/) - Learn more about the event system {/* This documentation is manually curated based on: https://github.com/adobe-commerce/storefront-purchase-order */} ## Data Models The following data models are used in event payloads for this drop-in. ### PurchaseOrderModel Used in: [`order/data`](#orderdata-emits), [`purchase-order/data`](#purchase-orderdata-emits-and-listens), [`purchase-order/placed`](#purchase-orderplaced-emits). ```ts interface PurchaseOrderModel { typename: string; uid: string; number: string; status: string; availableActions: string[]; approvalFlow: | { ruleName: string; events: Array<{ message: string; name: string; role: string; status: string; updatedAt: string; }>; }[] | []; comments?: Array<{ uid: string; createdAt: string; author: { firstname: string; lastname: string; email: string; }; text: string; }>; createdAt: string; updatedAt: string; createdBy: { firstname: string; lastname: string; email: string; }; historyLog?: Array<{ activity: string; createdAt: string; message: string; uid: string; }>; quote: QuoteProps | null; order: { orderNumber: string; id: string; }; } ``` --- # Purchase Order Functions The Purchase Order drop-in provides functions for managing the complete purchase order workflow, including adding items to cart, approving, rejecting, canceling, and tracking purchase order status. Version: 1.2.0 | Function | Description | | --- | --- | | [`addPurchaseOrderComment`](#addpurchaseordercomment) | Adds a comment to a purchase order. | | [`addPurchaseOrderItemsToCart`](#addpurchaseorderitemstocart) | Adds purchase order items to a cart. | | [`approvePurchaseOrders`](#approvepurchaseorders) | Approves one or more purchase orders. | | [`cancelPurchaseOrders`](#cancelpurchaseorders) | Cancels one or more purchase orders. | | [`createPurchaseOrderApprovalRule`](#createpurchaseorderapprovalrule) | Creates a new purchase order approval rule. | | [`currencyInfo`](#currencyinfo) | Fetches currency information including the base currency code and available currency codes from the GraphQL API. | | [`deletePurchaseOrderApprovalRule`](#deletepurchaseorderapprovalrule) | Deletes one or more purchase order approval rules. | | [`getPurchaseOrder`](#getpurchaseorder) | Gets a single purchase order by UID. | | [`getPurchaseOrderApprovalRule`](#getpurchaseorderapprovalrule) | Retrieves a specific purchase order approval rule by its unique identifier. | | [`getPurchaseOrderApprovalRuleMetadata`](#getpurchaseorderapprovalrulemetadata) | Gets the current user's purchase order approval rule metadata. | | [`getPurchaseOrderApprovalRules`](#getpurchaseorderapprovalrules) | Gets the current user's purchase order approval rules with pagination support. | | [`getPurchaseOrders`](#getpurchaseorders) | Gets a list of purchase orders with optional filtering and pagination. | | [`placeOrderForPurchaseOrder`](#placeorderforpurchaseorder) | Places an order from an approved purchase order. | | [`placePurchaseOrder`](#placepurchaseorder) | Places a purchase order from a cart. | | [`rejectPurchaseOrders`](#rejectpurchaseorders) | Rejects one or more purchase orders. | | [`updatePurchaseOrderApprovalRule`](#updatepurchaseorderapprovalrule) | Updates an existing purchase order approval rule. | | [`validatePurchaseOrders`](#validatepurchaseorders) | Validates one or more purchase orders. | ## addPurchaseOrderComment Adds a comment to a purchase order. ```ts const addPurchaseOrderComment = async ( uid: string, comment: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `uid` | `string` | Yes | The unique identifier for the purchase order to which the comment will be added. | | `comment` | `string` | Yes | The text content of the comment to add to the purchase order. Use this to provide context, approval notes, or communication between team members reviewing the purchase order. | ### Events Does not emit any drop-in events. ### Returns Returns [`PurchaseOrderCommentModel`](#purchaseordercommentmodel). ## addPurchaseOrderItemsToCart Adds purchase order items to a cart. ```ts const addPurchaseOrderItemsToCart = async ( purchaseOrderUid: string, cartId: string, replaceExistingCartItems: boolean = false ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `purchaseOrderUid` | `string` | Yes | The unique identifier for the purchase order containing the items to add to the cart. | | `cartId` | `string` | Yes | The unique identifier for the shopping cart. This ID is used to track and persist cart data across sessions. | | `replaceExistingCartItems` | `boolean` | No | A boolean flag controlling cart merge behavior. When `true`, replaces all existing cart items with the purchase order items. When `false` (default), appends the purchase order items to existing cart contents. | ### Events Does not emit any drop-in events. ### Returns Returns [`CartModel`](#cartmodel). ## approvePurchaseOrders Approves one or more purchase orders. ```ts const approvePurchaseOrders = async ( uids: string | string[] ): Promise<{ errors: { message: string; type: string }[]; purchaseOrders: PurchaseOrderModel[]; }> ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `uids` | `string \| string[]` | Yes | One or more purchase order unique identifiers to approve. Can be a single UID string or an array of UIDs for batch approval operations. | ### Events Does not emit any drop-in events. ### Returns ```ts Promise<{ errors: { message: string; type: string }[]; purchaseOrders: PurchaseOrderModel[]; }> ``` See [`PurchaseOrderModel`](#purchaseordermodel). ## cancelPurchaseOrders Cancels one or more purchase orders. ```ts const cancelPurchaseOrders = async ( uids: string | string[] ): Promise<{ errors: { message: string; type: string }[]; purchaseOrders: PurchaseOrderModel[]; }> ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `uids` | `string \| string[]` | Yes | One or more purchase order unique identifiers to cancel. Can be a single UID string or an array of UIDs for batch cancellation operations. | ### Events Does not emit any drop-in events. ### Returns ```ts Promise<{ errors: { message: string; type: string }[]; purchaseOrders: PurchaseOrderModel[]; }> ``` See [`PurchaseOrderModel`](#purchaseordermodel). ## createPurchaseOrderApprovalRule Creates a new purchase order approval rule. ```ts const createPurchaseOrderApprovalRule = async ( input: any ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | `any` | Yes | Input parameters for the operation. | ### Events Does not emit any drop-in events. ### Returns Returns [`PurchaseOrderApprovalRuleModel`](#purchaseorderapprovalrulemodel). ## currencyInfo The `currencyInfo` function fetches currency information, including the base currency code and available currency codes, from the `GraphQL` API. ```ts const currencyInfo = async (): Promise<{ baseCurrencyCode: string; availableCurrencyCodes: { text: string; value: string }[]; }> ``` ### Events Does not emit any drop-in events. ### Returns ```ts Promise<{ baseCurrencyCode: string; availableCurrencyCodes: { text: string; value: string }[]; }> ``` ## deletePurchaseOrderApprovalRule Deletes one or more purchase order approval rules. ```ts const deletePurchaseOrderApprovalRule = async ( uids: string | string[] ): Promise<{ deletePurchaseOrderApprovalRule: { errors: { message?: string; type?: string }[]; }; }> ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `uids` | `string \| string[]` | Yes | One or more approval rule unique identifiers to delete. Can be a single UID string or an array of UIDs for batch deletion. This permanently removes the approval rules from the purchase order workflow. | ### Events Does not emit any drop-in events. ### Returns ```ts Promise<{ deletePurchaseOrderApprovalRule: { errors: { message?: string; type?: string }[]; }; }> ``` ## getPurchaseOrder Gets a single purchase order by UID. ```ts const getPurchaseOrder = async ( uid: string ): Promise<{ purchaseOrder: PurchaseOrderModel; }> ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `uid` | `string` | Yes | The unique identifier for the purchase order to retrieve. Returns complete purchase order details including items, status, history, comments, and approval information. | ### Events Does not emit any drop-in events. ### Returns ```ts Promise<{ purchaseOrder: PurchaseOrderModel; }> ``` See [`PurchaseOrderModel`](#purchaseordermodel). ## getPurchaseOrderApprovalRule Retrieves a specific purchase order approval rule by its unique identifier. This function fetches detailed information about an approval rule including its configuration, applicable roles, approval conditions, and approvers. ```ts const getPurchaseOrderApprovalRule = async ( id: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `id` | `string` | Yes | See function signature above | ### Events Does not emit any drop-in events. ### Returns Returns [`PurchaseOrderApprovalRuleModel`](#purchaseorderapprovalrulemodel). ## getPurchaseOrderApprovalRuleMetadata Gets the current user's purchase order approval rule metadata. ```ts const getPurchaseOrderApprovalRuleMetadata = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns [`PurchaseOrderApprovalRuleMetadataModel`](#purchaseorderapprovalrulemetadatamodel). ## getPurchaseOrderApprovalRules Gets the current user's purchase order approval rules with pagination support. ```ts const getPurchaseOrderApprovalRules = async ( currentPage: number = DEFAULT_PAGE_INFO.currentPage, pageSize: number = DEFAULT_PAGE_INFO.pageSize ): Promise<{ totalCount: number; pageInfo: { currentPage: number; pageSize: number; totalPages: number; }; items: PurchaseOrderApprovalRuleModel[]; }> ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `currentPage` | `number` | No | The page number for pagination (1-indexed). Used to navigate through multiple pages of approval rules. | | `pageSize` | `number` | No | The number of approval rules to return per page. Controls how many rules appear on each page of results. | ### Events Does not emit any drop-in events. ### Returns ```ts Promise<{ totalCount: number; pageInfo: { currentPage: number; pageSize: number; totalPages: number; }; items: PurchaseOrderApprovalRuleModel[]; }> ``` See [`PurchaseOrderApprovalRuleModel`](#purchaseorderapprovalrulemodel). ## getPurchaseOrders Gets a list of purchase orders with optional filtering and pagination. ```ts const getPurchaseOrders = async ( filter?: any, pageSize: number = 20, currentPage: number = 1 ): Promise<{ totalCount: number; pageInfo: { currentPage: number; pageSize: number; totalPages: number; }; purchaseOrderItems: PurchaseOrderModel[]; }> ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `filter` | `any` | No | See function signature above | | `pageSize` | `number` | No | The number of purchase orders to return per page. Controls how many orders appear on each page of results. | | `currentPage` | `number` | No | The page number for pagination (1-indexed). Used to navigate through multiple pages of purchase orders. | ### Events Does not emit any drop-in events. ### Returns ```ts Promise<{ totalCount: number; pageInfo: { currentPage: number; pageSize: number; totalPages: number; }; purchaseOrderItems: PurchaseOrderModel[]; }> ``` See [`PurchaseOrderModel`](#purchaseordermodel). ## placeOrderForPurchaseOrder Places an order from an approved purchase order. ```ts const placeOrderForPurchaseOrder = async ( purchaseOrderUid: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `purchaseOrderUid` | `string` | Yes | See function signature above | ### Events Does not emit any drop-in events. ### Returns Returns [`CustomerOrderModel`](#customerordermodel). ## placePurchaseOrder Places a purchase order from a cart. ```ts const placePurchaseOrder = async ( cartId: string ): Promise<{ purchaseOrder: PurchaseOrderModel }> ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `cartId` | `string` | Yes | The unique identifier for the shopping cart. This ID is used to track and persist cart data across sessions. | ### Events Emits the [`purchase-order/placed`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/events/#purchase-orderplaced-emits) event. ### Returns Returns `{ purchaseOrder: PurchaseOrderModel }`. See [`PurchaseOrderModel`](#purchaseordermodel). ## rejectPurchaseOrders Rejects one or more purchase orders. ```ts const rejectPurchaseOrders = async ( uids: string | string[] ): Promise<{ errors: { message: string; type: string }[]; purchaseOrders: PurchaseOrderModel[]; }> ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `uids` | `string \| string[]` | Yes | One or more purchase order unique identifiers to reject. Can be a single UID string or an array of UIDs for batch rejection operations. | ### Events Does not emit any drop-in events. ### Returns ```ts Promise<{ errors: { message: string; type: string }[]; purchaseOrders: PurchaseOrderModel[]; }> ``` See [`PurchaseOrderModel`](#purchaseordermodel). ## updatePurchaseOrderApprovalRule Updates an existing purchase order approval rule. ```ts const updatePurchaseOrderApprovalRule = async ( input: any ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | `any` | Yes | Input parameters for the operation. | ### Events Does not emit any drop-in events. ### Returns Returns [`PurchaseOrderApprovalRuleModel`](#purchaseorderapprovalrulemodel). ## validatePurchaseOrders Validates one or more purchase orders. ```ts const validatePurchaseOrders = async ( uids: string | string[] ): Promise<{ errors: { message: string; type: string }[]; purchaseOrders: PurchaseOrderModel[]; }> ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `uids` | `string \| string[]` | Yes | One or more purchase order unique identifiers to validate. Checks whether the purchase orders exist, are in a valid state, and can be processed for the requested operation. | ### Events Does not emit any drop-in events. ### Returns ```ts Promise<{ errors: { message: string; type: string }[]; purchaseOrders: PurchaseOrderModel[]; }> ``` See [`PurchaseOrderModel`](#purchaseordermodel). ## Data Models The following data models are used by functions in this drop-in. ### CartModel The `CartModel` object is returned by the following functions: [`addPurchaseOrderItemsToCart`](#addpurchaseorderitemstocart). ```ts interface CartModel { cart: { id: string; items: { uid: string; quantity: number; product: { uid: string; name: string; sku: string; }; }[]; pagination?: { currentPage: number; pageSize: number; totalPages: number; totalCount: number; }; }; userErrors: Array<{ message: string; }>; } ``` ### CustomerOrderModel The `CustomerOrderModel` object is returned by the following functions: [`placeOrderForPurchaseOrder`](#placeorderforpurchaseorder). ```ts interface CustomerOrderModel { appliedCoupons: Coupon[]; appliedGiftCards: GiftCard[]; availableActions: string[]; billingAddress: CustomerAddress; carrier: string; comments: string[]; creditMemos: any[]; customerInfo: CustomerInfo; email: string; giftMessage: string; giftReceiptIncluded: boolean; giftWrapping: any; id: string; invoices: any[]; isVirtual: boolean; items: OrderItem[]; itemsEligibleForReturn: any[]; number: string; orderDate: string; orderStatusChangeDate: string; paymentMethods: PaymentMethod[]; printedCardIncluded: boolean; returns: any; shipments: Shipment[]; shippingAddress: CustomerAddress; shippingMethod: string; status: string; token: string; total: OrderTotal; } ``` ### PurchaseOrderApprovalRuleMetadataModel The `PurchaseOrderApprovalRuleMetadataModel` object is returned by the following functions: [`getPurchaseOrderApprovalRuleMetadata`](#getpurchaseorderapprovalrulemetadata). ```ts interface PurchaseOrderApprovalRuleMetadataModel { availableAppliesTo: CompanyRole[]; availableRequiresApprovalFrom: CompanyRole[]; } ``` ### PurchaseOrderApprovalRuleModel The `PurchaseOrderApprovalRuleModel` object is returned by the following functions: [`createPurchaseOrderApprovalRule`](#createpurchaseorderapprovalrule), [`getPurchaseOrderApprovalRule`](#getpurchaseorderapprovalrule), [`getPurchaseOrderApprovalRules`](#getpurchaseorderapprovalrules), [`updatePurchaseOrderApprovalRule`](#updatepurchaseorderapprovalrule). ```ts interface PurchaseOrderApprovalRuleModel { createdAt: string; createdBy: string; description: string; updatedAt: string; name: string; status: string; uid: string; appliesToRoles: { id: string; name: string; usersCount: number; permissions: Array<{ id: string; sortOrder: number; text: string; }>; }[]; condition: { attribute: string; operator: string; quantity: number; amount: { currency: string; value: number; }; }; approverRoles: { id: string; name: string; usersCount: number; permissions: Array<{ id: string; sortOrder: number; text: string; }>; }[]; } ``` ### PurchaseOrderCommentModel The `PurchaseOrderCommentModel` object is returned by the following functions: [`addPurchaseOrderComment`](#addpurchaseordercomment). ```ts interface PurchaseOrderCommentModel { createdAt: string; text: string; uid: string; author: { allowRemoteShoppingAssistance: boolean; confirmationStatus: string; createdAt: string; dateOfBirth: string; email: string; firstname: string; gender: number; jobTitle: string; lastname: string; middlename: string; prefix: string; status: string; structureId: string; suffix: string; telephone: string; }; } ``` ### PurchaseOrderModel The `PurchaseOrderModel` object is returned by the following functions: [`approvePurchaseOrders`](#approvepurchaseorders), [`cancelPurchaseOrders`](#cancelpurchaseorders), [`getPurchaseOrder`](#getpurchaseorder), [`getPurchaseOrders`](#getpurchaseorders), [`placePurchaseOrder`](#placepurchaseorder), [`rejectPurchaseOrders`](#rejectpurchaseorders), [`validatePurchaseOrders`](#validatepurchaseorders). ```ts interface PurchaseOrderModel { typename: string; uid: string; number: string; status: string; availableActions: string[]; approvalFlow: | { ruleName: string; events: Array<{ message: string; name: string; role: string; status: string; updatedAt: string; }>; }[] | []; comments?: Array<{ uid: string; createdAt: string; author: { firstname: string; lastname: string; email: string; }; text: string; }>; createdAt: string; updatedAt: string; createdBy: { firstname: string; lastname: string; email: string; }; historyLog?: Array<{ activity: string; createdAt: string; message: string; uid: string; }>; quote: QuoteProps | null; order: { orderNumber: string; id: string; }; } ``` {/* This documentation is auto-generated from the drop-in source repository: REPO_URL */} --- # Purchase Order overview The Purchase Order drop-in enables purchase order creation and purchase order approval rules for Adobe Commerce storefronts. It also supports approval workflows, comments, and history. ## Supported Commerce features The following table provides an overview of the Adobe Commerce features that the Purchase Order drop-in supports: | Feature | Status | | ------- | ------ | | Purchase order creation | Supported | | Purchase order approval rules | Supported | | Purchase order approval workflows | Supported | | Purchase order comments and history | Supported | | Purchase order list views | Supported | | Purchase order details view | Supported | | Conditional checkout logic | Supported | | Company and subordinate views | Supported | | Bulk approve/reject actions | Supported | | Convert purchase order to order | Supported | | ACL permission-based access control | Supported | | GraphQL API integration | Supported | ## Key events The Purchase Order drop-in exposes the following key events through the boilerplate: ### purchase-order/data Emitted by the purchase order initializer. Requires passing a `poRef` (Purchase Order UID) to the initializer. The event contains the full purchase order payload used by the purchase order details container. After loading and transforming the purchase order data, it also emits an `order/data` event, which is required to initialize the following Order drop-in containers (used on the purchase order details page): - **CustomerDetails** - **OrderCostSummary** - **OrderProductList** ### purchase-order/refresh Should be emitted when all purchase order containers need to refresh their data (for example, when the company context changes). ## Section topics The topics in this section will help you understand how to customize and use the Purchase Order drop-in effectively within your storefront. ### Quick Start Provides quick reference information and a getting started guide for the Purchase Order drop-in. This topic covers package details, import paths, and basic usage examples to help you integrate Purchase Order functionality into your site. ### Initialization Explains how to initialize the Purchase Order drop-in with configuration options including language definitions for internationalization, custom data models for type transformations, and the `poRef` parameter for loading specific purchase order details. The initializer emits key events (`purchase-order/data` and `order/data`) that are used by containers to display purchase order information. ### Containers Describes the 12 UI containers including: approval rule management (`ApprovalRuleDetails`, `ApprovalRuleForm`, `ApprovalRulesList`), purchase order lists (`CompanyPurchaseOrders`, `CustomerPurchaseOrders`, `RequireApprovalPurchaseOrders`), and purchase order details components (`PurchaseOrderStatus`, `PurchaseOrderApprovalFlow`, `PurchaseOrderCommentForm`, `PurchaseOrderCommentsList`, `PurchaseOrderHistoryLog`, `PurchaseOrderConfirmation`). Each container is optimized for specific user roles and workflows based on ACL permissions. ### Functions Documents the 17 API functions for managing purchase orders including creating and placing purchase orders, managing approval rules (create, update, delete, retrieve), performing purchase order actions (approve, reject, cancel, validate), adding comments, converting purchase orders to orders, and retrieving purchase order data with filtering and pagination support. ### Events Explains the events emitted during the purchase order lifecycle: `purchase-order/data` (emitted by the initializer with the full purchase order payload), `purchase-order/refresh` (triggers a data refresh across all containers), and `purchase-order/placed` (emitted when a new purchase order is created from a cart). The `purchase-order/data` event also triggers an `order/data` event to initialize Order drop-in containers on the details page. ### Slots Describes the customization slots available for extending UI functionality, including the `PurchaseOrderActions` slot in the `PurchaseOrderStatus` container for customizing action buttons (approve, reject, cancel, place order) or adding additional custom actions with business logic. ### Dictionary Explains the 251 internationalization keys for translating purchase order UI text including approval rule labels, purchase order status messages, form field labels, action button text, validation errors, and empty state messages. Supports full localization for multi-language B2B storefronts. ### Styles Describes how to customize the appearance of purchase order containers including approval rule forms and lists, purchase order tables, status badges, action buttons, comment forms, history logs, and approval flow displays using CSS variables and design tokens. Covers styling for all 12 container components with detailed CSS class references. --- # Purchase Order initialization The **Purchase Order initializer** configures the drop-in for managing purchase order workflows, approval rules, and order tracking. Use initialization to set the purchase order reference, customize data models, and enable internationalization for multi-language B2B storefronts. Version: 1.2.1 ## Configuration options The following table describes the configuration options available for the **Purchase Order** initializer: | Parameter | Type | Req? | Description | |---|---|---|---| | `langDefinitions` | [`LangDefinitions`](#langdefinitions) | No | Language definitions for internationalization (i18n). Override dictionary keys for localization or branding. | | `poRef` | `string` | No | Purchase order reference identifier used to load and display a specific purchase order. Pass this to initialize the drop-in with purchase order details on page load. | ## Default configuration The initializer runs with these defaults when no configuration is provided: ```javascript title="scripts/initializers/purchase-order.js" // All configuration options are optional await initializers.mountImmediately(initialize, { langDefinitions: {}, // Uses built-in English strings models: {}, // Uses default data models // Drop-in-specific defaults: // poRef: undefined // See configuration options below }); ``` ## Language definitions Override dictionary keys for localization or branding. The `langDefinitions` object maps locale keys to custom strings that override default text for the drop-in. ```javascript title="scripts/initializers/purchase-order.js" const customStrings = { 'AddToCart': 'Add to Bag', 'Checkout': 'Complete Purchase', 'Price': 'Cost', }; const langDefinitions = { default: customStrings, }; await initializers.mountImmediately(initialize, { langDefinitions }); ``` > For complete dictionary customization including all available keys and multi-language support, see the [Purchase Order Dictionary](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/dictionary/) page. ## Customizing data models Extend or transform data models by providing custom transformer functions. Use the `models` option to add custom fields or modify existing data structures returned from the backend. ### Available models The following models can be customized through the `models` configuration option: | Model | Description | |---|---| | [`PurchaseOrderModel`](#purchaseordermodel) | Transforms purchase order data from `GraphQL` including order details, approval status, items, totals, and history. Use this to add custom fields or modify existing purchase order data structures. | The following example shows how to customize the `PurchaseOrderModel` model for the **Purchase Order** drop-in: ```javascript title="scripts/initializers/purchase-order.js" const models = { PurchaseOrderModel: { transformer: (data) => ({ // Add approval status badge text approvalStatusDisplay: data?.status === 'PENDING' ? 'Awaiting Approval' : data?.status === 'APPROVED' ? 'Approved - Ready to Order' : data?.status, // Add formatted approval flow summary approvalSummary: data?.approvalFlow?.map(rule => `${rule.ruleName}: ${rule.events?.length || 0} events` ).join(', '), // Add created by full name createdByName: data?.createdBy ? `${data.createdBy.firstname} ${data.createdBy.lastname}` : null, }), }, }; await initializers.mountImmediately(initialize, { models }); ``` ## Drop-in configuration The **Purchase Order initializer** configures the drop-in for managing purchase order workflows, approval rules, and order tracking. Use initialization to set the purchase order reference, customize data models, and enable internationalization for multi-language B2B storefronts. ```javascript title="scripts/initializers/purchase-order.js" await initializers.mountImmediately(initialize, { langDefinitions: {}, poRef: 'abc123', }); ``` > Refer to the [Configuration options](#configuration-options) table for detailed descriptions of each option. ## Configuration types The following TypeScript definitions show the structure of each configuration object: ### langDefinitions Maps locale identifiers to dictionaries of key-value pairs. The `default` locale is used as the fallback when no specific locale matches. Each dictionary key corresponds to a text string used in the drop-in UI. ```typescript langDefinitions?: { [locale: string]: { [key: string]: string; }; }; ``` ## Model definitions The following TypeScript definitions show the structure of each customizable model: ### PurchaseOrderModel ```typescript interface PurchaseOrderModel { typename: string; uid: string; number: string; status: string; availableActions: string[]; approvalFlow: | { ruleName: string; events: Array<{ message: string; name: string; role: string; status: string; updatedAt: string; }>; }[] | []; comments?: Array<{ uid: string; createdAt: string; author: { firstname: string; lastname: string; email: string; }; text: string; }>; createdAt: string; updatedAt: string; createdBy: { firstname: string; lastname: string; email: string; }; historyLog?: Array<{ activity: string; createdAt: string; message: string; uid: string; }>; quote: QuoteProps | null; order: { orderNumber: string; id: string; }; } ``` --- # Purchase Order Quick Start Get started with the Purchase Order drop-in to enable purchase order approval workflows in your B2B storefront. Version: 1.2.0 ## Quick example The Purchase Order drop-in is included in the https://github.com/hlxsites/aem-boilerplate-commerce. This example shows the basic pattern: ```js // 1. Import initializer (handles all setup) // 2. Import the container you need // 3. Import the provider // 4. Render in your block export default async function decorate(block) { await provider.render(ApprovalRuleDetails, { // Configuration options - see Containers page })(block); } ``` **New to drop-ins?** See the [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/) guide for complete step-by-step instructions. ## Quick reference **Import paths:** - Initializer: `import '../../scripts/initializers/purchase-order.js'` - Containers: `import ContainerName from '@dropins/storefront-purchase-order/containers/ContainerName.js'` - Provider: `import { render } from '@dropins/storefront-purchase-order/render.js'` **Package:** `@dropins/storefront-purchase-order` **Version:** 1.2.0 (verify compatibility with your Commerce instance) **Example container:** `ApprovalRuleDetails` ## Learn more - [Containers](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/containers/) - Available UI components and configuration options - [Initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/initialization/) - Customize initializer settings and data models - [Functions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/functions/) - Control drop-in behavior programmatically - [Events](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/events/) - Listen to and respond to drop-in state changes - [Slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/slots/) - Extend containers with custom content --- # Purchase Order Slots The Purchase Order drop-in exposes slots for customizing specific UI sections. Use slots to replace or extend container components. For default properties available to all slots, see [Extending drop-in components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/extending/). Version: 1.2.0 | Container | Slots | |-----------|-------| | [`PurchaseOrderStatus`](#purchaseorderstatus-slots) | `PurchaseOrderActions` | ## PurchaseOrderStatus slots The slots for the `PurchaseOrderStatus` container allow you to customize its appearance and behavior. ```typescript interface PurchaseOrderStatusProps { slots?: { PurchaseOrderActions: SlotProps; }; } ``` ### PurchaseOrderActions slot Customizes the action buttons displayed in the `PurchaseOrderStatus` container. Use this slot to override default action buttons (Approve, Reject, Cancel, Place Order) or add custom actions with custom business logic. #### Context properties The slot receives the following context properties: - **`loading`** - Loading flag indicating whether purchase order data is being initialized. - **`availableActions`** - List of available purchase order actions returned by the backend. Actions are filtered based on the current purchase order status and user permissions. - **`handleApprove`** - Callback function to approve the purchase order. Triggers the approve purchase order GraphQL mutation. - **`handleReject`** - Callback function to reject the purchase order. Triggers the reject purchase order GraphQL mutation. - **`handleCancel`** - Callback function to cancel the purchase order. Triggers the cancel purchase order GraphQL mutation. - **`handlePlaceOrder`** - Callback function to place an order for the purchase order. Triggers the place order GraphQL mutation. #### Usage scenarios - Override default action buttons with custom styling or layout - Add additional custom actions beyond the standard approve/reject/cancel/place order - Conditionally render actions based on custom business rules - Integrate third-party approval workflows or external systems #### Example ```js await provider.render(PurchaseOrderStatus, { slots: { PurchaseOrderActions: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom PurchaseOrderActions'; ctx.appendChild(element); } } })(block); ``` --- # Purchase Order styles Customize the Purchase Order drop-in using CSS classes and design tokens. This page covers the Purchase Order-specific container classes and customization examples. For comprehensive information about design tokens, responsive breakpoints, and styling best practices, see [Styling Drop-In Components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/). Version: 1.2.0 ## Customization example Add this to the CSS file of the specific https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/ where you're using the Purchase Order drop-in. For a complete list of available design tokens (colors, spacing, typography, and more), see the [Design tokens reference](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/#design-tokens-reference). ```css title="styles/styles.css" del={2-2} ins={3-3} .purchase-orders-confirmation-content__title { color: var(--color-neutral-800); color: var(--color-brand-800); } ``` ## Container classes The Purchase Order drop-in uses BEM-style class naming. Use the browser DevTools to inspect elements and find specific class names. ```css /* ApprovalRuleDetailsContent */ .approval-rule-details__button {} .b2b-purchase-order-approval-rule-details-content {} .b2b-purchase-order-approval-rule-details-content__item {} .b2b-purchase-order-approval-rule-details-content__label {} .b2b-purchase-order-approval-rule-details-content__value {} /* ApprovalRuleForm */ .approval-rule-form-loader__buttons {} .approval-rule-form-loader__section {} .b2b-purchase-order-approval-rule-form {} .b2b-purchase-order-approval-rule-form__applies-to {} .b2b-purchase-order-approval-rule-form__approval-role {} .b2b-purchase-order-approval-rule-form__buttons {} .b2b-purchase-order-approval-rule-form__rule-condition {} .b2b-purchase-order-approval-rule-form__rule-condition-container {} .b2b-purchase-order-approval-rule-form__rule-condition-container--error {} .b2b-purchase-order-approval-rule-form__rule-type {} .dropin-checkbox {} .dropin-in-line-alert {} .dropin-multi-select {} .dropin-skeleton {} .error-message {} /* FormLoader */ .approval-rule-form-loader__buttons {} .approval-rule-form-loader__section {} .b2b-purchase-order-form-loader {} .dropin-skeleton {} /* PurchaseOrderApprovalFlowContent */ .b2b-purchase-order-approval-flow-content__description {} .b2b-purchase-order-approval-flow-content__divider {} .b2b-purchase-order-approval-flow-content__icon--approved {} .b2b-purchase-order-approval-flow-content__icon--pending {} .b2b-purchase-order-approval-flow-content__icon--rejected {} .b2b-purchase-order-approval-flow-content__item {} .b2b-purchase-order-approval-flow-content__list {} .b2b-purchase-order-approval-flow-content__title {} /* PurchaseOrderCommentFormContent */ .b2b-purchase-order-comment-form-content {} .dropin-textarea {} .dropin-textarea__label--floating {} /* PurchaseOrderCommentsListContent */ .b2b-purchase-order-comment-list-content__actions {} .b2b-purchase-order-comment-list-content__description {} .b2b-purchase-order-comment-list-content__divider {} .b2b-purchase-order-comment-list-content__item {} .b2b-purchase-order-comment-list-content__list {} .b2b-purchase-order-comment-list-content__title {} /* PurchaseOrderConfirmationContent */ .purchase-orders-confirmation-content__link {} .purchase-orders-confirmation-content__message {} .purchase-orders-confirmation-content__title {} /* PurchaseOrderHistoryLogContent */ .b2b-purchase-order-history-log-content__actions {} .b2b-purchase-order-history-log-content__description {} .b2b-purchase-order-history-log-content__divider {} .b2b-purchase-order-history-log-content__item {} .b2b-purchase-order-history-log-content__list {} .b2b-purchase-order-history-log-content__title {} /* PurchaseOrderStatusContent */ .b2b-purchase-order-status-content__actions {} .b2b-purchase-order-status-content__message {} .dropin-in-line-alert__description {} .purchase-order-status {} /* PurchaseOrdersHeader */ .dropin-divider {} .purchase-orders-header {} .purchase-orders-header--with-divider {} /* PurchaseOrdersTable */ .b2b-purchase-order-purchase-orders-table {} .b2b-purchase-order-purchase-orders-table--empty-state {} .b2b-purchase-order-purchase-orders-table__row-details {} .b2b-purchase-order-purchase-orders-table__row-details-action-inner-wrapper {} .b2b-purchase-order-purchase-orders-table__row-details-content {} .b2b-purchase-order-purchase-orders-table__status {} .b2b-purchase-order-purchase-orders-table__status--negative {} .b2b-purchase-order-purchase-orders-table__status--positive {} .b2b-purchase-order-purchase-orders-table__status--waiting {} .dropin-action-button {} .dropin-card__content {} .dropin-table__body {} .dropin-table__body__cell {} .dropin-table__header__row {} .purchase-orders-table__empty-state {} .purchase-orders-table__header {} .purchase-orders-table__item--skeleton {} .purchase-orders-table__pagination {} .purchase-orders-table__pagination--loading {} .purchase-orders-table__pagination-counter {} .purchase-orders-table__pagination-page-size {} .purchase-orders-table__pagination-wrapper {} /* PurchaseOrdersTableActions */ .b2b-purchase-order-purchase-orders-table-actions {} .b2b-purchase-order-purchase-orders-table-actions__buttons {} ``` For the source CSS files, see the https://github.com/adobe-commerce/storefront-purchase-order/tree/main/src. --- # Quick Order Containers The Quick Order B2B drop-in provides four container components: three for the Quick Order page (bulk ordering by SKU, search, or CSV) and one for Grid Ordering on the product detail page (PDP). ## What are Containers? Containers are pre-built UI components that combine functionality, state management, and presentation. They communicate exclusively via the event bus (for example, `quick-order/add-items`, `quick-order/loading`). SKUs added in `QuickOrderCsvUpload`, `QuickOrderMultipleSku`, or via the search within `QuickOrderItems` appear in the `QuickOrderItems` list. Because communication is event-driven, each container remains independent. Any container can be replaced with a custom implementation that follows the defined event contracts and payload structure. All Quick Order containers support extensibility through [Slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/slots/). ## Available Containers | Container | Description | | --------- | ----------- | | [QuickOrderCsvUpload](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/containers/quick-order-csv-upload/) | CSV file upload with required "SKU" and "QTY" columns (max 200 rows). Validates file, parses data, and emits `quick-order/add-items`. Provides sample CSV download. | | [QuickOrderItems](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/containers/quick-order-items/) | Product list with search, quantity editing, product options for configurables, validation, and "Add All to Cart". Listens to `quick-order/add-items` and coordinates with the other two containers. | | [QuickOrderMultipleSku](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/containers/quick-order-multiple-sku/) | Text area for entering multiple SKUs (comma, space, or newline separated). Parses and deduplicates SKUs, then emits `quick-order/add-items` to add them to the list. | | [QuickOrderVariantsGrid](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/containers/quick-order-variants-grid/) | Grid interface on the product detail page for configurable products. Displays variants with quantity inputs and bulk add-to-cart. Used when Grid Ordering is enabled. | ## Quick Order The Quick Order page combines [QuickOrderCsvUpload](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/containers/quick-order-csv-upload/), [QuickOrderItems](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/containers/quick-order-items/), and [QuickOrderMultipleSku](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/containers/quick-order-multiple-sku/) for bulk ordering by SKU, search, or CSV upload. Render all three containers together so they communicate via the event bus. ## Grid Ordering (PDP) The Grid Ordering feature introduces a new B2B purchasing experience designed specifically for configurable products. Buyers view all product variants within a single grid interface and specify quantities for multiple variants at once before adding them to the cart. This feature is exclusive to Adobe Storefront and provides an efficient workflow for purchasing multiple variant combinations without navigating through individual product pages. The `QuickOrderVariantsGrid` container runs on the Product Details Page (PDP) when Grid Ordering is enabled for configurable products, and the Product Details block integrates it. See the [QuickOrderVariantsGrid container](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/containers/quick-order-variants-grid/) for configuration and the [Product Details drop-in](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/) for block setup. --- # QuickOrderCsvUpload Container The **QuickOrderCsvUpload** container provides CSV file upload for bulk quick order in the drop-in. It is useful for repeat orders or bulk purchases where manual SKU entry would be inefficient. The file must include `SKU` and `QTY` columns (max 200 rows). The container manages the full workflow: file selection, validation, and error handling. It verifies the uploaded file structure, validates required fields, and provides clear feedback when issues are detected. On success, it parses the content and emits `quick-order/add-items`. **QuickOrderItems** fetches product data and updates the list. When Quick Order is disabled, the container displays an overlay indicating that functionality is unavailable. ![QuickOrderCsvUpload container showing file upload area and sample CSV download button](https://experienceleague.adobe.com/developer/commerce/storefront/images/quick-order-csv-upload.png) *QuickOrderCsvUpload container with file upload and sample CSV download* ## Configuration | Parameter | Type | Req? | Description | |---|---|---|---| | `className` | `string` | No | CSS class applied to the container root (for example, `quick-order-csv-upload`). | | `routeSampleCSV` | `() => string` | No | Returns the URL or path for the sample CSV download (for example, `/path/to/sample.csv`). If not provided, the container generates a default sample CSV (`SKU,QTY` followed by `SKU123,1`, `SKU456,2`, `SKU789,3`) and triggers a browser download. | | `onFileUpload` | `(values: SubmitSkuValue) => void` | No | Optional callback when a valid file is parsed; otherwise the container emits `quick-order/add-items`. | ## CSV requirements - **Format:** CSV with header row. - **Required columns:** "SKU" and "QTY". - **Max rows:** 200 (excluding header). - **QTY:** Must be a positive integer per row. - **SKU:** Required for each row; invalid or empty rows produce validation errors. ## Validation errors The container displays specific messages for: invalid file type, empty file, missing or extra columns, max rows exceeded, invalid quantity, SKU required, no valid data rows, and parse/read failures. See [Quick Order Dictionary](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/dictionary/) for `CsvFileInput.uploadCSVErrors` keys and [Dictionary customization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/dictionaries/). ## Behavior 1. User selects a CSV file. 2. Container validates format and content. 3. On success: emits `quick-order/add-items` with parsed `SubmitSkuValue` (or calls `onFileUpload` if provided). 4. QuickOrderItems receives the event and fetches product data, updating the list. 5. User can download a sample CSV via "Download sample". When `routeSampleCSV` is provided, the container uses that URL; otherwise it generates a default sample and triggers a browser download. ## Usage Basic integration with a custom CSS class: ```js quickOrderProvider.render(QuickOrderCsvUpload, { className: 'quick-order-csv-upload', })(quickOrderCsvUploadContainer); ``` With `onFileUpload` callback (custom processing before adding to Quick Order): ```js quickOrderProvider.render(QuickOrderCsvUpload, { className: 'quick-order-csv-upload', onFileUpload: (parsedData) => { console.log('CSV file uploaded:', parsedData); // Custom processing before adding to Quick Order // If you omit this, the container emits quick-order/add-items automatically }, })(quickOrderCsvUploadContainer); ``` With custom sample CSV URL: ```js quickOrderProvider.render(QuickOrderCsvUpload, { className: 'quick-order-csv-upload', routeSampleCSV: () => '/quick-order/sample-csv', })(quickOrderCsvUploadContainer); ``` ## Events - **Emits:** `quick-order/add-items` (with parsed SKU/quantity array from valid CSV), `quick-order/loading` (during validation/processing). ## Admin panel No container-specific settings. Enable Quick Order in Adobe Commerce: **Stores** > **Settings** > **Configuration** > **General** > **B2B Features** > **Enable Quick Order**. --- # QuickOrderItems Container The **QuickOrderItems** container is the central component for managing and reviewing products in the drop-in workflow. It displays items added via CSV upload, multiple SKU entry, and product search. Each item shows name, SKU, price, and quantity. Users can update quantities, remove items, and configure options for configurable products. An integrated search input allows adding products through autocomplete without leaving the page. The container surfaces validation issues through contextual notifications and highlights problematic items with clickable SKU references that scroll to the relevant product. It handles full-success and partial-success add-to-cart scenarios, informing users which products were added and which require attention. Loading states are managed at global and item levels. When Quick Order is disabled, the container displays an overlay while preserving the underlying content. ![QuickOrderItems container showing product list with quantities, search input, and Add All to Cart button](https://experienceleague.adobe.com/developer/commerce/storefront/images/quick-order-items.png) *QuickOrderItems container with product list and search* ## Configuration | Parameter | Type | Req? | Description | |---|---|---|---| | `className` | `string` | No | CSS class applied to the container root. | | `getProductsData` | `(items: OrderItemInput[]) => Promise` | Yes | Fetches product data for the given SKUs/items. `OrderItemInput`: `{ sku: string; variantSku?: string; quantity?: number; replaceItemSku?: string }`. Typically from PDP drop-in `getProductsData`. Required for resolving products when items are added. | | `productsSearch` | `(params: { phrase: string; filter: Array<{ attribute: string; in: string[] }> }) => Promise<{ items: OrderItem[] }>` | No | Search API for product search by SKU or name. Typically from Product Discovery drop-in `search`. If not provided, search functionality is disabled in the UI. | | `searchFilter` | `Array<{ attribute: string; eq?: string; in?: string[] }>` | No | Filters applied to product search. Default: `[{ attribute: 'visibility', in: ['Search', 'Catalog, Search'] }]`. The Commerce boilerplate uses both `eq` (for categoryPath) and `in` (for visibility); the drop-in type definition may list only `in`. | | `handleAddToCart` | `(items: any[], clearItems: () => void) => void \| string \| Promise` | No | Custom handler when "Add All to Cart" is clicked. Receives the cart items array and a `clearItems` function to reset the list after successful addition. If omitted, the drop-in emits `quick-order/add-to-cart`. Return an error message string to show a notification and emit `quick-order/add-to-cart-error`. Invoke `clearItems()` after successful addition to reset the Quick Order list. | | `slots` | `object` | No | Slots for ProductPrice, ProductOptions, AddAllToCartButton, QuickOrderItemSearch, QuickOrderSearchAutocompleteItem. See [Slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/slots/). | > The TypeScript type for `searchFilter` in the drop-in may show only `{ attribute: string; in: string[] }`. The Commerce boilerplate passes both `eq` and `in`. Use the shape that your search API expects. > The `ProductOptions` slot is required to support configurable products. Without it, validation will show "Configuration required" for configurable items. Use the PDP drop-in `ProductOptions` container as the baseline implementation. ## Slots This container exposes slots for price display, product options (configurables), the add-to-cart button, and search UI. See [Quick Order Slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/slots/#quickorderitems-slots) and [Extending drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/extending/). ## Usage This container requires `getProductsData` and `productsSearch` (typically from the PDP and Product Discovery drop-ins). If `handleAddToCart` is omitted, the container emits `quick-order/add-to-cart` for external handlers. Example with PDP and Cart integration: ```js quickOrderProvider.render(QuickOrderItems, { getProductsData: pdpApi.getProductsData, productsSearch: searchApi.search, searchFilter: [ { attribute: 'categoryPath', eq: '' }, { attribute: 'visibility', in: ['Search', 'Catalog, Search'] }, ], className: 'quick-order-items', handleAddToCart: async (values) => { if (!values.length) return; try { await cartApi.addProductsToCart(values); window.location.href = rootLink('/cart'); } catch (error) { return error.message || 'Failed to add products to cart.'; } }, slots: { ProductPrice: (ctx) => { const priceContainer = document.createElement('div'); priceContainer.className = 'product-price-slot'; pdpProvider.render(ProductPrice, { scope: ctx.scope, initialData: ctx.item })(priceContainer); ctx.replaceWith(priceContainer); }, ProductOptions: (ctx) => { const optionsContainer = document.createElement('div'); optionsContainer.className = 'product-options-slot'; pdpProvider.render(ProductOptions, { scope: ctx.scope })(optionsContainer); ctx.replaceWith(optionsContainer); }, }, })(quickOrderItemsContainer); ``` ## Events - **Listens:** `quick-order/add-items` (adds/merges items and fetches product data), `quick-order/loading` (updates loading state), `cart/product/added` (from Cart drop-in; shows success notification). - **Emits:** `quick-order/add-items` (from integrated search when user adds via autocomplete), `quick-order/loading`, `quick-order/add-to-cart` (when no custom handler), `quick-order/add-to-cart-error`. ## Notifications The container shows notifications for: validation errors (missing options, not found, out of stock), backend add-to-cart errors, partial success (number of items added and number of failed SKUs), and full success (item count). ## Admin panel No container-specific settings. Enable Quick Order in Adobe Commerce: **Stores** > **Settings** > **Configuration** > **General** > **B2B Features** > **Enable Quick Order**. --- # QuickOrderMultipleSku Container The **QuickOrderMultipleSku** container provides a text area for entering multiple SKUs in the drop-in. Users can paste or type SKU lists in space-separated, comma-separated, or line-separated format—convenient for B2B buyers who have SKUs from catalogs, previous orders, spreadsheets, or external procurement systems. The container parses the input, removes duplicates, and aggregates quantities when the same SKU appears multiple times. Input processing is debounced (300ms) for good performance with large SKU lists. After entry, users click "Add to List" to add SKUs to the Quick Order list. When Quick Order is disabled, the container displays an overlay indicating that functionality is unavailable. ![QuickOrderMultipleSku container showing text area for entering multiple SKUs and Add to List button](https://experienceleague.adobe.com/developer/commerce/storefront/images/quick-order-multiple-sku.png) *QuickOrderMultipleSku container with SKU text area* ## Configuration | Parameter | Type | Req? | Description | |---|---|---|---| | `className` | `string` | No | CSS class applied to the container root (for example, `quick-order-multiple-sku`). | | `onChange` | `(payload: SubmitSkuValue) => void` | No | Callback invoked when the SKU list in the textarea changes (debounced 300ms). Receives parsed and deduplicated SKUs with quantities: `Array<{ sku: string; quantity: number }>`. Use for analytics, validation, or mirroring to external state. | | `slots` | `object` | No | Slot for `AddToListButton`. Context: `{ handleAddToList: (values?: SubmitSkuValue) => void; loading: boolean; textAreaValue: string }`. Use this to replace the default "Add to List" button with custom UI. If not customized, the default button emits `quick-order/add-items` with parsed SKUs when clicked. | ## Behavior 1. User enters SKUs in the text area (comma, space, or newline separated). 2. User clicks "Add to List". 3. Container parses input, deduplicates SKUs and sums quantities for duplicates. 4. Container emits `quick-order/add-items` with payload `SubmitSkuValue` (array of `{ sku, quantity }`). 5. QuickOrderItems receives the event, fetches product data via `getProductsData`, and updates the list. 6. Text area can be cleared on successful submission (implementation-dependent). ## Usage Basic integration: ```js quickOrderProvider.render(QuickOrderMultipleSku, { className: 'quick-order-multiple-sku', })(quickOrderMultipleSkuContainer); ``` With `onChange` callback: ```js quickOrderProvider.render(QuickOrderMultipleSku, { className: 'quick-order-multiple-sku', onChange: (payload) => { console.log('Parsed TextArea content', payload); // Output: [{ sku: 'SKU123', quantity: 2 }, { sku: 'SKU456', quantity: 1 }] }, })(quickOrderMultipleSkuContainer); ``` With custom `AddToListButton` slot: ```js quickOrderProvider.render(QuickOrderMultipleSku, { className: 'quick-order-multiple-sku', slots: { AddToListButton: (ctx) => { const { handleAddToList, loading, textAreaValue } = ctx; const button = document.createElement('button'); button.className = 'add-to-list-button'; button.textContent = 'Custom add to list'; button.addEventListener('click', () => handleAddToList(textAreaValue)); ctx.replaceWith(button); }, }, })(quickOrderMultipleSkuContainer); ``` ## Events - **Emits:** `quick-order/add-items` (with parsed SKU/quantity array), `quick-order/loading` (during processing). ## Admin panel No container-specific settings. Enable Quick Order in Adobe Commerce: **Stores** > **Settings** > **Configuration** > **General** > **B2B Features** > **Enable Quick Order**. ## Dictionary Labels such as "Add Products by SKU", "Use commas or paragraphs to separate SKUs.", "Enter SKUs here...", and "Add to List" come from the Quick Order dictionary. See [Quick Order Dictionary](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/dictionary/) and [Dictionary customization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/dictionaries/). --- # QuickOrderVariantsGrid Container The **QuickOrderVariantsGrid** container provides a grid-based interface for ordering product variants. It is designed for configurable products on the product detail page (PDP) where B2B customers need to select quantities across multiple variants (sizes, colors, and other attributes) within a single view. The grid displays all available variants in a structured table with attributes, pricing, and availability. Users enter quantities for multiple variants, review subtotals, and add them to the cart in bulk. The container integrates with the Product Details block when Grid Ordering is enabled. > The component returns null (unmounts) when variants are empty and loading is complete. It listens for `quick-order/grid-ordering-variants` and `quick-order/grid-ordering-reset-selected-variants`; it emits `quick-order/grid-ordering-selected-variants`. See [Grid Ordering events](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/events/#grid-ordering-events). ![QuickOrderVariantsGrid container showing variant grid with image, SKU, availability, price, quantity, and subtotal columns](https://experienceleague.adobe.com/developer/commerce/storefront/images/quick-order-variants-grid.png) *QuickOrderVariantsGrid container on product detail page* ## Configuration | Parameter | Type | Req? | Description | |---|---|---|---| | `className` | `string` | No | CSS class applied to the container root. | | `initialVariants` | `ProductVariant[]` | No | Initial array of product variants to display. When provided, automatically emits `quick-order/grid-ordering-variants`. If not provided, the component listens for the event from external sources. | | `onVariantsLoaded` | `(variants: VariantWithQuantity[]) => void` | No | Callback invoked when variants are successfully loaded and initialized (all quantities start at 0). | | `onSelectedVariantsChange` | `(data: VariantTableData[]) => void` | No | Debounced callback invoked when user changes quantities. Receives only variants with quantity > 0. `VariantTableData`: `{ sku: string; name: string; inStock: boolean; attributes: Record; price: number; quantity: number; subtotal: number; image: string }`. | | `debounceMs` | `number` | No | Debounce delay in milliseconds for `onSelectedVariantsChange` and event emissions. Default: `300`. | | `initialLoading` | `boolean` | No | Initial loading state before variants are loaded. Default: `true`. | | `visibleVariantsLimit` | `number` | No | Number of variant rows displayed initially before showing the **"Show All"** option. Default: `10`. Set to a very high number (for example, `1000`) to display all variants without collapsing the list. | | `columns` | `Array<{ key: string; label: string; sortBy?: 'asc' \| 'desc' \| true }>` | No | Custom column configuration. Default: `[{ key: 'image', label: 'Image' }, { key: 'sku', label: 'SKU' }, { key: 'availability', label: 'Availability' }, { key: 'price', label: 'Price' }, { key: 'quantity', label: 'Quantity' }, { key: 'subtotal', label: 'Subtotal' }]`. When using custom columns, provide corresponding slot implementations for custom column keys. | | `slots` | `object` | No | Slots for Actions, ImageCell, SKUCell, AvailabilityCell, PriceCell, QuantityCell, SubtotalCell, and custom column keys. See [Slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/slots/). | ## Architecture Grid Ordering replaces standard PDP interactions such as quantity selection and configurable product option selection. Because Grid Ordering allows selecting multiple variants at once, the standard PDP add-to-cart flow (single product with selected options) no longer applies. The grid provides a bulk-selection interface. The container handles variant selection and UI; the PDP integration layer (Product Details block) processes selections and executes the bulk add-to-cart operation. ## Usage The Product Details block integrates QuickOrderVariantsGrid when Grid Ordering is enabled for configurable products. The example below shows the pattern; the block provides `readBlockConfig`, `product`, and `gridOrderingContainer`: ```js // Identify whether this feature is enabled based on the block config const { 'grid-ordering-enabled': gridOrderingEnabledString = 'false' } = readBlockConfig(block); const gridOrderingEnabled = gridOrderingEnabledString === 'true'; // Based on product data, identify whether the feature should be enabled for a specific product // The Grid Ordering B2B feature (Quick Order drop-in) is enabled only for configurable products const isGridOrderingView = gridOrderingEnabled && product?.productType === 'complex' && !product?.isBundle; let gridOrderingSelectedVariants = []; // Conditionally render Grid Ordering container isGridOrderingView ? quickOrderProvider.render(QuickOrderVariantsGrid, { className: 'quick-order-variants-grid', columns: [ { key: 'image', label: 'Image' }, { key: 'variantOptionAttributes', label: 'Variant' }, { key: 'sku', label: 'SKU' }, { key: 'availability', label: 'Availability' }, { key: 'price', label: 'Price' }, { key: 'quantity', label: 'Quantity' }, { key: 'subtotal', label: 'Subtotal' }, ], slots: { VariantOptionAttributesCell: (ctx) => { const { variant } = ctx; const { variantOptionAttributes } = variant.product; const cellWrapper = document.createElement('div'); variantOptionAttributes.forEach((attr) => { const attributeWrapper = document.createElement('div'); attributeWrapper.classList.add('product-details__variants-grid-attribute'); const label = document.createElement('strong'); label.textContent = `${attr.label}:`; const value = document.createElement('span'); value.textContent = attr.value; attributeWrapper.appendChild(label); attributeWrapper.appendChild(value); cellWrapper.appendChild(attributeWrapper); }); ctx.appendChild(cellWrapper); }, }, })($gridOrderingContainer) : null; ``` ## Slots | Slot | Context | Description | |-----|---------|-------------| | `Actions` | `{ onClear: () => void; onSaveToCsv: () => void; onCollectData: () => VariantTableData[]; isDisabled: boolean; variantsCount: number }` | Replace the entire action bar (Clear, Save to CSV, Collect Data buttons). | | `ImageCell` | `{ variant: ProductVariant; quantity: number; onQuantityChange: (sku, qty) => void }` | Customize image cell rendering for each variant row. | | `SKUCell` | `{ variant: ProductVariant; quantity: number; onQuantityChange: (sku, qty) => void }` | Customize SKU cell rendering. | | `AvailabilityCell` | `{ variant: ProductVariant; quantity: number; onQuantityChange: (sku, qty) => void }` | Customize availability/stock status cell. | | `PriceCell` | `{ variant: ProductVariant; quantity: number; onQuantityChange: (sku, qty) => void }` | Customize price display. | | `QuantityCell` | `{ variant: ProductVariant; quantity: number; onQuantityChange: (sku, qty) => void }` | Replace the quantity input/incrementer with custom controls. | | `SubtotalCell` | `{ variant: ProductVariant; quantity: number; onQuantityChange: (sku, qty) => void }` | Customize subtotal calculation and display. | | `[CustomColumnKey]Cell` | `{ variant: ProductVariant; quantity: number; onQuantityChange: (sku, qty) => void }` | Custom rendering for any custom column defined in `columns`. The slot name should match the column `key` value with the `Cell` suffix. | ## Events - **Listens:** `quick-order/grid-ordering-variants` (variant data from PDP integration), `quick-order/grid-ordering-reset-selected-variants` (resets all quantities when emitted by integration layer, for example, after successful add-to-cart). - **Emits:** `quick-order/grid-ordering-variants` (when `initialVariants` is provided), `quick-order/grid-ordering-selected-variants` (when selection changes; debounced). ## Admin panel No container-specific settings. Grid Ordering is enabled via the Product Details block configuration. See the https://github.com/hlxsites/aem-boilerplate-commerce/blob/b2b/blocks/product-details/product-details.js for setup. --- # Quick Order Dictionary The **Quick Order dictionary** holds all user-facing text, labels, and messages in the drop-in. Customize it to localize the drop-in, match your brand voice, or override default text without changing the drop-in source. Each string uses a unique key path under `QuickOrder` (i18n pattern). ## How to customize Override dictionary values during drop-in initialization. The drop-in deep-merges your values with the defaults. Include only the keys you want to change. ```javascript await initializers.mountImmediately(initialize, { langDefinitions: { default: { QuickOrder: { QuickOrderItem: { addAllToCart: 'Add All to Cart', emptyList: 'No products in the list', }, CsvFileInput: { title: 'Add from File', downloadSample: 'Download sample', }, }, }, }, }); ``` For multi-language support and advanced patterns, see [Dictionary customization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/dictionaries/). ## Default keys and values Below are the default English (`en_US`) strings provided by the **Quick Order** drop-in: ```json title="en_US.json" { "QuickOrder": { "Search": { "placeholder": "Search by SKU...", "ariaLabel": "Search for products by SKU", "emptyState": "No results found", "resultsAvailable": "results available", "resultAvailable": "result available", "srInstructions": "Use arrow keys or Tab to navigate, Enter or Space to select, Escape to close." }, "SkuListInput": { "title": "Add Products by SKU", "helperText": "Use commas or paragraphs to separate SKUs.", "textArea": { "label": "Enter Multiple SKUs", "placeholder": "Enter SKUs here..." }, "button": "Add to List" }, "CsvFileInput": { "title": "Add from File", "helperText": "File must be in .csv format and include \"SKU\" and \"QTY\" columns ", "downloadSample": "Download sample", "inputLabel": "Choose File", "selectedFile": "Selected file", "uploadCSVErrors": { "invalidFile": "Invalid CSV file", "emptyFile": "File is empty", "missingColumns": "Must contain \"SKU\" and \"QTY\" columns", "extraColumns": "Must contain only \"SKU\" and \"QTY\" columns", "maxRowsExceeded": "File exceeds maximum of {maxRows} rows", "skuRequired": "Row {rowNumber}: SKU is required", "invalidQuantity": "Row {rowNumber}: QTY must be a positive integer", "noValidData": "File contains no valid data rows", "onlyCSV": "Only CSV files are allowed", "failedToRead": "Failed to read file", "failedToParse": "Failed to parse CSV file" } }, "QuickOrderItem": { "title": "Enter SKU or search by Product Name", "quantity": "Quantity: ", "price": "Price: ", "sku": "SKU", "remove": "Remove", "removeItem": "Remove item", "showOptions": "Show additional options", "hideOptions": "Hide additional options", "additionalOptions": "Additional options", "noAdditionalOptions": "No additional options available", "emptyList": "No products in the list", "loading": "Loading...", "productNotFound": "Product not found", "productNotFoundDescription": "The product with SKU {sku} could not be found", "configurableProductError": "Configuration required", "configurableProductErrorDescription": "Use ProductOptions Slot in QuickOrderItems container to enable configurable product options.", "configurableOptionsWarning": "Product configuration required", "configurableOptionsWarningDescription": "Please select all required product options before adding to cart", "productOptions": "Product Options", "outOfStock": "Out of Stock", "addAllToCart": "Add to Cart", "disabledMessage": "Quick Order feature disabled", "notification": { "validationError": "Product(s) require(s) your attention", "backendError": "An error occurred while adding products to the cart", "success": "{count} product(s) successfully added to the cart", "partialSuccess": "{count} of {total} products were added to the cart. Some products could not be added", "unexpectedError": "An unexpected error has occurred" } }, "VariantsGrid": { "imageColumn": "Image", "attributesColumn": "Attributes", "skuColumn": "SKU", "availabilityColumn": "Availability", "priceColumn": "Price", "minOrderColumn": "Min Order / Pack Size", "quantityColumn": "Quantity", "subtotalColumn": "Subtotal", "clearButton": "Clear", "saveToCsvButton": "Save to CSV", "collectDataButton": "Collect Data", "inStock": "In Stock", "outOfStock": "Out of Stock", "tableCaption": "Product Variants Grid", "quantityLabel": "Quantity for", "showAll": "Show All Items", "showLess": "Show Less" } } } ``` --- # Quick Order Data & Events The **Quick Order** drop-in uses the [event bus](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/events/) to coordinate containers (**QuickOrderCsvUpload**, **QuickOrderItems**, **QuickOrderMultipleSku**) and to integrate with the Cart drop-in. Events coordinate adding items, loading states, and add-to-cart success or error handling. ## Events reference | Event | Direction | Description | |-------|-----------|-------------| | [`quick-order/add-items`](#quick-orderadd-items-emits-and-listens) | Emits and listens | Items added via CSV upload, multiple SKU entry, or Quick Order search within QuickOrderItems. Payload: `SubmitSkuValue` (array of `{ sku, quantity }`). Triggers product fetch and list update in QuickOrderItems. | | [`quick-order/loading`](#quick-orderloading-emits-and-listens) | Emits and listens | Loading state changed. Payload: `boolean`. Disables inputs and shows loading indicators while processing. | | [`quick-order/add-to-cart`](#quick-orderadd-to-cart-emits) | Emits | Request to add items to cart when no custom `handleAddToCart` is used. Payload: array of cart item values. Default cart handler processes items. | | [`quick-order/add-to-cart-error`](#quick-orderadd-to-cart-error-emits) | Emits | Add-to-cart operation failed. Payload: `{ message: string }`. Shows error notification. | | [`b2b-quick-order/error`](#b2b-quick-ordererror-emits) | Emits | Emitted when a network error occurs during any Quick Order API call. | | [`quick-order/grid-ordering-variants`](#quick-ordergrid-ordering-variants-emits-and-listens) | Emits and listens | Provides variant data to `QuickOrderVariantsGrid`. Emitted externally or when `initialVariants` is set. | | [`quick-order/grid-ordering-selected-variants`](#quick-ordergrid-ordering-selected-variants-emits) | Emits | Notifies when selected variants or quantities change in the `QuickOrderVariantsGrid`. Debounced. | | [`quick-order/grid-ordering-reset-selected-variants`](#quick-ordergrid-ordering-reset-selected-variants-listens) | Listens | Resets the current grid selection (clears all quantities). | | [`cart/product/added`](#cartproductadded-listens) | Listens | Products added to cart successfully. Payload: `any[]`. Quick Order shows success notification. | > The drop-in also emits `quick-order/add-to-cart-success` (payload: `void`) internally when the add-to-cart operation completes. For integration, listen to `cart/product/added` from the Cart drop-in instead. ## Event details ### `quick-order/add-items` (emits and listens) QuickOrderCsvUpload and QuickOrderMultipleSku emit this when the user adds items via CSV or the SKU text area. QuickOrderItems also emits it when the user adds a product via the integrated search (autocomplete). QuickOrderItems listens, fetches product data, and updates the list. #### Event payload ```typescript type SubmitSkuValue = Array<{ sku: string; quantity: number }>; ``` #### When triggered - After CSV file is validated and parsed (QuickOrderCsvUpload) - After user clicks "Add to List" in QuickOrderMultipleSku with parsed SKUs - When user selects a product from the search autocomplete in QuickOrderItems #### Example ```js events.on('quick-order/add-items', (payload) => { console.log('Items to add:', payload); // SubmitSkuValue }); ``` ### `quick-order/loading` (emits and listens) Containers emit this when a loading state starts or ends (for example, while fetching products or adding to cart). #### Event payload ```typescript boolean ``` #### Example ```js events.on('quick-order/loading', (isLoading) => { console.log('Quick Order loading:', isLoading); }); ``` ### `quick-order/add-to-cart` (emits) QuickOrderItems emits this when the user clicks "Add All to Cart" and no custom `handleAddToCart` is provided. A default handler may listen and call the Cart API. #### Event payload ```typescript any[] // Cart item values (sku, quantity, options, and so on) ``` ### `quick-order/add-to-cart-error` (emits) QuickOrderItems emits this when add-to-cart fails (backend error or custom handler returns an error message string). #### Event payload ```typescript { message: string } ``` #### Example ```js events.on('quick-order/add-to-cart-error', ({ message }) => { console.error('Add to cart failed:', message); }); ``` ### `cart/product/added` (listens) The Cart drop-in emits this when products are added to the cart. Quick Order listens and shows a success notification. #### Event payload ```typescript any[] ``` ### `b2b-quick-order/error` (emits) Emitted when a network error occurs during any Quick Order API call. Does not fire for intentional user cancellations (`AbortError`). #### Event payload ```typescript { source: 'auth'; type: 'network'; error: Error; } ``` #### Example ```js events.on('b2b-quick-order/error', ({ source, type, error }) => { console.error('Quick Order network error:', error.message); }); ``` ### `quick-order/grid-ordering-variants` (emits and listens) Provides variant data to `QuickOrderVariantsGrid`. Emitted externally by the integration layer (for example, the Product Details block) after fetching product variants. Also emitted by `QuickOrderVariantsGrid` itself when `initialVariants` is provided as a prop. Not required if `initialVariants` is passed directly. #### Event payload ```typescript ProductVariant[] // Array of product variants ``` #### Example ```js // Emit after fetching variants from the PDP events.emit('quick-order/grid-ordering-variants', productVariants); ``` ### `quick-order/grid-ordering-selected-variants` (emits) Emitted by `QuickOrderVariantsGrid` when the user changes quantities for any variant. Payload contains only variants with `quantity > 0`. Emissions are debounced. Captured by the PDP integration layer to execute bulk add-to-cart. #### Event payload ```typescript Array<{ sku: string; name: string; inStock: boolean; attributes: Record; price: number; quantity: number; subtotal: number; image: string; }> ``` #### Example ```js events.on('quick-order/grid-ordering-selected-variants', (selectedVariants) => { console.log('Selected variants:', selectedVariants); // selectedVariants contains only variants with quantity > 0 }); ``` ### `quick-order/grid-ordering-reset-selected-variants` (listens) Listens for this event to reset all quantities in the `QuickOrderVariantsGrid` to zero. Typically emitted by the integration layer after a successful add-to-cart operation. #### Event payload ```typescript void ``` #### Example ```js // Reset the grid after successful add-to-cart events.emit('quick-order/grid-ordering-reset-selected-variants'); ``` ## PDP integration events (internal) QuickOrderItems emits PDP-scoped events to enable reuse of PDP containers (for example, ProductPrice, ProductOptions) within the Quick Order interface: | Event | Payload | Description | |-------|---------|--------------| | `{scope}/pdp/data` | `OrderItem` | Scoped event for product data updates per item. | | `{scope}/pdp/values` | option values | Captures selected product options for configurable products. | These events are used internally by the slot system and typically do not require custom handling. ## Grid Ordering events The QuickOrderVariantsGrid container (Grid Ordering on PDP) uses these events: | Event | Direction | Description | |-------|-----------|-------------| | `quick-order/grid-ordering-variants` | Emitted externally (integration layer) | Provides variant data to QuickOrderVariantsGrid. Payload: array of product variants. Typically emitted after variants are fetched on the PDP. Not required if `initialVariants` prop is used. | | `quick-order/grid-ordering-selected-variants` | Emitted by QuickOrderVariantsGrid | Notifies when selected variants or quantities change. Payload: array of selected variants (quantity > 0) with enriched data (sku, attributes, price, quantity, subtotal). Captured by PDP integration for bulk add-to-cart. Emissions are debounced. | | `quick-order/grid-ordering-reset-selected-variants` | Emitted externally | Resets current grid selection (clears all quantities). Typically used after successful add-to-cart. | ## Listening to events All Quick Order events use the centralized event bus: ```js events.on('quick-order/add-items', handleAddItems); events.on('quick-order/loading', handleLoading); events.on('quick-order/add-to-cart-error', handleAddToCartError); events.on('cart/product/added', handleCartProductAdded); // Clean up when needed events.off('quick-order/add-items', handleAddItems); ``` > When using a custom `handleAddToCart` in QuickOrderItems, you control redirects and error messages; the drop-in still emits `quick-order/add-to-cart-error` when your handler returns an error message string. --- # Quick Order Functions The Quick Order drop-in exposes API functions for store configuration. Use them to determine whether Quick Order is enabled. Containers use this to show or hide the disabled overlay. ## Functions reference | Function | Description | | --- | --- | | [`getStoreConfig`](#getstoreconfig) | Returns store configuration including the Quick Order feature flag (`quickOrderActive`). | ## getStoreConfig Fetches store configuration via GraphQL and returns `quickorder_active` (mapped to `quickOrderActive`). The drop-in uses it to show a disabled overlay when Quick Order is off in Adobe Commerce Admin. ```ts const getStoreConfig = async (): Promise<{ storeConfig: { quickOrderActive: boolean; }; }> ``` ### Returns - `storeConfig.quickOrderActive` — `true` when Quick Order is enabled in store config. ### Example ```js const { storeConfig } = await getStoreConfig(); if (storeConfig.quickOrderActive) { console.log('Quick Order is enabled'); } else { console.log('Quick Order is disabled'); } ``` > The initializer sets the GraphQL endpoint via `setEndpoint()` and loads store config during initialization. Containers read the feature state from the drop-in context. Call `getStoreConfig` in your block only when building custom logic around the feature flag. ## API dependencies Quick Order does not implement or duplicate APIs for product data, search, or add-to-cart. Instead, it relies on external APIs provided by the PDP, Cart, and Product Discovery drop-ins (for example, `getProductsData`, `productsSearch`, Cart add-to-cart). This approach avoids code duplication, maintains consistency with existing logic, and preserves extensibility. You can pass custom API methods in the same way when needed. See the [QuickOrderItems](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/containers/quick-order-items/) container for required and optional parameters. --- # Quick Order overview The **Quick Order** B2B drop-in introduces two purchasing workflows for Adobe Storefront, designed for B2B buyers who need to place orders quickly and efficiently. The drop-in delivers two core features: 1. **Quick Order** — A fast product ordering interface that provides functional parity with the Adobe Commerce Quick Order experience. Customers add products to an order list using SKU search, CSV upload, or multiple SKU input. 2. **Grid Ordering** — A new B2B ordering experience exclusive to Adobe Storefront. Buyers add multiple configurable product variants to the cart from a single grid interface. The drop-in consists of four containers: **QuickOrderCsvUpload**, **QuickOrderItems**, and **QuickOrderMultipleSku** for the Quick Order page, and **QuickOrderVariantsGrid** for Grid Ordering on the PDP. The Quick Order workflow suits buyers who need to add known products quickly without navigating catalog pages. Containers communicate via the [event bus](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/events/), ensuring loose coupling and flexible extensibility. Any container can be replaced with a custom implementation that follows the defined event contracts. By default, Quick Order does not enforce additional permissions or access restrictions beyond your storefront's existing authentication. The Quick Order page is implemented in the https://github.com/hlxsites/aem-boilerplate-commerce/blob/b2b/blocks/commerce-b2b-quick-order/commerce-b2b-quick-order.js. Grid Ordering is integrated into the https://github.com/hlxsites/aem-boilerplate-commerce/blob/b2b/blocks/product-details/product-details.js. ![Quick Order page layout showing product list, multiple SKU text area, and CSV file upload with sample download](https://experienceleague.adobe.com/developer/commerce/storefront/images/quick-order-index.png) *Quick Order page layout with product list, SKU entry, and CSV upload* ## Supported Commerce features The following table provides an overview of the Adobe Commerce features that the Quick Order drop-in supports: | Feature | Status | | ------- | ------ | | Quick Order page (bulk add by SKU/search) | Supported | | Multiple SKU text entry | Supported | | CSV file upload (SKU, QTY) | Supported | | Product search by SKU and name | Supported | | Configurable product options in Quick Order | Supported | | Bulk add to cart with validation | Supported | | Grid Ordering on PDP (configurable variants) | Supported | | Quick Order feature toggle (`quickOrderActive`) | Supported | | Event-driven container coordination | Supported | | Internationalization (i18n) support | Supported | | Integration with Cart and PDP drop-ins | Supported | ## Section topics - **[Quick Start](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/quick-start/)** — Package details, import paths, and block example. New to drop-ins? See [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/). - **[Initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/initialization/)** — Configure the initializer with language definitions and store config (`quickOrderActive`). - **[Containers](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/containers/)** — **QuickOrderCsvUpload**, **QuickOrderItems**, **QuickOrderMultipleSku** (Quick Order page); **QuickOrderVariantsGrid** (Grid Ordering on PDP). Covers container configuration and how the containers work together. - **[Functions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/functions/)** — API functions (for example, `getStoreConfig`) for store configuration. - **[Events](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/events/)** — Event bus usage: `quick-order/add-items`, `quick-order/loading`, `quick-order/add-to-cart`, `quick-order/add-to-cart-error`, `cart/product/added`. See [Event bus reference](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/events/). - **[Slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/slots/)** — Customize `ProductPrice`, `ProductOptions`, `AddAllToCartButton`, and search slots. See [Extending drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/extending/). - **[Dictionary](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/dictionary/)** — i18n keys for labels and messages. See [Dictionary customization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/dictionaries/). - **[Styles](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/styles/)** — CSS classes for the block and containers. See [Styling](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/). --- # Quick Order initialization The **Quick Order initializer** configures the Quick Order B2B drop-in for bulk ordering: it sets the GraphQL endpoint (Core Service), loads placeholders from `placeholders/quick-order.json`, and passes language definitions. The drop-in reads the store configuration (`quickOrderActive`) to enable or disable the feature. Version: 1.1.0 ## Configuration options | Parameter | Type | Req? | Description | |---|---|---|---| | `langDefinitions` | [`LangDefinitions`](#langdefinitions) | No | Language definitions for internationalization (i18n). Override dictionary keys for localization or branding. | | `quickOrderActive` | `boolean` | No | Override for the Quick Order feature flag. When `false`, containers show a disabled overlay. By default the drop-in reads this from store config (`storeConfig.quickorder_active`). | ## Default configuration Defaults when no configuration is provided: ```javascript title="scripts/initializers/quick-order.js" await initializeDropin(async () => { setEndpoint(CORE_FETCH_GRAPHQL); const labels = await fetchPlaceholders('placeholders/quick-order.json'); const langDefinitions = { default: { ...labels }, }; return initializers.mountImmediately(initialize, { langDefinitions }); })(); ``` > The boilerplate uses `initializeDropin` to coordinate initialization order. The `labels` from `placeholders/quick-order.json` are spread into `langDefinitions.default`. The drop-in deep-merges these with its built-in defaults. ## Language definitions Override dictionary keys for localization or branding. The `langDefinitions` object maps locale keys to custom strings that override default text for the drop-in. ```javascript title="scripts/initializers/quick-order.js" const langDefinitions = { default: { QuickOrder: { QuickOrderItem: { addAllToCart: 'Add All to Cart', emptyList: 'No products in the list', // ... other keys — see Dictionary page }, }, }, }; return initializers.mountImmediately(initialize, { langDefinitions }); ``` > For the full list of keys and multi-language support, see the [Quick Order Dictionary](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/dictionary/). For patterns and placeholders, see [Dictionary customization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/dictionaries/). ## Store configuration The drop-in reads store config from Adobe Commerce: | Config key | Description | | ---------- | ----------- | | `quickorder_active` | When `false`, Quick Order is disabled; all containers show a disabled overlay. | Enable Quick Order in Adobe Commerce Admin: **Stores** > **Settings** > **Configuration** > **General** > **B2B Features** > **Enable Quick Order**. Apply the configuration to both `.page` and `.live` when using this drop-in. ## Configuration types ### LangDefinitions Maps locale identifiers to dictionaries of key-value pairs. The `default` locale is used as the fallback when no specific locale matches. ```typescript langDefinitions?: { [locale: string]: { [key: string]: string | Record; }; }; ``` --- # Quick Order Quick Start Enable bulk ordering by SKU, search, and CSV upload in your B2B storefront with the Quick Order drop-in for Adobe Storefront. This drop-in provides fast product ordering and Grid Ordering for configurable products. ## Block DOM skeleton The block creates the container divs, then runs `provider.render` into them. Use these class names when building your block or matching the boilerplate: - `.quick-order-title` — Page title (optional; boilerplate uses Header component). - `.quick-order-main-container` — Wrapper for the two-column layout. - `.quick-order-items-container` — Target for QuickOrderItems. - `.quick-order-right-side` — Wrapper for the right column. - `.quick-order-multiple-sku-container` — Target for QuickOrderMultipleSku. - `.quick-order-csv-upload-container` — Target for QuickOrderCsvUpload. ## Quick example The https://github.com/hlxsites/aem-boilerplate-commerce/blob/b2b/blocks/commerce-b2b-quick-order/commerce-b2b-quick-order.js in the Commerce boilerplate uses this pattern for the Quick Order page with all three containers: ```js // 1. Import initializers (Quick Order + dependencies: cart, PDP, search) // 2. Import containers, provider, APIs, and commerce helpers // 3. Render in your block (for example, commerce-b2b-quick-order) export default async function decorate(block) { const itemsContainer = block.querySelector('.quick-order-items-container'); const multipleSkuContainer = block.querySelector('.quick-order-multiple-sku-container'); const csvUploadContainer = block.querySelector('.quick-order-csv-upload-container'); quickOrderProvider.render(QuickOrderItems, { getProductsData: pdpApi.getProductsData, productsSearch: searchApi.search, searchFilter: [ { attribute: 'categoryPath', eq: '' }, { attribute: 'visibility', in: ['Search', 'Catalog, Search'] }, ], handleAddToCart: async (values) => { if (!values.length) return; try { await cartApi.addProductsToCart(values); window.location.href = rootLink('/cart'); } catch (error) { return error.message || 'Failed to add products to cart.'; } }, slots: { ProductPrice: (ctx) => { /* ... */ }, ProductOptions: (ctx) => { /* ... */ } }, })(itemsContainer); quickOrderProvider.render(QuickOrderMultipleSku, { className: 'quick-order-multiple-sku' })(multipleSkuContainer); quickOrderProvider.render(QuickOrderCsvUpload, { className: 'quick-order-csv-upload' })(csvUploadContainer); } ``` **New to drop-ins?** See [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/) for step-by-step instructions. ## Quick reference **Import paths:** - Initializer: `import '../../scripts/initializers/quick-order.js'` - Containers: `import ContainerName from '@dropins/storefront-quick-order/containers/ContainerName.js'` - Provider: `import { render } from '@dropins/storefront-quick-order/render.js'` **Package:** `@dropins/storefront-quick-order` **Example containers:** `QuickOrderCsvUpload`, `QuickOrderItems`, `QuickOrderMultipleSku` ## Learn more - [Containers](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/containers/) — Available UI components and configuration options - [Initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/initialization/) — Configure initializer and language definitions - [Events](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/events/) — Event-driven coordination between containers - [Slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/slots/) — Extend containers with custom content - [Dictionary](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/dictionary/) — i18n keys and customization - [Styles](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/styles/) — CSS classes and styling --- # Quick Order Slots The Quick Order B2B drop-in exposes slots for specific UI sections, primarily on **QuickOrderItems**. Use slots to replace or extend the product price, product options (configurables), add-to-cart button, and search UI. See [Extending drop-in components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/extending/) for slot behavior. | Container | Slots | |-----------|-------| | [QuickOrderItems](#quickorderitems-slots) | `ProductPrice`, `ProductOptions`, `AddAllToCartButton`, `QuickOrderItemSearch`, `QuickOrderSearchAutocompleteItem` | | [QuickOrderMultipleSku](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/containers/quick-order-multiple-sku/) | `AddToListButton` (optional) | | [QuickOrderVariantsGrid](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/containers/quick-order-variants-grid/#slots) | `Actions`, `ImageCell`, `SKUCell`, `AvailabilityCell`, `PriceCell`, `QuantityCell`, `SubtotalCell`, custom column keys | ## QuickOrderItems slots The `QuickOrderItems` slots let you replace the default product price, configurable product options, "Add All to Cart" button, and search/autocomplete UI. ```typescript interface QuickOrderItemsProps { slots?: { ProductPrice?: SlotProps; ProductOptions?: SlotProps; AddAllToCartButton?: SlotProps; QuickOrderItemSearch?: SlotProps; QuickOrderSearchAutocompleteItem?: SlotProps; }; } ``` ### ProductPrice slot Context: `{ item: OrderItem; scope: string }`. Use this slot to render price with the PDP drop-in `ProductPrice` container (or a custom component) for correct tier pricing and currency. The boilerplate passes `scope` and `initialData: ctx.item` to the PDP container. ### ProductOptions slot Context: `{ item: OrderItem; scope: string }`. Use this slot to render configurable product options (for example, size, color) with the PDP drop-in `ProductOptions` container. Required for configurables in the Quick Order list so users can select options before adding to cart. ### AddAllToCartButton slot Context: `{ handleAddToCart: () => void; clearItems: () => void; loading: boolean; isDisabledButton: boolean }`. Replace the default "Add All to Cart" button with custom UI or behavior. Call `handleAddToCart()` to trigger add-to-cart. Call `clearItems()` after success to reset the list. ### QuickOrderItemSearch slot Context: `{ item: OrderItem; scope: string; handleSearchChange: (e: Event) => void; searchResults: OrderItem[]; searchValue: string; shouldShowResults: boolean; handleItemClick: (item: OrderItem) => void }`. Customize the search input and results area for adding or replacing an item via search. ### QuickOrderSearchAutocompleteItem slot Context: `{ item: OrderItem; index: number; activeIndex: number; createItemClickHandler: (item: OrderItem) => () => void }`. Customize how each search result option renders in the autocomplete list. ## Example: ProductPrice and ProductOptions The Commerce boilerplate wires PDP containers into Quick Order so each list item shows price and options correctly: ```js quickOrderProvider.render(QuickOrderItems, { getProductsData: pdpApi.getProductsData, productsSearch: searchApi.search, handleAddToCart: async (values) => { /* ... */ }, slots: { ProductPrice: (ctx) => { const priceContainer = document.createElement('div'); priceContainer.className = 'product-price-slot'; pdpProvider.render(ProductPrice, { scope: ctx.scope, initialData: ctx.item })(priceContainer); ctx.replaceWith(priceContainer); }, ProductOptions: (ctx) => { const optionsContainer = document.createElement('div'); optionsContainer.className = 'product-options-slot'; pdpProvider.render(ProductOptions, { scope: ctx.scope })(optionsContainer); ctx.replaceWith(optionsContainer); }, }, })(quickOrderItemsContainer); ``` > For configurable products, providing the `ProductOptions` slot is required so users can select options before adding to cart; otherwise validation will show "Configuration required" for those items. ## QuickOrderMultipleSku slots The `QuickOrderMultipleSku` container exposes one slot for customizing the "Add to List" button. ```typescript interface QuickOrderMultipleSkuProps { slots?: { AddToListButton?: SlotProps<{ handleAddToList: (values?: SubmitSkuValue) => void; loading: boolean; textAreaValue: string; }>; }; } ``` ### AddToListButton slot Context: `{ handleAddToList: (values?: SubmitSkuValue) => void; loading: boolean; textAreaValue: string }`. Use this to replace the default "Add to List" button with custom UI. Call `handleAddToList()` to trigger the add-items flow. #### Example ```js quickOrderProvider.render(QuickOrderMultipleSku, { slots: { AddToListButton: (ctx) => { const { handleAddToList, loading, textAreaValue } = ctx; const button = document.createElement('button'); button.textContent = loading ? 'Adding...' : 'Custom Add to List'; button.disabled = loading; button.addEventListener('click', () => handleAddToList()); ctx.replaceWith(button); }, }, })(quickOrderMultipleSkuContainer); ``` ## QuickOrderVariantsGrid slots The `QuickOrderVariantsGrid` container exposes slots for customizing each cell type in the variants grid, as well as the action bar. ```typescript interface QuickOrderVariantsGridProps { slots?: { Actions?: SlotProps<{ onClear: () => void; onSaveToCsv: () => void; onCollectData: () => VariantTableData[]; isDisabled: boolean; variantsCount: number }>; ImageCell?: SlotProps<{ variant: ProductVariant; quantity: number; onQuantityChange: (sku: string, qty: number) => void }>; SKUCell?: SlotProps<{ variant: ProductVariant; quantity: number; onQuantityChange: (sku: string, qty: number) => void }>; AvailabilityCell?: SlotProps<{ variant: ProductVariant; quantity: number; onQuantityChange: (sku: string, qty: number) => void }>; PriceCell?: SlotProps<{ variant: ProductVariant; quantity: number; onQuantityChange: (sku: string, qty: number) => void }>; QuantityCell?: SlotProps<{ variant: ProductVariant; quantity: number; onQuantityChange: (sku: string, qty: number) => void }>; SubtotalCell?: SlotProps<{ variant: ProductVariant; quantity: number; onQuantityChange: (sku: string, qty: number) => void }>; [customColumnKey: string]: SlotProps<{ variant: ProductVariant; quantity: number; onQuantityChange: (sku: string, qty: number) => void }> | undefined; }; } ``` ### Actions slot Context: `{ onClear, onSaveToCsv, onCollectData, isDisabled, variantsCount }`. Replace the entire action bar (Clear, Save to CSV, and Collect Data buttons) with custom UI. ### ImageCell slot Context: `{ variant, quantity, onQuantityChange }`. Customize image cell rendering for each variant row. ### SKUCell slot Context: `{ variant, quantity, onQuantityChange }`. Customize the SKU cell rendering. ### AvailabilityCell slot Context: `{ variant, quantity, onQuantityChange }`. Customize availability/stock status display. ### PriceCell slot Context: `{ variant, quantity, onQuantityChange }`. Customize price display for each variant. ### QuantityCell slot Context: `{ variant, quantity, onQuantityChange }`. Replace the quantity input with custom controls. Call `onQuantityChange(sku, qty)` to update state. ### SubtotalCell slot Context: `{ variant, quantity, onQuantityChange }`. Customize subtotal calculation and display. ### Custom column slots For any column defined in the `columns` prop with a custom `key`, provide a slot named `{key}Cell`. Context is the same as other cell slots. #### Example: Custom VariantOptionAttributesCell ```js quickOrderProvider.render(QuickOrderVariantsGrid, { columns: [{ key: 'variantOptionAttributes', label: 'Variant' }], slots: { VariantOptionAttributesCell: (ctx) => { const { variant } = ctx; const { variantOptionAttributes } = variant.product; const cellWrapper = document.createElement('div'); variantOptionAttributes.forEach((attr) => { const item = document.createElement('div'); item.textContent = `${attr.label}: ${attr.value}`; cellWrapper.appendChild(item); }); ctx.appendChild(cellWrapper); }, }, })(gridOrderingContainer); ``` --- # Quick Order styles This page lists CSS classes for the Quick Order block layout and Grid Ordering (PDP) visibility. The Commerce boilerplate uses these classes. For design tokens and styling, see [Styling Drop-In Components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/). ## Quick Order block (commerce-b2b-quick-order) Add or override these classes in the block CSS (for example, `blocks/commerce-b2b-quick-order/commerce-b2b-quick-order.css`). Source: https://github.com/hlxsites/aem-boilerplate-commerce/blob/b2b/blocks/commerce-b2b-quick-order/commerce-b2b-quick-order.css. ```css .commerce-b2b-quick-order { padding: var(--spacing-large) 0; } .quick-order-main-container { display: flex; flex-direction: column; gap: var(--spacing-medium); width: 100%; } .quick-order-items-container { width: 100%; } .quick-order-right-side { display: flex; flex-direction: column; gap: var(--spacing-medium); width: 100%; } .quick-order-multiple-sku-container, .quick-order-csv-upload-container { width: 100%; } @media (min-width: 800px) { .quick-order-main-container { flex-direction: row; align-items: flex-start; gap: var(--spacing-medium); } .quick-order-items-container { flex: 2; min-width: 0; } .quick-order-right-side { flex: 1; min-width: 0; border-left: 1px solid var(--color-neutral-400); padding-left: var(--spacing-medium); } } ``` - **`.commerce-b2b-quick-order`** — Wrapper for the Quick Order block; vertical padding. - **`.quick-order-main-container`** — Flex container: column on small screens, row on wider (800px+). - **`.quick-order-items-container`** — Holds the QuickOrderItems container; takes 2/3 width on desktop. - **`.quick-order-right-side`** — Holds QuickOrderMultipleSku and QuickOrderCsvUpload; 1/3 width on desktop with left border and padding. - **`.quick-order-multiple-sku-container`**, **`.quick-order-csv-upload-container`** — Wrappers for the two right-side containers. ## Grid Ordering (product details) When Grid Ordering is enabled for configurable products, the Product Details block uses these classes to show or hide the variants grid. Source: https://github.com/hlxsites/aem-boilerplate-commerce/blob/b2b/blocks/product-details/product-details.css. ```css .product-details__variants-grid-attribute strong { font-weight: var(--type-body-1-strong-font); margin-right: var(--spacing-xxsmall); } .product-details__grid-ordering--enabled { display: block; } .product-details__grid-ordering--disabled { display: none; } ``` - **`.product-details__grid-ordering--enabled`** — Shown when Grid Ordering is on; contains the QuickOrderVariantsGrid. - **`.product-details__grid-ordering--disabled`** — Hidden when Grid Ordering is off. ## Drop-in component classes The Quick Order drop-in uses additional BEM-style and data attributes for items list, search, CSV input, and notifications. Use browser DevTools to inspect elements; many are prefixed with `b2b-quick-order-` or `dropin-` from the drop-in package. For the source CSS, see the https://github.com/adobe-commerce/storefront-quick-order (when available). --- # Quote Management Containers The **Quote Management** drop-in provides pre-built container components for integrating into your storefront. Version: 1.2.0 ## What are Containers? Containers are pre-built UI components that combine functionality, state management, and presentation. They provide a complete solution for specific features and can be customized through props, slots, and CSS. ## Available Containers | Container | Description | | --------- | ----------- | | [ItemsQuoted](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/containers/items-quoted/) | Displays a summary of items that have been quoted, providing a quick overview of quoted products. | | [ItemsQuotedTemplate](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/containers/items-quoted-template/) | Displays items stored in a quote template for reuse in future quote requests. | | [ManageNegotiableQuote](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/containers/manage-negotiable-quote/) | Provides comprehensive quote management capabilities for existing quotes. | | [ManageNegotiableQuoteTemplate](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/containers/manage-negotiable-quote-template/) | Provides the interface for managing quote templates with template-specific actions and details. | | [OrderSummary](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/containers/order-summary/) | Displays a comprehensive pricing breakdown for quotes including subtotal calculations, applied discounts, tax information, and grand total. | | [OrderSummaryLine](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/containers/order-summary-line/) | Renders individual line items within the order summary such as subtotal, shipping, or tax rows. | | [QuoteCommentsList](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/containers/quote-comments-list/) | Displays all comments and communications between buyer and seller for a negotiable quote. | | [QuoteHistoryLog](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/containers/quote-history-log/) | Shows the complete history of actions, status changes, and updates for a quote throughout its lifecycle. | | [QuoteSummaryList](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/containers/quote-summary-list/) | Displays quote metadata including quote ID, status, dates, buyer information, and shipping details. | | [QuoteTemplateCommentsList](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/containers/quote-template-comments-list/) | Displays all comments associated with a quote template. | | [QuoteTemplateHistoryLog](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/containers/quote-template-history-log/) | Shows the complete history of changes and updates for a quote template. | | [QuoteTemplatesListTable](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/containers/quote-templates-list-table/) | Displays all quote templates in a paginated table with search, filter, and action capabilities. | | [QuotesListTable](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/containers/quotes-list-table/) | Displays a list of quotes with pagination capabilities, status indicators, page size selection, and item range display. | | [RequestNegotiableQuoteForm](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/containers/request-negotiable-quote-form/) | Enables customers to request new negotiable quotes from their cart contents. | | [ShippingAddressDisplay](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/containers/shipping-address-display/) | Shows the selected shipping address for a quote or a warning if there is no shipping address set. | > Each container is designed to work independently but can be composed together to create comprehensive user experiences. --- # ItemsQuoted Container The `ItemsQuoted` container displays a summary of items that have been quoted, providing a quick overview of quoted products. It shows product information and pricing, quantity and discount details, subtotal calculations, and action buttons for quote management. The component includes responsive design for mobile and desktop viewing. Version: 1.2.0 ## Configuration The `ItemsQuoted` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `quoteData` | `NegotiableQuoteModel` | No | Quote data object. Auto-populated from drop-in state when omitted. | | `onItemCheckboxChange` | `function` | No | Callback when item checkbox is toggled. Use for tracking selections or custom validation. | | `onItemDropdownChange` | `function` | No | Callback when item action dropdown changes. Use for custom action handling or analytics. | | `onUpdate` | `function` | No | Callback on form submission (`quantity/note` updates). Use for custom validation or tracking. | | `onRemoveItemsRef` | `function` | No | Provides access to internal item removal handler. Use for custom removal workflows. | | `onRemoveModalStateChange` | `function` | No | Callback when remove confirmation modal `opens/closes`. Use for tracking or custom modal logic. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container exposes the following slots for customization: | Slot | Type | Required | Description | |------|------|----------|-------------| | `ProductListTable` | `function` | No | Customizes the quoted items table. Use to replace or wrap the default table UI while keeping the built-in handlers for item selection, dropdown actions, quantity changes, and submission. | | `QuotePricesSummary` | `SlotProps` | No | Customizes the pricing summary for quoted items. Use to change how totals and price breakdowns are displayed. | ## Usage The following example demonstrates how to use the `ItemsQuoted` container: ```js await provider.render(ItemsQuoted, { onItemCheckboxChange: (itemCheckbox) => console.log('ItemCheckboxChange', itemCheckbox), onItemDropdownChange: (itemDropdown) => console.log('ItemDropdownChange', itemDropdown), slots: { // Add custom slot implementations here } })(block); ``` --- # ItemsQuotedTemplate Container The `ItemsQuotedTemplate` container displays items stored in a quote template for reuse in future quote requests. Version: 1.2.0 ## Configuration The `ItemsQuotedTemplate` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `templateData` | `NegotiableQuoteTemplateModel` | No | Template data object. Auto-populated from drop-in state when omitted. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container exposes the following slots for customization: | Slot | Type | Required | Description | |------|------|----------|-------------| | `ProductListTable` | `function` | No | Customizes the quote template items table. Use to replace or wrap the default table UI while keeping the built-in handlers for dropdown actions, quantity changes, and submission. | | `QuotePricesSummary` | `SlotProps` | No | Customizes the pricing summary for quote template items. Use to change how totals and price breakdowns are displayed. | ## Usage The following example demonstrates how to use the `ItemsQuotedTemplate` container: ```js // Omit templateData to use drop-in state. When passing from parent: const templateData = props.templateData; await provider.render(ItemsQuotedTemplate, { templateData, })(block); ``` --- # ManageNegotiableQuote Container The `ManageNegotiableQuote` container provides comprehensive quote management capabilities for existing quotes. It displays quote details (creation date, sales rep, expiration), manages quote status and updates, shows the product list with pricing and quantity controls, provides quote actions (print, copy, delete, send for review), displays shipping information, and includes a quote comments section. All actions respect permission-based access control. Version: 1.2.0 ## Configuration The `ManageNegotiableQuote` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `onActionsDropdownChange` | `function` | No | Callback when the actions dropdown selection changes. Use for custom action handling, analytics, or to intercept actions before they execute. | | `onActionsButtonClick` | `function` | No | Callback when an action button is clicked. Use for custom action handling, analytics, or to add additional behavior when users trigger quote actions. | | `onSendForReview` | `function` | No | Callback when the quote is sent for review. Use to implement custom notifications, trigger follow-up workflows, or integrate with external systems. | | `maxFiles` | `number` | No | Sets the maximum number of files that can be attached when sending a quote for review. Enforces company policies on attachment limits and prevents excessive file uploads that could impact performance or storage. | | `maxFileSize` | `number` | No | Sets the maximum file size in bytes for attachments. Controls the upper limit for individual file uploads. Use to prevent large file uploads that could impact performance, storage, or network bandwidth. | | `acceptedFileTypes` | `string[]` | No | Specifies an array of MIME types allowed for file attachments (for example, `\['application/pdf', 'image/jpeg', 'image/png'\]`). Use to restrict uploads to specific document types required by your quote approval process. | | `onDuplicateQuote` | `function` | No | Callback when the quote is duplicated. Use to implement custom notifications, navigate to the new quote, or sync with external systems. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container exposes the following slots for customization: | Slot | Type | Required | Description | |------|------|----------|-------------| | `QuoteName` | `SlotProps` | No | Customize the quote name display and rename functionality. Use to add custom icons, styling, or additional metadata next to the quote name. | | `QuoteStatus` | `SlotProps` | No | Customize how the quote status is displayed. Use to add custom status badges, colors, or additional status information. | | `Banner` | `SlotProps` | No | Customize the alert banner shown for specific quote states (submitted, pending, expired). Use to provide custom messaging or styling for different quote statuses. | | `DuplicateQuoteWarningBanner` | `SlotProps` | No | Customizes the warning banner shown after duplicating a quote when the new quote contains out-of-stock items. Use to change the messaging, styling, or dismissal behavior. | | `Details` | `SlotProps` | No | Customize the quote metadata display (created date, sales rep, expiration). Use to add additional fields, reorder information, or apply custom formatting. | | `ActionBar` | `SlotProps` | No | Customize the action buttons and dropdown menu for quote operations. Use to add custom actions, reorder existing actions, or integrate with external systems. | | `QuoteContent` | `SlotProps` | No | Customize the entire tabbed content area containing items, comments, and history. Use to add new tabs, reorder tabs, or completely replace the tabbed interface. | | `ItemsQuotedTab` | `SlotProps` | No | Customize the Items Quoted tab content. Use to add additional product information, custom filtering, or integrate with inventory systems. | | `CommentsTab` | `SlotProps` | No | Customize the Comments tab displaying quote discussions. Use to add custom comment filters, sorting, or rich text formatting. | | `HistoryLogTab` | `SlotProps` | No | Customize the History Log tab showing quote activity. Use to add custom filtering, grouping by action type, or export functionality. | | `ShippingInformationTitle` | `SlotProps` | No | Customize the shipping section heading. Use to add icons, tooltips, or additional contextual information about shipping requirements. | | `ShippingInformation` | `function` | No | Customize the shipping address display and selection. Use to integrate with third-party shipping services, add address validation, or provide custom address formatting. | | `QuoteCommentsTitle` | `SlotProps` | No | Customize the quote comments section heading. Use to add help text, character limits, or formatting guidelines. | | `QuoteComments` | `SlotProps` | No | Customize the comment input field. Use to add rich text editing, @mentions, file attachments inline, or comment templates. | | `AttachFilesField` | `function` | No | Customize the file attachment input control. Use to integrate with document management systems, add drag-and-drop functionality, or provide custom file previews. | | `AttachedFilesList` | `function` | No | Customize how attached files are displayed. Use to add file previews, virus scanning status, or integration with external document viewers. | | `Footer` | `function` | No | Customize the Send for Review button and footer actions. Use to add additional submission options, validation steps, or approval workflow controls. | ## Usage The following example demonstrates how to use the `ManageNegotiableQuote` container: ```js await provider.render(ManageNegotiableQuote, { acceptedFileTypes: ACCEPTED_FILE_TYPES, onActionsButtonClick: (action) => { switch (action) { case 'print': window.print(); break; default: break; } }, slots: { Footer: async (ctx) => { ctx.appendChild(checkoutButtonContainer); // Get the current user email currentUserEmail = await getCurrentUserEmail(); // Checkout button is enabled if the quote can be checked out // and the current user email is the same as the quote email const enabled = ctx.quoteData?.canCheckout && currentUserEmail === ctx.quoteData?.email; // Initial render renderCheckoutButton(ctx, enabled); // Re-render on state changes ctx.onChange((next) => { // Checkout button is enabled if the quote can be checked out // and the current user email is the same as the quote email const nextEnabled = next.quoteData?.canCheckout && currentUserEmail === next.quoteData?.email; renderCheckoutButton(next, nextEnabled); }); }, ShippingInformation: (ctx) => { // Append the address error container to the shipping information container ctx.appendChild(addressErrorContainer); const shippingInformation = document.createElement('div'); shippingInformation.classList.add('negotiable-quote__select-shipping-information'); ctx.appendChild(shippingInformation); const progressSpinner = document.createElement('div'); progressSpinner.classList.add('negotiable-quote__progress-spinner-container'); progressSpinner.setAttribute('hidden', true); ctx.appendChild(progressSpinner); UI.render(ProgressSpinner, { className: 'negotiable-quote__progress-spinner', size: 'large', })(progressSpinner); ctx.onChange((next) => { // Remove existing content from the shipping information container shippingInformation.innerHTML = ''; const { quoteData } = next; if (!quoteData) return; if (!quoteData.canSendForReview) return; if (quoteData.canSendForReview) { accountRenderer.render(Addresses, { minifiedView: false, withActionsInMinifiedView: false, selectable: true, className: 'negotiable-quote__shipping-information-addresses', selectShipping: true, defaultSelectAddressId: 0, onAddressData: (params) => { const { data, isDataValid: isValid } = params; const addressUid = data?.uid; if (!isValid) return; if (!addressUid) return; progressSpinner.removeAttribute('hidden'); shippingInformation.setAttribute('hidden', true); setShippingAddress({ quoteUid: quoteId, addressId: addressUid, }).finally(() => { progressSpinner.setAttribute('hidden', true); shippingInformation.removeAttribute('hidden'); }); }, onSubmit: (event, formValid) => { if (!formValid) return; const formValues = getFormValues(event.target); const [regionCode, regionId] = formValues.region?.split(', ') || []; const regionIdNumber = parseInt(regionId, 10); // iterate through the object entries and combine the values of keys that have // a prefix of 'street' into an array const streetInputValues = Object.entries(formValues) .filter(([key]) => key.startsWith('street')) .map(([_, value]) => value); const createCustomerAddressInput = { city: formValues.city, company: formValues.company, countryCode: formValues.countryCode, defaultBilling: !!formValues.defaultBilling || false, defaultShipping: !!formValues.defaultShipping || false, fax: formValues.fax, firstname: formValues.firstName, lastname: formValues.lastName, middlename: formValues.middlename, postcode: formValues.postcode, prefix: formValues.prefix, region: regionCode ? { regionCode, regionId: regionIdNumber, } : undefined, street: streetInputValues, suffix: formValues.suffix, telephone: formValues.telephone, vatId: formValues.vatId, }; progressSpinner.removeAttribute('hidden'); shippingInformation.setAttribute('hidden', true); createCustomerAddress(createCustomerAddressInput) .then((result) => { const addressUid = typeof result === 'string' ? result : result?.uid; if (!addressUid) { throw new Error('Address uid not returned from createCustomerAddress.'); } return setShippingAddress({ quoteUid: quoteId, addressId: addressUid, }); }) .catch((error) => { addressErrorContainer.removeAttribute('hidden'); UI.render(InLineAlert, { type: 'error', description: `${error}`, })(addressErrorContainer); }) .finally(() => { progressSpinner.setAttribute('hidden', true); shippingInformation.removeAttribute('hidden'); }); }, })(shippingInformation); } }); }, } })(block); ``` --- # ManageNegotiableQuoteTemplate Container The `ManageNegotiableQuoteTemplate` container provides the interface for managing quote templates with template-specific actions and details. Version: 1.2.0 ## Configuration The `ManageNegotiableQuoteTemplate` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `onActionsButtonClick` | `function` | No | Callback when an action button is clicked. Use for custom action handling, analytics, or to add additional behavior when users trigger template actions. | | `onSendForReview` | `function` | No | Callback when the quote template is sent for review. Use to implement custom notifications, trigger follow-up workflows, or integrate with external systems. | | `maxFiles` | `number` | No | Sets the maximum number of files that can be attached when sending a quote template for review. Use to enforce attachment limits and prevent excessive uploads. | | `maxFileSize` | `number` | No | Sets the maximum file size in bytes for quote template attachments. Use to prevent large uploads and provide consistent UX for file validation. | | `acceptedFileTypes` | `string[]` | No | Specifies an array of MIME types allowed for quote template attachments (for example `\['application/pdf', 'image/jpeg', 'image/png'\]`). Use to restrict uploads to supported document types. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container exposes the following slots for customization: | Slot | Type | Required | Description | |------|------|----------|-------------| | `TemplateName` | `SlotProps` | No | Customizes the template name area, including the rename affordance. Use to change how the template name is displayed or to add additional metadata next to it. | | `TemplateStatus` | `SlotProps` | No | Customizes how the template status is displayed. Use to change the status label, badge styling, or status-specific messaging. | | `Banner` | `SlotProps` | No | Customizes the alert banner area for template state changes and actions. Use to add custom messaging for template lifecycle events (for example, updated, in review, accepted). | | `Details` | `SlotProps` | No | Customizes the template details section. Use to add or reorder template metadata and apply custom formatting. | | `ActionBar` | `SlotProps` | No | Customizes the action bar for quote template operations. Use to add custom actions, reorder controls, or integrate with external workflows. | | `ReferenceDocuments` | `function` | No | Customizes the reference documents section. Use to replace the default list UI or to customize add, edit, and remove behavior. | | `ItemsTable` | `SlotProps` | No | Customizes the template items section container. Use to wrap or replace the default items table layout. | | `ItemsQuotedTab` | `SlotProps` | No | Customizes the Items Quoted tab for template items. Use to add additional item details, custom actions, or supplemental content. | | `CommentsTab` | `SlotProps` | No | Customizes the Comments tab for quote template discussions. Use to change how comments are displayed or to add validation and formatting. | | `HistoryLogTab` | `SlotProps` | No | Customizes the History Log tab for template activity. Use to filter, group, or extend the activity feed. | | `CommentsTitle` | `SlotProps` | No | Customizes the comments section heading for a quote template. Use to add help text or additional context for commenters. | | `Comments` | `SlotProps` | No | Customizes the comments section content for a quote template. Use to replace the default comments UI or to integrate with an external commenting system. | | `AttachFilesField` | `function` | No | Customizes the file attachment input for a quote template. Use to add drag-and-drop UX, validation messaging, or integrations with document storage. | | `AttachedFilesList` | `function` | No | Customizes how attached files are displayed for a quote template. Use to add previews, custom removal UX, or external viewers. | | `HistoryLogTitle` | `SlotProps` | No | Customizes the history log section heading for a quote template. Use to add contextual help or status indicators. | | `HistoryLog` | `SlotProps` | No | Customizes the history log content for a quote template. Use to change formatting, sorting, or grouping of history entries. | | `Footer` | `function` | No | Customizes the footer actions for quote template management. Use to change submit and accept controls, add validation steps, or show custom status messaging. | | `ShippingInformationTitle` | `SlotProps` | No | Customizes the shipping information section heading for a quote template. Use to add icons, tooltips, or additional context. | | `ShippingInformation` | `function` | No | Customizes the shipping information section for a quote template. Use to replace the default display or integrate with custom shipping workflows. | ## Usage The following example demonstrates how to use the `ManageNegotiableQuoteTemplate` container: ```js await provider.render(ManageNegotiableQuoteTemplate, { acceptedFileTypes: ACCEPTED_FILE_TYPES, slots: { ShippingInformation: (ctx) => { // Append the address error container to the shipping information container ctx.appendChild(addressErrorContainer); const shippingInformation = document.createElement('div'); shippingInformation.classList.add('negotiable-quote-template__select-shipping-information'); ctx.appendChild(shippingInformation); const progressSpinner = document.createElement('div'); progressSpinner.classList.add('negotiable-quote-template__progress-spinner-container'); progressSpinner.setAttribute('hidden', true); ctx.appendChild(progressSpinner); UI.render(ProgressSpinner, { className: 'negotiable-quote-template__progress-spinner', size: 'large', })(progressSpinner); ctx.onChange((next) => { // Remove existing content from the shipping information container shippingInformation.innerHTML = ''; const { templateData } = next; if (!templateData) return; if (!templateData.canSendForReview) return; if (templateData.canSendForReview) { accountRenderer.render(Addresses, { minifiedView: false, withActionsInMinifiedView: false, selectable: true, className: 'negotiable-quote-template__shipping-information-addresses', selectShipping: true, defaultSelectAddressId: 0, showShippingCheckBox: false, showBillingCheckBox: false, onAddressData: (params) => { const { data, isDataValid: isValid } = params; const addressUid = data?.uid; if (!isValid) return; if (!addressUid) return; progressSpinner.removeAttribute('hidden'); shippingInformation.setAttribute('hidden', true); addQuoteTemplateShippingAddress({ templateId: quoteTemplateId, shippingAddress: { customerAddressUid: addressUid, }, }).finally(() => { progressSpinner.setAttribute('hidden', true); shippingInformation.removeAttribute('hidden'); }); }, onSubmit: (event, formValid) => { if (!formValid) return; const formValues = getFormValues(event.target); const [regionCode, _regionId] = formValues.region?.split(', ') || []; // iterate through the object entries and combine the values of keys that have // a prefix of 'street' into an array const streetInputValues = Object.entries(formValues) .filter(([key]) => key.startsWith('street')) .map(([_, value]) => value); const addressInput = { firstname: formValues.firstName, lastname: formValues.lastName, company: formValues.company, street: streetInputValues, city: formValues.city, region: regionCode, postcode: formValues.postcode, countryCode: formValues.countryCode, telephone: formValues.telephone, }; // These values are not part of the standard address input const additionalAddressInput = { vat_id: formValues.vatId, }; progressSpinner.removeAttribute('hidden'); shippingInformation.setAttribute('hidden', true); addQuoteTemplateShippingAddress({ templateId: quoteTemplateId, shippingAddress: { address: { ...addressInput, additionalInput: additionalAddressInput, }, customerNotes: formValues.customerNotes, }, }) .catch((error) => { addressErrorContainer.removeAttribute('hidden'); UI.render(InLineAlert, { type: 'error', description: `${error}`, })(addressErrorContainer); }) .finally(() => { progressSpinner.setAttribute('hidden', true); shippingInformation.removeAttribute('hidden'); }); }, })(shippingInformation); } }); }, } })(block); ``` --- # OrderSummary Container The `OrderSummary` container displays a comprehensive pricing breakdown for quotes including subtotal calculations, applied discounts, tax information, and grand total. This component provides transparency in quote pricing. Version: 1.2.0 ## Configuration The `OrderSummary` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `showTotalSaved` | `boolean` | No | S`hows/hides` total savings amount. | | `updateLineItems` | `function` | No | Callback to transform line items before display. Use for custom line item logic. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `OrderSummary` container: ```js await provider.render(OrderSummary, { showTotalSaved: true, updateLineItems: () => {}, initialData: {}, })(block); ``` --- # OrderSummaryLine Container The `OrderSummaryLine` container renders individual line items within the order summary such as subtotal, shipping, or tax rows. Version: 1.2.0 ## Configuration The `OrderSummaryLine` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `label` | `VNode \| string` | Yes | Yes \| Label text or component for the line item. | | `price` | `VNode>` | Yes | Price component for the line item. | | `classSuffixes` | `Array` | No | Provides an array of CSS class suffixes for styling variants. Use to apply different visual styles to order summary line items based on their type or context. | | `labelClassSuffix` | `string` | No | CSS class suffix specifically for the label. | | `testId` | `string` | No | Test ID for automated testing. | | `children` | `any` | No | Child elements to render within the container | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `OrderSummaryLine` container: ```js // In updateLineItems or a slot - lineItem from quote prices/order summary data const label = lineItem.label ?? 'Subtotal'; const price = document.createElement('span'); price.textContent = lineItem.formattedValue ?? String(lineItem.value ?? 0); await provider.render(OrderSummaryLine, { label, price, classSuffixes: [lineItem.key] })(block); ``` --- # QuoteCommentsList Container The `QuoteCommentsList` container displays all comments and communications between buyer and seller for a negotiable quote. Version: 1.2.0 ## Configuration The `QuoteCommentsList` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `quoteData` | `NegotiableQuoteModel` | No | Quote data object. Auto-populated from drop-in state when omitted. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `QuoteCommentsList` container: ```js // Omit quoteData to use drop-in state. When passing from parent: const quoteData = props.quoteData; await provider.render(QuoteCommentsList, { quoteData, })(block); ``` --- # QuoteHistoryLog Container The `QuoteHistoryLog` container shows the complete history of actions, status changes, and updates for a quote throughout its lifecycle. Version: 1.2.0 ## Configuration The `QuoteHistoryLog` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `quoteData` | `NegotiableQuoteModel` | No | Quote data object. Auto-populated from drop-in state when omitted. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `QuoteHistoryLog` container: ```js // Omit quoteData to use drop-in state. When passing from parent: const quoteData = props.quoteData; await provider.render(QuoteHistoryLog, { quoteData, })(block); ``` --- # QuoteSummaryList Container The `QuoteSummaryList` container displays quote metadata including quote ID, status, dates, buyer information, and shipping details. Version: 1.2.0 ## Configuration The `QuoteSummaryList` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `hideHeading` | `boolean` | No | Hides the list heading when true. | | `hideFooter` | `boolean` | No | Hides item footers when true. | | `routeProduct` | `function` | No | Generates product detail URLs. Receives item data as a parameter and returns a URL string. Use to create links to product pages, add query parameters, or integrate with your application's product routing system. | | `showMaxItems` | `boolean` | No | Shows maximum item count indicator when true. | | `attributesToHide` | `SwitchableAttributes[]` | No | Specifies an array of product attributes to hide from display. Use to customize which product attributes are visible in the quote summary, reducing visual clutter or focusing on specific attribute types. | | `accordion` | `boolean` | No | Enables accordion-style collapsible items when true. | | `variant` | `'primary' \| 'secondary'` | No | Visual variant (primary or secondary). | | `showDiscount` | `boolean` | No | Shows discount information when true. | | `showSavings` | `boolean` | No | Shows savings amount when true. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container exposes the following slots for customization: | Slot | Type | Required | Description | |------|------|----------|-------------| | `Heading` | `SlotProps` | No | Customize the heading displaying the item count. Receives the total quantity and quote ID. Use to add custom branding, icons, or additional quote metadata in the header. | | `Footer` | `SlotProps` | No | Customize the footer section below individual quote items. Receives the item data. Use to add custom actions like add to cart, remove, or item-specific notes for each line item. | | `Thumbnail` | `SlotProps` | No | Customize the product image display for each item. Receives the item and default image props. Use to add image overlays, badges for discounted items, or custom image loading behavior. | | `ProductAttributes` | `SlotProps` | No | Customize how product attributes (size, color) are displayed for each item. Receives the item data. Use to add custom formatting, grouping, or additional attribute information. | | `QuoteSummaryFooter` | `SlotProps` | No | Customize the View More button and footer actions for the entire quote list. Receives the display state. Use to add additional actions like export to PDF or email quote. | | `QuoteItem` | `SlotProps` | No | Customize the entire quote item row. Receives comprehensive item data and formatting functions. Use for complete control over item rendering, such as custom layouts for mobile versus desktop. | | `ItemTitle` | `SlotProps` | No | Customize the product title display for each item. Receives the item data. Use to add product badges, custom linking, or additional product information inline with the title. | | `ItemPrice` | `SlotProps` | No | Customize the unit price display for each item. Receives the item data. Use to add price comparison, original pricing with strikethrough, or custom currency formatting. | | `ItemTotal` | `SlotProps` | No | Customize the line total display for each item. Receives the item data. Use to add savings calculations, tax breakdowns, or custom total formatting with discounts highlighted. | | `ItemSku` | `SlotProps` | No | Customize the SKU display for each item. Receives the item data. Use to add copy-to-clipboard functionality, links to product pages, or custom SKU formatting. | ## Usage The following example demonstrates how to use the `QuoteSummaryList` container: ```js await provider.render(QuoteSummaryList, { hideHeading: true, hideFooter: true, variant: 'secondary', routeProduct: (item) => `/product/${item.url?.urlKey ?? item.sku}`, slots: { // Add custom slot implementations here } })(block); ``` --- # QuoteTemplateCommentsList Container The `QuoteTemplateCommentsList` container displays all comments associated with a quote template. Version: 1.2.0 ## Configuration The `QuoteTemplateCommentsList` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `templateData` | `NegotiableQuoteTemplateModel` | No | Template data object. Auto-populated from drop-in state when omitted. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `QuoteTemplateCommentsList` container: ```js // Omit templateData to use drop-in state. When passing from parent: const templateData = props.templateData; await provider.render(QuoteTemplateCommentsList, { templateData, })(block); ``` --- # QuoteTemplateHistoryLog Container The `QuoteTemplateHistoryLog` container shows the complete history of changes and updates for a quote template. Version: 1.2.0 ## Configuration The `QuoteTemplateHistoryLog` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `templateData` | `NegotiableQuoteTemplateModel` | No | Template data object. Auto-populated from drop-in state when omitted. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `QuoteTemplateHistoryLog` container: ```js // Omit templateData to use drop-in state. When passing from parent: const templateData = props.templateData; await provider.render(QuoteTemplateHistoryLog, { templateData, })(block); ``` --- # QuoteTemplatesListTable Container The `QuoteTemplatesListTable` container displays all quote templates in a paginated table with search, filter, and action capabilities. Version: 1.2.0 ## Configuration The `QuoteTemplatesListTable` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `pageSize` | `number` | No | Sets the number of items displayed per page for pagination. Controls how many quote template items appear in each page view. Use to optimize display for different screen sizes or match user preferences. | | `showItemRange` | `boolean` | No | Shows item range indicator when true. | | `showPageSizePicker` | `boolean` | No | Shows page size selector when true. | | `showPagination` | `boolean` | No | Shows pagination controls when true. | | `onViewQuoteTemplate` | `function` | No | Callback when viewing a template. Receives template ID, name, and status. | | `onGenerateQuoteFromTemplate` | `function` | No | Callback when generating quote from template. Receives template and quote IDs. | | `onPageSizeChange` | `function` | No | Callback when page size changes. | | `onPageChange` | `function` | No | Callback when page changes. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container exposes the following slots for customization: | Slot | Type | Required | Description | |------|------|----------|-------------| | `Name` | `SlotProps` | No | Customize template name cell. | | `State` | `SlotProps` | No | Customize state cell (active/inactive). | | `Status` | `SlotProps` | No | Customize status cell. | | `ValidUntil` | `SlotProps` | No | Customize valid until date cell. | | `MinQuoteTotal` | `SlotProps` | No | Customize minimum quote total cell. | | `OrdersPlaced` | `SlotProps` | No | Customize orders placed count cell. | | `LastOrdered` | `SlotProps` | No | Customize last ordered date cell. | | `Actions` | `function` | No | Customize actions cell (view, generate quote buttons). | | `EmptyTemplates` | `SlotProps` | No | Customize empty state message when no templates exist. | | `ItemRange` | `SlotProps` | No | Customize item range display (for example '1-10 of 50'). | | `PageSizePicker` | `function` | No | Customize page size selector. | | `Pagination` | `function` | No | Customize pagination controls. | ## Usage The following example demonstrates how to use the `QuoteTemplatesListTable` container: ```js await provider.render(QuoteTemplatesListTable, { // Append quote template id to the url to navigate to render the details view onViewQuoteTemplate: (id) => { window.location.href = `${window.location.pathname}?quoteTemplateId=${id}`; }, pageSize: 10, showItemRange: true, showPageSizePicker: true, showPagination: true })(block); ``` --- # QuotesListTable Container The `QuotesListTable` container displays a list of quotes with pagination capabilities. It includes quote list display with status indicators, pagination controls, page size selection, item range display, and responsive table design. Version: 1.2.0 ## Configuration The `QuotesListTable` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `pageSize` | `number` | No | Sets the number of items displayed per page for pagination. Controls how many quote items appear in each page view. Use to optimize display for different screen sizes or match user preferences. | | `showItemRange` | `boolean` | No | Shows item range indicator when true. | | `showPageSizePicker` | `boolean` | No | Shows page size selector when true. | | `showPagination` | `boolean` | No | Shows pagination controls when true. | | `onViewQuote` | `function` | No | Callback when viewing a quote. Receives quote ID, name, and status. | | `onPageSizeChange` | `function` | No | Callback when page size changes. | | `onPageChange` | `function` | No | Callback when page changes. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container exposes the following slots for customization: | Slot | Type | Required | Description | |------|------|----------|-------------| | `QuoteName` | `SlotProps` | No | Customize quote name cell. | | `Created` | `SlotProps` | No | Customize created date cell. | | `CreatedBy` | `SlotProps` | No | Customize created by (buyer name) cell. | | `Status` | `SlotProps` | No | Customize status cell. | | `LastUpdated` | `SlotProps` | No | Customize last updated date cell. | | `QuoteTemplate` | `SlotProps` | No | Customize quote template reference cell. | | `QuoteTotal` | `SlotProps` | No | Customize quote total amount cell. | | `Actions` | `function` | No | Customize actions cell (view button). | | `EmptyQuotes` | `SlotProps` | No | Customize empty state message when no quotes exist. | | `ItemRange` | `SlotProps` | No | Customize item range display (for example '1-10 of 50'). | | `PageSizePicker` | `function` | No | Customize page size selector. | | `Pagination` | `function` | No | Customize pagination controls. | ## Usage The following example demonstrates how to use the `QuotesListTable` container: ```js await provider.render(QuotesListTable, { onViewQuote: (id, _quoteName, _status) => { // Append quote id to the url to navigate to render the manage quote view window.location.href = `${window.location.pathname}?quoteid=${id}`; }, showItemRange: true, showPageSizePicker: true, showPagination: true })(block); ``` --- # RequestNegotiableQuoteForm Container The `RequestNegotiableQuoteForm` container enables customers to request new negotiable quotes from their cart contents. This component handles quote name and comment input, draft saving functionality, form validation and error handling, and `success/error` messaging. It includes support for file attachments and integrates with cart contents. Version: 1.2.0 ## Configuration The `RequestNegotiableQuoteForm` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `cartId` | `string` | Yes | Specifies the cart ID to create the quote from. Required to identify which shopping cart contains the items to be quoted. | | `maxFiles` | `number` | No | Sets the maximum number of files that can be attached when sending a quote for review. Enforces company policies on attachment limits and prevents excessive file uploads that could impact performance or storage. | | `maxFileSize` | `number` | No | Sets the maximum file size in bytes for attachments. Controls the upper limit for individual file uploads. Use to prevent large file uploads that could impact performance, storage, or network bandwidth. | | `acceptedFileTypes` | `string[]` | No | Specifies an array of allowed MIME types for file attachments. Use to restrict uploads to specific document types required by your quote approval process (for example, `\['application/pdf', 'image/jpeg', 'image/png'\]`). | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container exposes the following slots for customization: | Slot | Type | Required | Description | |------|------|----------|-------------| | `ErrorBanner` | `SlotProps` | No | Customize the error message display when quote submission fails. Receives the error message. Use to add custom error tracking, retry mechanisms, or support contact information. | | `SuccessBanner` | `SlotProps` | No | Customize the success message display after quote submission. Receives the success message. Use to add next steps, links to the quote details page, or custom celebration animations. | | `Title` | `SlotProps` | No | Customize the form heading. Receives the title text. Use to add custom branding, help icons, or contextual information about the quote process. | | `CommentField` | `function` | No | Customize the comment input field. Receives form state and error handlers. Use to add character counters, rich text editing, comment templates, or AI-assisted comment generation. | | `QuoteNameField` | `function` | No | Customize the quote name input field. Receives form state and error handlers. Use to add auto-naming logic based on cart contents, validation rules, or naming conventions specific to your organization. | | `AttachFileField` | `function` | No | Customize the file attachment input control. Receives upload handler and form state. Use to add drag-and-drop functionality, file previews, or integration with document management systems. | | `AttachedFilesList` | `function` | No | Customize how attached files are displayed. Receives the file list and removal handler. Use to add file previews, virus scanning status, download links, or file metadata display. | | `RequestButton` | `function` | No | Customize the primary submit button for requesting a quote. Receives the submission handler and form state. Use to add confirmation dialogs, custom loading states, or multi-step submission workflows. | | `SaveDraftButton` | `function` | No | Customize the draft save button for saving incomplete quote requests. Receives the save handler and form state. Use to add auto-save functionality, draft naming conventions, or draft management interfaces. | ## Usage The following example demonstrates how to use the `RequestNegotiableQuoteForm` container: ```js await provider.render(RequestNegotiableQuoteForm, { cartId, acceptedFileTypes: ACCEPTED_FILE_TYPES })(block); ``` --- # ShippingAddressDisplay Container The `ShippingAddressDisplay` container shows the selected shipping address for a quote or a warning if there is no shipping address set. Version: 1.2.0 ## Configuration The `ShippingAddressDisplay` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `shippingAddress` | `ShippingAddress` | No | Provides the shipping address object to display for the quote. Contains address details such as street, city, region, postal code, and country. Required to render the address information in the container. | | `loading` | `boolean` | No | Controls the loading state of the container. Shows a loading indicator when set to `true` while address data is being fetched or processed. Use to provide visual feedback during async operations. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `ShippingAddressDisplay` container: ```js // Omit shippingAddress to use drop-in state. When passing from parent: const address = props.quoteData?.shipping_address; await provider.render(ShippingAddressDisplay, { shippingAddress: address, loading: false, })(block); ``` --- # Quote Management Dictionary The **Quote Management dictionary** contains all user-facing text, labels, and messages displayed by this drop-in. Customize the dictionary to: - **Localize** the drop-in for different languages and regions - **Customize** labels and messages to match your brand voice - **Override** default text without modifying source code for the drop-in Dictionaries use the **i18n (internationalization)** pattern, where each text string is identified by a unique key path. Version: 1.2.0 ## How to customize The Quote Management drop-in dictionary comes from the default `en_US` dictionary shipped in the Quote Management repo (`src/i18n/en_US.json`). In the current Quote Management implementation, the `langDefinitions` value passed into `initialize.init()` is stored in config but is not applied to the UI provider that renders the containers. If you need to override dictionary values without forking the drop-in, provide your own `UIProvider` and `Render` wrapper with a full `langDefinitions` object. ```javascript // Copy the defaults from the JSON block on this page, then change the keys you need. const langDefinitions = { default: { ConfirmationModal: { cancel: 'Custom value', confirm: 'Confirm', }, // ... include the rest of the default dictionary ... }, }; const provider = new Render(); await provider.render(ItemsQuoted, { // container props })(block); ``` To avoid missing strings, start from the defaults on this page and change only the keys you need. For multi-language support and advanced patterns, see the [Dictionary customization guide](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/dictionaries/). ## Default keys and values Below are the default English (`en_US`) strings provided by the **Quote Management** drop-in: ```json title="en_US.json" { "ConfirmationModal": { "cancel": "Cancel", "confirm": "Confirm" }, "NegotiableQuote": { "Request": { "title": "Request a Quote", "comment": "Comment", "commentError": "Please add your comment", "quoteName": "Quote name", "quoteNameError": "Please add a quote name", "attachmentsError": "Error uploading attachments", "maxFilesExceeded": "Maximum {maxFiles} file(s) allowed", "maxFileSizeExceeded": "File size exceeds maximum limit of {maxSize}", "invalidFileType": "File type not accepted", "removeFile": "Remove file", "uploading": "Uploading...", "uploadSuccess": "Upload complete", "uploadError": "Upload failed", "requestCta": "Request a Quote", "saveDraftCta": "Save as draft", "error": { "header": "Error", "unauthenticated": "Please sign in to request a quote.", "unauthorized": "You are not authorized to request a quote.", "missingCart": "Could not find a valid cart." }, "success": { "header": "Success", "submitted": "Quote request submitted successfully!", "draftSaved": "Quote saved as draft successfully!" } }, "Manage": { "createdLabel": "Created:", "salesRepLabel": "Sales Rep:", "expiresLabel": "Expires:", "actionsLabel": "Actions", "actions": { "remove": "Remove" }, "attachFile": "Attach File", "attachFiles": "Attach Files", "fileUploadError": "Failed to upload file. Please try again.", "maxFilesExceeded": "Maximum {maxFiles} file(s) allowed", "maxFileSizeExceeded": "File size exceeds maximum limit of {maxSize}", "invalidFileType": "File type not accepted", "removeFile": "Remove file", "uploading": "Uploading...", "uploadSuccess": "Upload complete", "uploadError": "Upload failed", "bannerTitle": "Alert", "bannerStatusMessages": { "submitted": "This quote is currently locked for editing. It will become available once released by the Merchant.", "pending": "This quote is currently locked for editing. It will become available once released by the Merchant.", "expired": "Your quote has expired and the product prices have been updated as per the latest prices in your catalog. You can either re-submit the quote to seller for further negotiation or go to checkout." }, "actionButtons": { "close": "Close quote", "delete": "Delete quote", "print": "Print quote", "createTemplate": "Create quote template", "createCopy": "Create copy", "sendForReview": "Send for review" }, "confirmationModal": { "cancel": "Cancel", "delete": { "title": "Delete Quote", "message": "Are you sure you want to delete this quote?", "confirm": "Delete", "errorHeading": "Error", "errorFallback": "Failed to delete quote", "successHeading": "Success", "successDescription": "Quote has been successfully deleted" }, "duplicate": { "title": "Duplicate Quote", "message": "Are you sure you want to create a copy of this quote?", "confirm": "Create Copy", "errorHeading": "Error", "errorFallback": "Failed to duplicate quote", "successHeading": "Success", "successDescription": "Quote has been successfully duplicated. You will be redirected to the new quote shortly.", "outOfStockWarningHeading": "Alert", "outOfStockWarningMessage": "Some items were skipped during duplication due to errors." }, "close": { "message": "Are you sure you want to close this quote?", "confirm": "Close", "confirmLoading": "Closing...", "successHeading": "Success", "successDescription": "Quote has been successfully closed" }, "createTemplate": { "message": "Are you sure you want to create a quote template from this quote?", "confirm": "Create Template", "confirmLoading": "Creating...", "successHeading": "Success", "successDescription": "Quote template has been successfully created", "errorHeading": "Error", "errorFallback": "Failed to create quote template" }, "noItemsSelected": { "title": "Please Select Quote Items", "message": "Please select at least one quote item to proceed.", "confirm": "Ok" } }, "shippingInformation": { "title": "Shipping Information" }, "shippingAddress": { "noAddress": "No shipping address has been set for this quote.", "noAddressHeading": "No Shipping Address", "noAddressDescription": "Please select or enter a shipping address." }, "quoteComments": { "title": "Quote Comments", "placeholder": "Add your comment", "emptyState": "No comments yet", "by": "by", "attachments": "Attachments:" }, "productListTable": { "headers": { "productName": "Product name", "sku": "SKU", "price": "Price", "quantity": "Quantity", "discount": "Discount", "subtotal": "Subtotal", "actions": "Actions" }, "submitButton": "Update", "actions": { "editNoteToSeller": "Edit note to seller", "remove": "Remove" }, "notes": { "header": "NOTES", "leftANote": "left a note:", "buyer": "Buyer", "seller": "Seller" }, "outOfStock": "Out of Stock", "outOfStockMessage": "This item is currently out of stock." }, "rename": { "title": "Rename Quote", "quoteNameLabel": "Quote name", "reasonLabel": "Reason for change", "renameButton": "Rename", "cancelButton": "Cancel", "errorHeading": "Error", "quoteNameRequired": "Quote name is required", "errorDefault": "Failed to rename quote. Please try again.", "successHeading": "Success", "successMessage": "Quote renamed successfully!" }, "lineItemNote": { "title": "Leave a note to seller", "productLabel": "Name & SKU", "skuLabel": "SKU", "priceLabel": "Price", "stockLabel": "Stock", "quantityLabel": "Qty", "discountLabel": "Discount", "subtotalLabel": "Subtotal", "noteLabel": "Note to seller", "notePlaceholder": "Can I get a discount on this?", "noteHelper": "The seller will see the note when you send the quote back.", "confirmButton": "Confirm", "cancelButton": "Cancel", "noteError": "Please enter a note", "quantityError": "Quantity must be greater than 0" }, "tabbedContent": { "itemsQuoted": "Items quoted", "comments": "Comments", "historyLog": "History log" }, "quotePricesSummary": { "subtotal": { "excludingTax": "Quote Subtotal (excluding tax)" }, "appliedTaxes": "Applied Taxes", "grandTotal": { "includingTax": "Quote Grand Total (including tax)" } }, "updateQuantitiesModal": { "title": "Change Quote Items", "description": "Making changes to any quote item changes the terms of the quote. After you update the quote, return it to the seller for review and approval.", "cancelButton": "Cancel", "updateButton": "Apply Changes", "successHeading": "Success", "successMessage": "Quote quantities have been successfully updated.", "errorHeading": "Error", "errorMessage": "Failed to update quote quantities. Please try again." }, "removeItemsModal": { "title": "Change Quote Items", "description": "Making changes to any quote item changes the terms of the quote. After you update the quote, return it to the seller for review and approval.", "cancelButton": "Cancel", "confirmButton": "Remove", "confirmButtonRemoving": "Removing...", "successHeading": "Success", "successMessage": "Quote items have been successfully removed.", "errorHeading": "Error", "errorMessage": "Failed to remove quote items. Please try again." } }, "PriceSummary": { "taxToBeDetermined": "TBD", "orderSummary": "Order Summary", "giftOptionsTax": { "printedCard": { "title": "Printed card", "inclTax": "Including taxes", "exclTax": "excluding taxes" }, "itemGiftWrapping": { "title": "Item gift wrapping", "inclTax": "Including taxes", "exclTax": "excluding taxes" }, "orderGiftWrapping": { "title": "Order gift wrapping", "inclTax": "Including taxes", "exclTax": "excluding taxes" } }, "subTotal": { "label": "Subtotal", "withTaxes": "Including taxes", "withoutTaxes": "excluding taxes" }, "shipping": { "label": "Shipping", "withTaxes": "Including taxes", "withoutTaxes": "excluding taxes" }, "taxes": { "total": "Tax Total", "totalOnly": "Tax", "breakdown": "Taxes", "showBreakdown": "Show Tax Breakdown", "hideBreakdown": "Hide Tax Breakdown" }, "total": { "free": "Free", "label": "Total", "withoutTax": "Total excluding taxes", "saved": "Total saved" } }, "QuoteSummaryList": { "discountedPrice": "Discounted Price", "discountPercentage": "{discount}% off", "editQuote": "Edit", "file": "{count} file", "files": "{count} files", "heading": "Negotiable Quote ({count})", "listOfQuoteItems": "List of Quote Items", "regularPrice": "Regular Price", "savingsAmount": "Savings", "viewMore": "View more" } }, "NegotiableQuoteTemplate": { "Manage": { "createdLabel": "Created:", "salesRepLabel": "Sales Rep:", "expiresLabel": "Expires:", "templateIdLabel": "Template ID:", "referenceDocuments": { "title": "Reference Documents", "add": "Add", "edit": "Edit", "remove": "Remove", "noReferenceDocuments": "No reference documents", "form": { "title": "Document Information", "documentNameLabel": "Document name", "documentIdentifierLabel": "Document identifier", "referenceUrlLabel": "Reference URL", "addButton": "Add to Quote Template", "updateButton": "Update Document", "cancelButton": "Cancel", "documentNameRequired": "Document name is required", "documentIdentifierRequired": "Document identifier is required", "referenceUrlRequired": "Reference URL is required", "invalidUrl": "Please enter a valid URL", "errorHeading": "Error", "duplicateUidError": "A document with this identifier already exists in the template. Please use a different identifier." } }, "shippingInformation": { "title": "Shipping Information" }, "comments": { "title": "Comments" }, "historyLog": { "title": "History Log" }, "tabs": { "itemsQuoted": "Items Quoted", "comments": "Comments", "historyLog": "History Log" }, "templateComments": { "title": "Template Comments", "placeholder": "Add your comment" }, "actionsLabel": "Actions", "actionButtons": { "sendForReview": "Send for review", "delete": "Delete template", "cancel": "Cancel template", "accept": "Accept", "generateQuote": "Generate quote" }, "removeItemsModal": { "title": "Change Quote Template Items", "description": "Making changes to any quote template item changes the terms of the template. After you update the template, return it to the seller for review and approval.", "cancelButton": "Cancel", "confirmButton": "Remove", "confirmButtonRemoving": "Removing...", "successHeading": "Success", "successMessage": "Quote template items have been successfully removed.", "errorHeading": "Error", "errorMessage": "Failed to remove quote template items. Please try again." }, "updateQuantitiesModal": { "title": "Change Quote Template Items", "description": "Making changes to any quote template item changes the terms of the template. After you update the template, return it to the seller for review and approval.", "cancelButton": "Cancel", "updateButton": "Apply Changes", "successHeading": "Success", "successMessage": "Quote template quantities have been successfully updated.", "errorHeading": "Error", "errorMessage": "Failed to update quote template quantities. Please try again." }, "confirmationModal": { "cancel": "Cancel", "delete": { "title": "Delete Quote Template", "message": "Are you sure you want to delete this quote template?", "confirm": "Delete", "errorHeading": "Error", "errorFallback": "Failed to delete quote template", "successHeading": "Success", "successDescription": "Quote template has been successfully deleted" }, "cancelTemplate": { "title": "Cancel Quote Template", "message": "Are you sure you want to cancel this quote template?", "confirm": "Cancel Template", "errorHeading": "Error", "errorFallback": "Failed to cancel quote template", "successHeading": "Success", "successDescription": "Quote template has been successfully cancelled" }, "accept": { "title": "Accept Quote Template", "message": "Are you sure you want to accept this quote template?", "confirm": "Accept", "confirmLoading": "Accepting...", "successHeading": "Quote Template Accepted", "successDescription": "Quote template has been successfully accepted.", "errorHeading": "Error", "errorFallback": "Failed to accept quote template. Please try again." }, "generateQuote": { "message": "Are you sure you want to generate a quote from this template?", "confirm": "Generate Quote", "confirmLoading": "Generating...", "successHeading": "Quote Generated", "successDescription": "Quote has been successfully generated from the template.", "errorHeading": "Error", "errorFallback": "Failed to generate quote from template. Please try again." } }, "quotePricesSummary": { "subtotal": { "excludingTax": "Quote Template Subtotal (excluding tax)" }, "appliedTaxes": "Applied Taxes", "grandTotal": { "includingTax": "Quote Template Grand Total (including tax)" } }, "lineItemNoteModal": { "errorHeading": "Error" }, "rename": { "title": "Rename Quote Template", "templateNameLabel": "Template name", "reasonLabel": "Reason for change", "renameButton": "Rename", "cancelButton": "Cancel", "errorHeading": "Error", "templateNameRequired": "Template name is required", "errorDefault": "Failed to rename quote template. Please try again.", "successHeading": "Success", "successMessage": "Quote template renamed successfully!" }, "expirationDate": { "title": "Set Expiration Date", "expirationDateLabel": "Expiration date", "saveButton": "Save", "cancelButton": "Cancel", "errorHeading": "Error", "expirationDateRequired": "Expiration date is required", "invalidDate": "Please enter a valid date", "pastDateError": "Expiration date must be in the future", "errorDefault": "Failed to set expiration date. Please try again.", "successHeading": "Success" }, "unsavedChangesWarningHeading": "Unsaved Changes", "unsavedChangesWarningMessage": "The quote template must be submitted for review to save the changes.", "shippingAddressWarningHeading": "No Shipping Address", "shippingAddressWarningMessage": "No shipping address has been set for this quote template." } }, "historyLog": { "changeTypes": { "created": "Quote Created", "updated": "Quote Updated", "statusChanged": "Status Changed", "commentAdded": "Comment Added", "expirationChanged": "Expiration Changed" }, "noteTypes": { "buyerNoteAdded": "Buyer Note Added", "sellerNoteAdded": "Seller Note Added" }, "authorLabels": { "buyer": "(Buyer)", "seller": "(Seller)" }, "changeDetails": { "comment": "Comment: \"{comment}\"", "statusChangedFromTo": "Status changed from {oldStatus} to {newStatus}", "statusSetTo": "Status set to {newStatus}", "expirationChangedFromTo": "Expiration changed from {oldExpiration} to {newExpiration}", "expirationSetTo": "Expiration set to {newExpiration}", "totalChangedFromTo": "Total changed from {oldTotal} to {newTotal}", "customChange": "{title}: changed from \"{oldValue}\" to \"{newValue}\"", "productsRemovedFromCatalog": "Products removed from catalog: {products}", "productsRemovedFromQuote": "Products removed from quote: {products}", "noDetailsAvailable": "No details available" }, "emptyState": "No history available for this quote." }, "QuoteManagement": { "QuotesListTable": { "quoteName": "Quote Name", "created": "Created", "createdBy": "Created By", "status": "Status", "lastUpdated": "Last Updated", "quoteTemplate": "Quote Template", "quoteTotal": "Quote Total", "actions": "Action" }, "QuoteTemplatesListTable": { "name": "Template Name", "state": "State", "status": "Status", "validUntil": "Valid Until", "minQuoteTotal": "Min. Quote Total (Negotiated)", "ordersPlaced": "Orders Placed", "lastOrdered": "Last Ordered", "actions": "Action", "view": "View" } } } ``` --- # Quote Management Data & Events The **Quote Management** drop-in uses the [event bus](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/events/) to emit and listen to events for communication between drop-ins and external integrations. Version: 1.2.0 ## Events reference {/* EVENTS_TABLE_START */} | Event | Direction | Description | |-------|-----------|-------------| | [quote-management/file-upload-error](#quote-managementfile-upload-error-emits) | Emits | Emitted when a specific condition or state change occurs. | | [quote-management/line-item-note-set](#quote-managementline-item-note-set-emits) | Emits | Emitted when a specific condition or state change occurs. | | [quote-management/negotiable-quote-delete-error](#quote-managementnegotiable-quote-delete-error-emits) | Emits | Emitted when a specific condition or state change occurs. | | [quote-management/negotiable-quote-deleted](#quote-managementnegotiable-quote-deleted-emits) | Emits | Emitted when a specific condition or state change occurs. | | [quote-management/negotiable-quote-requested](#quote-managementnegotiable-quote-requested-emits) | Emits | Emitted when a specific condition or state change occurs. | | [quote-management/quote-data/error](#quote-managementquote-dataerror-emits) | Emits | Emitted when an error occurs. | | [quote-management/quote-data/initialized](#quote-managementquote-datainitialized-emits) | Emits | Emitted when the component completes initialization. | | [quote-management/quote-template-data/error](#quote-managementquote-template-dataerror-emits) | Emits | Emitted when an error occurs. | | [quote-management/quote-template-deleted](#quote-managementquote-template-deleted-emits) | Emits | Emitted when a specific condition or state change occurs. | | [quote-management/quote-template-generated](#quote-managementquote-template-generated-emits) | Emits | Emitted when a specific condition or state change occurs. | | [quote-management/quote-templates-data](#quote-managementquote-templates-data-emits) | Emits | Emitted when a specific condition or state change occurs. | | [auth/permissions](#authpermissions-listens) | Listens | Fired by Auth (`auth`) when permissions are updated. | | [checkout/updated](#checkoutupdated-listens) | Listens | Fired by Checkout (`checkout`) when the component state is updated. | | [quote-management/initialized](#quote-managementinitialized-emits-and-listens) | Emits and listens | Emitted when the component completes initialization. | | [quote-management/negotiable-quote-close-error](#quote-managementnegotiable-quote-close-error-emits-and-listens) | Emits and listens | Emitted and consumed for internal and external communication. | | [quote-management/negotiable-quote-closed](#quote-managementnegotiable-quote-closed-emits-and-listens) | Emits and listens | Emitted and consumed for internal and external communication. | | [quote-management/permissions](#quote-managementpermissions-emits-and-listens) | Emits and listens | Emitted when permissions are updated. | | [quote-management/quantities-updated](#quote-managementquantities-updated-emits-and-listens) | Emits and listens | Emitted and consumed for internal and external communication. | | [quote-management/quote-data](#quote-managementquote-data-emits-and-listens) | Emits and listens | Emitted and consumed for internal and external communication. | | [quote-management/quote-duplicated](#quote-managementquote-duplicated-emits-and-listens) | Emits and listens | Emitted and consumed for internal and external communication. | | [quote-management/quote-items-removed](#quote-managementquote-items-removed-emits-and-listens) | Emits and listens | Emitted and consumed for internal and external communication. | | [quote-management/quote-renamed](#quote-managementquote-renamed-emits-and-listens) | Emits and listens | Emitted and consumed for internal and external communication. | | [quote-management/quote-sent-for-review](#quote-managementquote-sent-for-review-emits-and-listens) | Emits and listens | Emitted and consumed for internal and external communication. | | [quote-management/quote-template-data](#quote-managementquote-template-data-emits-and-listens) | Emits and listens | Emitted and consumed for internal and external communication. | | [quote-management/shipping-address-set](#quote-managementshipping-address-set-emits-and-listens) | Emits and listens | Emitted and consumed for internal and external communication. | {/* EVENTS_TABLE_END */} ## Event details The following sections provide detailed information about each event, including its direction, event payload, and usage examples. ### `auth/permissions` (listens) Fired by Auth (`auth`) when Adobe Commerce permissions are updated. #### Event payload ```typescript AuthPermissionsPayload ``` #### Example ```js events.on('auth/permissions', (payload) => { console.log('auth/permissions event received:', payload); // Add your custom logic here }); ``` ### `checkout/updated` (listens) Fired by Checkout (`checkout`) when the component state is updated. Quote Management uses it to reload quote data when the checkout type is `quote`. #### Event payload #### Example ```js events.on('checkout/updated', (payload) => { console.log('checkout/updated event received:', payload); // Add your custom logic here }); ``` ### `quote-management/file-upload-error` (emits) Emitted when `uploadFile()` fails to upload or finalize a file attachment. #### Event payload ```typescript { error: string; fileName?: string; } ``` #### Example ```js events.on('quote-management/file-upload-error', (payload) => { console.log('quote-management/file-upload-error event received:', payload); // Add your custom logic here }); ``` ### `quote-management/initialized` (emits and listens) Emitted when the Quote Management initializer finishes loading store configuration. #### Event payload ```typescript { config: StoreConfigModel; } ``` See [`StoreConfigModel`](#storeconfigmodel) for full type definition. #### Example ```js events.on('quote-management/initialized', (payload) => { console.log('quote-management/initialized event received:', payload); // Add your custom logic here }); ``` ### `quote-management/line-item-note-set` (emits) Emitted after `setLineItemNote()` updates a line item note for a negotiable quote. #### Event payload ```typescript { quote: NegotiableQuoteModel; input: { quoteUid: string; itemUid: string; note: string; quantity?: number; } } ``` See [`NegotiableQuoteModel`](#negotiablequotemodel) for full type definition. #### Example ```js events.on('quote-management/line-item-note-set', (payload) => { console.log('quote-management/line-item-note-set event received:', payload); // Add your custom logic here }); ``` ### `quote-management/negotiable-quote-close-error` (emits and listens) Emitted when `closeNegotiableQuote()` fails to close one or more negotiable quotes. #### Event payload ```typescript { error: Error; attemptedQuoteUids: string[]; } ``` #### Example ```js events.on('quote-management/negotiable-quote-close-error', (payload) => { console.log('quote-management/negotiable-quote-close-error event received:', payload); // Add your custom logic here }); ``` ### `quote-management/negotiable-quote-closed` (emits and listens) Emitted after `closeNegotiableQuote()` closes one or more negotiable quotes. #### Event payload ```typescript { closedQuoteUids: string[]; resultStatus: string; } ``` #### Example ```js events.on('quote-management/negotiable-quote-closed', (payload) => { console.log('quote-management/negotiable-quote-closed event received:', payload); // Add your custom logic here }); ``` ### `quote-management/negotiable-quote-delete-error` (emits) Emitted when `deleteQuote()` fails to delete one or more negotiable quotes. #### Event payload ```typescript { error: Error; attemptedQuoteUids: string[]; } ``` #### Example ```js events.on('quote-management/negotiable-quote-delete-error', (payload) => { console.log('quote-management/negotiable-quote-delete-error event received:', payload); // Add your custom logic here }); ``` ### `quote-management/negotiable-quote-deleted` (emits) Emitted after `deleteQuote()` deletes one or more negotiable quotes. #### Event payload ```typescript { deletedQuoteUids: string[]; resultStatus: string; } ``` #### Example ```js events.on('quote-management/negotiable-quote-deleted', (payload) => { console.log('quote-management/negotiable-quote-deleted event received:', payload); // Add your custom logic here }); ``` ### `quote-management/negotiable-quote-requested` (emits) Emitted after `requestNegotiableQuote()` creates a negotiable quote from a cart. #### Event payload ```typescript { quote: NegotiableQuoteModel | null; input: { cartId: string; quoteName: string; comment?: string; attachments?: { key: string }[]; isDraft?: boolean; } } ``` See [`NegotiableQuoteModel`](#negotiablequotemodel) for full type definition. #### Example ```js events.on('quote-management/negotiable-quote-requested', (payload) => { console.log('quote-management/negotiable-quote-requested event received:', payload); // Add your custom logic here }); ``` ### `quote-management/permissions` (emits and listens) Emitted when the permissions state for Quote Management changes (for example, after `a`uth/permission`s` is processed or on logout). #### Event payload ```typescript typeof state.permissions ``` #### Example ```js events.on('quote-management/permissions', (payload) => { console.log('quote-management/permissions event received:', payload); // Add your custom logic here }); ``` ### `quote-management/quantities-updated` (emits and listens) Emitted after `updateQuantities()` updates item quantities in a negotiable quote. #### Event payload ```typescript { quote: NegotiableQuoteModel; input: { quoteUid: string; items: Array<{ quoteItemUid: string; quantity: number }>; } } ``` See [`NegotiableQuoteModel`](#negotiablequotemodel) for full type definition. #### Example ```js events.on('quote-management/quantities-updated', (payload) => { console.log('quote-management/quantities-updated event received:', payload); // Add your custom logic here }); ``` ### `quote-management/quote-data` (emits and listens) Emitted when negotiable quote data is loaded or updated. #### Event payload ```typescript { quote: NegotiableQuoteModel; permissions: typeof state.permissions; } ``` See [`NegotiableQuoteModel`](#negotiablequotemodel) for full type definition. #### Example ```js events.on('quote-management/quote-data', (payload) => { console.log('quote-management/quote-data event received:', payload); // Add your custom logic here }); ``` ### `quote-management/quote-data/error` (emits) Emitted when Quote Management fails to load negotiable quote data during initialization. #### Event payload ```typescript { error: Error; } ``` #### Example ```js events.on('quote-management/quote-data/error', (payload) => { console.log('quote-management/quote-data/error event received:', payload); // Add your custom logic here }); ``` ### `quote-management/quote-data/initialized` (emits) Emitted the first time Quote Management successfully loads negotiable quote data during initialization. #### Event payload ```typescript { quote: NegotiableQuoteModel; permissions: typeof state.permissions; } ``` See [`NegotiableQuoteModel`](#negotiablequotemodel) for full type definition. #### Example ```js events.on('quote-management/quote-data/initialized', (payload) => { console.log('quote-management/quote-data/initialized event received:', payload); // Add your custom logic here }); ``` ### `quote-management/quote-duplicated` (emits and listens) Emitted after `duplicateQuote()` creates a copy of a negotiable quote. #### Event payload ```typescript { quote: NegotiableQuoteModel; input: { quoteUid: string; duplicatedQuoteUid: string; } hasOutOfStockItems?: boolean; } ``` See [`NegotiableQuoteModel`](#negotiablequotemodel) for full type definition. #### Example ```js events.on('quote-management/quote-duplicated', (payload) => { console.log('quote-management/quote-duplicated event received:', payload); // Add your custom logic here }); ``` ### `quote-management/quote-items-removed` (emits and listens) Emitted after `removeNegotiableQuoteItems()` removes one or more items from a negotiable quote. #### Event payload ```typescript { quote: NegotiableQuoteModel; removedItemUids: string[]; input: RemoveNegotiableQuoteItemsInput; } ``` See [`NegotiableQuoteModel`](#negotiablequotemodel) for full type definition. #### Example ```js events.on('quote-management/quote-items-removed', (payload) => { console.log('quote-management/quote-items-removed event received:', payload); // Add your custom logic here }); ``` ### `quote-management/quote-renamed` (emits and listens) Emitted after `renameNegotiableQuote()` updates the name (and optional comment) of a negotiable quote. #### Event payload ```typescript { quote: NegotiableQuoteModel; input: { quoteUid: string; quoteName: string; quoteComment?: string; } } ``` See [`NegotiableQuoteModel`](#negotiablequotemodel) for full type definition. #### Example ```js events.on('quote-management/quote-renamed', (payload) => { console.log('quote-management/quote-renamed event received:', payload); // Add your custom logic here }); ``` ### `quote-management/quote-sent-for-review` (emits and listens) Emitted after `sendForReview()` submits a negotiable quote for merchant review. #### Event payload ```typescript { quote: NegotiableQuoteModel; input: { quoteUid: string; comment?: string; attachments?: { key: string }[]; } } ``` See [`NegotiableQuoteModel`](#negotiablequotemodel) for full type definition. #### Example ```js events.on('quote-management/quote-sent-for-review', (payload) => { console.log('quote-management/quote-sent-for-review event received:', payload); // Add your custom logic here }); ``` ### `quote-management/quote-template-data` (emits and listens) Emitted when quote template data is loaded or updated. #### Event payload ```typescript { quoteTemplate: NegotiableQuoteTemplateModel; permissions: typeof state.permissions; } ``` See [`NegotiableQuoteTemplateModel`](#negotiablequotetemplatemodel) for full type definition. #### Example ```js events.on('quote-management/quote-template-data', (payload) => { console.log('quote-management/quote-template-data event received:', payload); // Add your custom logic here }); ``` ### `quote-management/quote-template-data/error` (emits) Emitted when Quote Management fails to load quote template data during initialization. #### Event payload ```typescript { error: Error; } ``` #### Example ```js events.on('quote-management/quote-template-data/error', (payload) => { console.log('quote-management/quote-template-data/error event received:', payload); // Add your custom logic here }); ``` ### `quote-management/quote-template-deleted` (emits) Emitted after `deleteQuoteTemplate()` deletes a quote template. #### Event payload ```typescript { templateId: string; } ``` #### Example ```js events.on('quote-management/quote-template-deleted', (payload) => { console.log('quote-management/quote-template-deleted event received:', payload); // Add your custom logic here }); ``` ### `quote-management/quote-template-generated` (emits) Emitted after `generateQuoteFromTemplate()` creates a new negotiable quote from a template. #### Event payload ```typescript { quoteId: string; } ``` #### Example ```js events.on('quote-management/quote-template-generated', (payload) => { console.log('quote-management/quote-template-generated event received:', payload); // Add your custom logic here }); ``` ### `quote-management/quote-templates-data` (emits) Emitted after `getQuoteTemplates()` loads the quote templates list. #### Event payload ```typescript { quoteTemplates: NegotiableQuoteTemplatesListModel; permissions: typeof state.permissions; } ``` See [`NegotiableQuoteTemplatesListModel`](#negotiablequotetemplateslistmodel) for full type definition. #### Example ```js events.on('quote-management/quote-templates-data', (payload) => { console.log('quote-management/quote-templates-data event received:', payload); // Add your custom logic here }); ``` ### `quote-management/shipping-address-set` (emits and listens) Emitted after `setShippingAddress()` updates the shipping address for a negotiable quote. #### Event payload ```typescript { quote: NegotiableQuoteModel; input: { quoteUid: string; addressId?: number; addressData?: AddressInput; } } ``` See [`NegotiableQuoteModel`](#negotiablequotemodel) for full type definition. #### Example ```js events.on('quote-management/shipping-address-set', (payload) => { console.log('quote-management/shipping-address-set event received:', payload); // Add your custom logic here }); ``` ## Data Models The following data models are used in event payloads for this drop-in. ### NegotiableQuoteModel Used in: [`quote-management/line-item-note-set`](#quote-managementline-item-note-set-emits), [`quote-management/negotiable-quote-requested`](#quote-managementnegotiable-quote-requested-emits), [`quote-management/quantities-updated`](#quote-managementquantities-updated-emits-and-listens), [`quote-management/quote-data`](#quote-managementquote-data-emits-and-listens), [`quote-management/quote-data/initialized`](#quote-managementquote-datainitialized-emits), [`quote-management/quote-duplicated`](#quote-managementquote-duplicated-emits-and-listens), [`quote-management/quote-items-removed`](#quote-managementquote-items-removed-emits-and-listens), [`quote-management/quote-renamed`](#quote-managementquote-renamed-emits-and-listens), [`quote-management/quote-sent-for-review`](#quote-managementquote-sent-for-review-emits-and-listens), [`quote-management/shipping-address-set`](#quote-managementshipping-address-set-emits-and-listens). ```ts interface NegotiableQuoteModel { uid: string; name: string; createdAt: string; salesRepName: string; expirationDate: string; updatedAt: string; status: NegotiableQuoteStatus; isVirtual: boolean; buyer: { firstname: string; lastname: string; }; email?: string; templateName?: string; totalQuantity: number; comments?: { uid: string; createdAt: string; author: { firstname: string; lastname: string; }; text: string; attachments?: { name: string; url: string; }[]; }[]; history?: NegotiableQuoteHistoryEntry[]; prices: { appliedDiscounts?: Discount[]; appliedTaxes?: Tax[]; discount?: Currency; grandTotal?: Currency; grandTotalExcludingTax?: Currency; shippingExcludingTax?: Currency; shippingIncludingTax?: Currency; subtotalExcludingTax?: Currency; subtotalIncludingTax?: Currency; subtotalWithDiscountExcludingTax?: Currency; totalTax?: Currency; }; items: CartItemModel[]; shippingAddresses?: ShippingAddress[]; canCheckout: boolean; canSendForReview: boolean; lockedForEditing?: boolean; canDelete: boolean; canClose: boolean; canUpdateQuote: boolean; readOnly: boolean; } ``` ### NegotiableQuoteTemplateModel Used in: [`quote-management/quote-template-data`](#quote-managementquote-template-data-emits-and-listens). ```ts interface NegotiableQuoteTemplateModel { id: string; uid: string; name: string; createdAt: string; updatedAt: string; expirationDate?: string; status: NegotiableQuoteTemplateStatus; salesRepName: string; buyer: { firstname: string; lastname: string; }; comments?: QuoteTemplateComment[]; history?: NegotiableQuoteHistoryEntry[]; prices: { subtotalExcludingTax?: Currency; subtotalIncludingTax?: Currency; subtotalWithDiscountExcludingTax?: Currency; grandTotal?: Currency; appliedTaxes?: { amount: Currency; label: string; }[]; }; items: CartItemModel[]; shippingAddresses?: ShippingAddress[]; referenceDocuments?: { uid: string; name: string; identifier?: string; url: string; }[]; // Template-specific fields quantityThresholds?: { min?: number; max?: number; }; canAccept: boolean; canDelete: boolean; canReopen: boolean; canCancel: boolean; canSendForReview: boolean; canGenerateQuoteFromTemplate: boolean; canEditTemplateItems: boolean; } ``` ### NegotiableQuoteTemplatesListModel Used in: [`quote-management/quote-templates-data`](#quote-managementquote-templates-data-emits). ```ts interface NegotiableQuoteTemplatesListModel { items: NegotiableQuoteTemplateListEntry[]; pageInfo: { currentPage: number; pageSize: number; totalPages: number; }; totalCount: number; paginationInfo?: PaginationInfo; sortFields?: { default: string; options: Array<{ label: string; value: string; }>; }; } ``` ### StoreConfigModel Used in: [`quote-management/initialized`](#quote-managementinitialized-emits-and-listens). ```ts interface StoreConfigModel { quoteSummaryDisplayTotal: number; quoteSummaryMaxItems: number; quoteDisplaySettings: { zeroTax: boolean; subtotal: QuoteDisplayAmount; price: QuoteDisplayAmount; shipping: QuoteDisplayAmount; fullSummary: boolean; grandTotal: boolean; }; useConfigurableParentThumbnail: boolean; quoteMinimumAmount: number | null; quoteMinimumAmountMessage: string | null; } ``` --- # Quote Management Functions The Quote Management drop-in provides API functions for managing negotiable quotes and quote templates, including creating quote requests, working with quote templates, managing items and quantities, and handling attachments. Version: 1.2.0 | Function | Description | | --- | --- | | [`acceptQuoteTemplate`](#acceptquotetemplate) | Accepts a negotiable quote template. | | [`addQuoteTemplateLineItemNote`](#addquotetemplatelineitemnote) | Adds a buyer's note to a specific item in a negotiable quote template. | | [`addQuoteTemplateShippingAddress`](#addquotetemplateshippingaddress) | Assigns a shipping address to a negotiable quote template. | | [`cancelQuoteTemplate`](#cancelquotetemplate) | Cancels a negotiable quote template. | | [`closeNegotiableQuote`](#closenegotiablequote) | Closes one or more negotiable quotes and emits success or error events with operation results. | | [`createQuoteTemplate`](#createquotetemplate) | Creates a new negotiable quote template from an existing quote. | | [`deleteQuote`](#deletequote) | Deletes one or more negotiable quotes. | | [`deleteQuoteTemplate`](#deletequotetemplate) | Permanently deletes a negotiable quote template. | | [`duplicateQuote`](#duplicatequote) | Creates a copy of a negotiable quote and emits an event with the duplicated quote data. | | [`generateQuoteFromTemplate`](#generatequotefromtemplate) | Generates a negotiable quote from an accepted quote template. | | [`getQuoteData`](#getquotedata) | Retrieves negotiable quote details by ID and emits an event with the latest quote data. | | [`getQuoteTemplateData`](#getquotetemplatedata) | Fetches negotiable quote template data by template ID. | | [`getQuoteTemplates`](#getquotetemplates) | Retrieves the list of negotiable quote templates for the authenticated customer and emits an event with the template list. | | [`getStoreConfig`](#getstoreconfig) | Retrieves store configuration used by Quote Management. | | [`negotiableQuotes`](#negotiablequotes) | Retrieves the list of negotiable quotes for the authenticated customer. | | [`openQuoteTemplate`](#openquotetemplate) | Opens an existing negotiable quote template. | | [`removeNegotiableQuoteItems`](#removenegotiablequoteitems) | Removes one or more items from a negotiable quote and emits an event with the updated quote data. | | [`removeQuoteTemplateItems`](#removequotetemplateitems) | Removes one or more products from an existing negotiable quote template. | | [`renameNegotiableQuote`](#renamenegotiablequote) | Renames a negotiable quote. | | [`requestNegotiableQuote`](#requestnegotiablequote) | Creates a new negotiable quote request from the current cart. | | [`sendForReview`](#sendforreview) | Submits a negotiable quote for review by the seller. | | [`sendQuoteTemplateForReview`](#sendquotetemplateforreview) | Submits a negotiable quote template for review by the seller. | | [`setQuoteTemplateExpirationDate`](#setquotetemplateexpirationdate) | Sets the expiration date for a negotiable quote template. | | [`setLineItemNote`](#setlineitemnote) | Sets a note for a specific negotiable quote line item and emits events with the updated quote data. | | [`setShippingAddress`](#setshippingaddress) | Sets or updates the shipping address for a negotiable quote. | | [`updateQuantities`](#updatequantities) | Updates the quantities of items in a negotiable quote. | | [`updateQuoteTemplateItemQuantities`](#updatequotetemplateitemquantities) | Changes the quantity of one or more items in an existing negotiable quote template. | | [`uploadFile`](#uploadfile) | Uploads a file attachment and returns a key for associating the file with a quote or quote template. | ## acceptQuoteTemplate Accepts a negotiable quote template. This action finalizes the acceptance of a quote template from the buyer's side, returns the updated template data on success, and emits an event for successful acceptance. ```ts const acceptQuoteTemplate = async ( params: AcceptQuoteTemplateParams ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `params` | `AcceptQuoteTemplateParams` | Yes | An object of type \`AcceptQuoteTemplateParams\` containing the template UID and acceptance details. See the type definition for available fields. | ### Events Emits the [`quote-management/quote-template-data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-template-data-emits-and-listens) event. ### Returns Returns [`NegotiableQuoteModel`](#negotiablequotemodel). ## addQuoteTemplateLineItemNote Adds a buyer's note to a specific item in a negotiable quote template. This allows buyers to provide additional information or special instructions for individual line items. ```ts const addQuoteTemplateLineItemNote = async ( params: AddQuoteTemplateLineItemNoteParams ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `params` | `AddQuoteTemplateLineItemNoteParams` | Yes | An object of type \`AddQuoteTemplateLineItemNoteParams\` containing the template UID, item UID, and note text to add to a specific line item. | ### Events Emits the [`quote-management/quote-template-data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-template-data-emits-and-listens) event. ### Returns Returns `void`. ## addQuoteTemplateShippingAddress Assigns a shipping address to a negotiable quote template. This can be either a previously-defined customer address (by ID) or a new address provided with full details. ```ts const addQuoteTemplateShippingAddress = async ( params: AddQuoteTemplateShippingAddressParams ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `params` | `AddQuoteTemplateShippingAddressParams` | Yes | An object of type \`AddQuoteTemplateShippingAddressParams\` containing the template UID and shipping address details (street, city, region, postal code, country). | ### Events Emits the [`quote-management/quote-template-data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-template-data-emits-and-listens) event. ### Returns Returns `void`. ## cancelQuoteTemplate Cancels a negotiable quote template. This action allows buyers to cancel a quote template they no longer need, with an optional comment to provide the reason for cancellation. Returns the updated template data on success and emits an event for successful cancellation. ```ts const cancelQuoteTemplate = async ( params: CancelQuoteTemplateParams ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `params` | `CancelQuoteTemplateParams` | Yes | An object of type \`CancelQuoteTemplateParams\` containing the template UID to cancel. This moves the quote template to canceled status. | ### Events Emits the [`quote-management/quote-template-data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-template-data-emits-and-listens) event. ### Returns Returns `void`. ## closeNegotiableQuote Closes one or more negotiable quotes and emits success or error events with operation results. ```ts const closeNegotiableQuote = async ( input: CloseNegotiableQuoteInput ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | [`CloseNegotiableQuoteInput`](#closenegotiablequoteinput) | Yes | An object containing an array of quote UIDs to close. Required field: `quoteUids` (array of strings, must not be empty). | ### Events Emits the following events: [`quote-management/negotiable-quote-close-error`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementnegotiable-quote-close-error-emits-and-listens), [`quote-management/negotiable-quote-closed`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementnegotiable-quote-closed-emits-and-listens). ### Returns Returns `CloseNegotiableQuoteResult`. ## createQuoteTemplate Creates a new negotiable quote template from an existing quote. Returns the newly created template data on success and emits an event with the template data and user permissions. ```ts const createQuoteTemplate = async ( quoteId: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `quoteId` | `string` | Yes | The unique identifier for the negotiable quote to convert into a reusable template. Creates a template that can be used to generate similar quotes in the future. | ### Events Emits the [`quote-management/quote-template-data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-template-data-emits-and-listens) event. ### Returns Returns [`NegotiableQuoteTemplateModel`](#negotiablequotetemplatemodel) or `null`. ## deleteQuote Deletes one or more negotiable quotes. Deleted quotes become invisible from both the Admin and storefront. On success, it emits an event with the deleted quote UIDs. ```ts const deleteQuote = async ( quoteUids: string[] | string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `quoteUids` | `string[] \| string` | Yes | One or more negotiable quote unique identifiers to delete. Can be a single UID string or an array of UIDs for batch deletion. This permanently removes the quotes. | ### Events Emits the following events: [`quote-management/negotiable-quote-delete-error`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementnegotiable-quote-delete-error-emits), [`quote-management/negotiable-quote-deleted`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementnegotiable-quote-deleted-emits). ### Returns Returns `DeleteQuoteOutput`. ## deleteQuoteTemplate Permanently deletes a negotiable quote template. This action removes the template from the system, returns a success result, and emits an event for successful deletion. This operation is irreversible. ```ts const deleteQuoteTemplate = async ( params: DeleteQuoteTemplateParams ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `params` | `DeleteQuoteTemplateParams` | Yes | An object of type \`DeleteQuoteTemplateParams\` containing the template UID to delete. This permanently removes the quote template. | ### Events Emits the [`quote-management/quote-template-deleted`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-template-deleted-emits) event. ### Returns Returns `void`. ## duplicateQuote Creates a copy of a negotiable quote and emits an event with the duplicated quote data. ### Signature ```typescript function duplicateQuote(input: DuplicateQuoteInput): Promise ``` ### Parameters | Parameter | Type | Required | Description | |---|---|---|---| | `input` | [`DuplicateQuoteInput`](#duplicatequoteinput) | Yes | An object containing the original quote UID, duplicated quote UID, and optional out-of-stock flag. Required fields: `quoteUid` (string), `duplicatedQuoteUid` (string). Optional field: `hasOutOfStockItems` (boolean). | ### Returns Returns `Promise`. ### Events Emits the [`quote-management/quote-duplicated`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-duplicated-emits-and-listens) event. --- ## generateQuoteFromTemplate Generates a negotiable quote from an accepted quote template. This creates a new quote based on the template's configuration and items. ```ts const generateQuoteFromTemplate = async ( params: GenerateQuoteFromTemplateParams ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `params` | `GenerateQuoteFromTemplateParams` | Yes | An object of type \`GenerateQuoteFromTemplateParams\` containing the template UID and any customization parameters. Creates a new negotiable quote based on the template structure. | ### Events Emits the [`quote-management/quote-template-generated`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-template-generated-emits) event. ### Returns Returns `void`. ## getQuoteData Retrieves negotiable quote details by ID and emits an event with the latest quote data. ```ts const getQuoteData = async ( quoteId: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `quoteId` | `string` | Yes | The unique identifier for the negotiable quote to retrieve. Returns complete quote details including items, prices, history, comments, and negotiation status. | ### Events Emits the [`quote-management/quote-data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-data-emits-and-listens) event. ### Returns Returns `void`. ## getQuoteTemplateData Fetches negotiable quote template data by template ID. Returns the transformed template data on success and emits an event with the template data and user permissions. ```ts const getQuoteTemplateData = async ( templateId: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `templateId` | `string` | Yes | The unique identifier for the quote template to retrieve. Returns template details including structure, items, and configuration settings. | ### Events Emits the [`quote-management/quote-template-data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-template-data-emits-and-listens) event. ### Returns Returns [`NegotiableQuoteTemplateModel`](#negotiablequotetemplatemodel) or `null`. ## getQuoteTemplates Retrieves the list of negotiable quote templates for the authenticated customer and emits an event with the template list. ```ts const getQuoteTemplates = async ( params: GetQuoteTemplatesParams = {} ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `params` | `GetQuoteTemplatesParams` | No | An optional object of type \`GetQuoteTemplatesParams\` containing pagination and filter criteria (currentPage, pageSize, filter). Omit to retrieve all templates with default pagination. | ### Events Emits the [`quote-management/quote-templates-data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-templates-data-emits) event. ### Returns Returns [`NegotiableQuoteTemplatesListModel`](#negotiablequotetemplateslistmodel). ## getStoreConfig Retrieves store configuration used by Quote Management. ```ts const getStoreConfig = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns [`StoreConfigModel`](#storeconfigmodel). ## negotiableQuotes Retrieves the list of negotiable quotes for the authenticated customer. ```ts const negotiableQuotes = async ( params: NegotiableQuotesParams = {} ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `params` | `NegotiableQuotesParams` | No | An optional object of type \`NegotiableQuotesParams\` containing pagination and filter criteria (currentPage, pageSize, filter). Omit to retrieve all negotiable quotes with default pagination. | ### Events Does not emit any drop-in events. ### Returns Returns [`NegotiableQuotesListModel`](#negotiablequoteslistmodel). ## openQuoteTemplate Opens an existing negotiable quote template. This action allows buyers to reopen a quote template for viewing or editing, returns the updated template data on success, and emits an event with the template data. ```ts const openQuoteTemplate = async ( params: OpenQuoteTemplateParams ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `params` | `OpenQuoteTemplateParams` | Yes | An object of type \`OpenQuoteTemplateParams\` containing the template UID to open or activate. This makes the template available for generating quotes. | ### Events Emits the [`quote-management/quote-template-data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-template-data-emits-and-listens) event. ### Returns Returns `void`. ## removeNegotiableQuoteItems Removes one or more items from a negotiable quote and emits an event with the updated quote data. ```ts const removeNegotiableQuoteItems = async ( input: RemoveNegotiableQuoteItemsInput ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | [`RemoveNegotiableQuoteItemsInput`](#removenegotiablequoteitemsinput) | Yes | An object containing the quote UID and an array of item UIDs to remove. Required fields: `quoteUid` (string), `quoteItemUids` (array of strings, must not be empty). | ### Events Emits the [`quote-management/quote-items-removed`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-items-removed-emits-and-listens) event. ### Returns Returns [`NegotiableQuoteModel`](#negotiablequotemodel) or `null`. ## removeQuoteTemplateItems Removes one or more products from an existing negotiable quote template. This allows you to delete items from a template by providing their unique identifiers. ```ts const removeQuoteTemplateItems = async ( params: RemoveQuoteTemplateItemsParams ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `params` | `RemoveQuoteTemplateItemsParams` | Yes | An object of type \`RemoveQuoteTemplateItemsParams\` containing the template UID and an array of item UIDs to remove from the template. | ### Events Emits the [`quote-management/quote-template-data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-template-data-emits-and-listens) event. ### Returns Returns `void`. ## renameNegotiableQuote Renames a negotiable quote. It supports renaming quotes with or without a comment explaining the reason for the rename, returns the updated quote data on success, and emits an event for successful renames. ```ts const renameNegotiableQuote = async ( input: RenameNegotiableQuoteInput ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | [`RenameNegotiableQuoteInput`](#renamenegotiablequoteinput) | Yes | An object containing the quote UID, new quote name, and optional comment. Required fields: `quoteUid` (string), `quoteName` (string). Optional field: `quoteComment` (string). | ### Events Emits the [`quote-management/quote-renamed`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-renamed-emits-and-listens) event. ### Returns Returns [`NegotiableQuoteModel`](#negotiablequotemodel) or `null`. ## requestNegotiableQuote Creates a new negotiable quote request from the current cart. This initiates the quote negotiation workflow, converting cart items into a quote that can be reviewed and negotiated by the seller. ```ts const requestNegotiableQuote = async ( input: RequestNegotiableQuoteInput ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | [`RequestNegotiableQuoteInput`](#requestnegotiablequoteinput) | Yes | An object containing the cart ID, quote name, comment, optional draft flag, and optional file attachments. Required fields: `cartId` (string), `quoteName` (string), `comment` (string). Optional fields: `isDraft` (boolean), `attachments` (array of objects with `key` property). | ### Events Emits the [`quote-management/negotiable-quote-requested`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementnegotiable-quote-requested-emits) event. ### Returns Returns [`NegotiableQuoteModel`](#negotiablequotemodel) or `null`. ## sendForReview Submits a negotiable quote for review by the seller. It supports submitting quotes with or without a comment, returns the updated quote data on success, and emits an event for successful submissions. ```ts const sendForReview = async ( input: SendForReviewInput ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | [`SendForReviewInput`](#sendforreviewinput) | Yes | An object containing the quote UID and optional comment and attachments. Required field: `quoteUid` (string). Optional fields: `comment` (string), `attachments` (array of objects with `key` property). | ### Events Emits the [`quote-management/quote-sent-for-review`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-sent-for-review-emits-and-listens) event. ### Returns Returns [`NegotiableQuoteModel`](#negotiablequotemodel) or `null`. ## sendQuoteTemplateForReview Submits a negotiable quote template for review by the seller. It supports submitting templates with optional name, comment, and reference document links, returns the updated template data on success, and emits an event for successful submissions. ```ts const sendQuoteTemplateForReview = async ( params: SendQuoteTemplateForReviewParams ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `params` | `SendQuoteTemplateForReviewParams` | Yes | An object of type \`SendQuoteTemplateForReviewParams\` containing the template UID and optional review notes. Submits the template for review by the seller or approver. | ### Events Emits the [`quote-management/quote-template-data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-template-data-emits-and-listens) event. ### Returns Returns `void`. ## setQuoteTemplateExpirationDate Sets the expiration date for a negotiable quote template. The function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/b2b/negotiable-quote/mutations/set-quote-template-expiration-date/ mutation. ```ts const setQuoteTemplateExpirationDate = async ( params: SetQuoteTemplateExpirationDateParams ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `params` | [`SetQuoteTemplateExpirationDateParams`](#setquotetemplateexpirationdateparams) | Yes | An object containing the `templateId` (string) of the quote template and the `expirationDate` (string) to set. | ### Events Does not emit any drop-in events. ### Returns Returns [`NegotiableQuoteTemplateModel`](#negotiablequotetemplatemodel) or `null`. ## setLineItemNote Sets a note for a specific negotiable quote line item and emits events with the updated quote data. ```ts const setLineItemNote = async ( input: SetLineItemNoteInput ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | [`SetLineItemNoteInput`](#setlineitemnoteinput) | Yes | An object containing the quote UID, item UID, note text, and optional quantity. Required fields: `quoteUid` (string), `itemUid` (string), `note` (string). Optional field: `quantity` (number). | ### Events Emits the following events: [`quote-management/line-item-note-set`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementline-item-note-set-emits), [`quote-management/quote-data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-data-emits-and-listens). ### Returns Returns [`NegotiableQuoteModel`](#negotiablequotemodel) or `null`. ## setShippingAddress Sets or updates the shipping address for a negotiable quote. It supports setting the address using either a saved customer address ID or by providing new address data. Returns the updated quote data on success and emits an event for successful updates. ```ts const setShippingAddress = async ( input: SetShippingAddressInput ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | [`SetShippingAddressInput`](#setshippingaddressinput) | Yes | An object containing the quote UID and either a saved address ID or new address data. Required field: `quoteUid` (string). Provide either `addressId` (number) for a saved address OR `addressData` (object) for a new address. Cannot provide both. | ### Examples ```ts additionalInput: { vat_id: 'GB123456789', custom_attribute: 'value', delivery_instructions: 'Leave at door' } ``` ### Events Emits the [`quote-management/shipping-address-set`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementshipping-address-set-emits-and-listens) event. ### Returns Returns [`NegotiableQuoteModel`](#negotiablequotemodel) or `null`. ## updateQuantities Updates the quantities of items in a negotiable quote. It validates input, transforms the request to `GraphQL` format, returns the updated quote data on success, and emits an event for successful updates. ```ts const updateQuantities = async ( input: UpdateQuantitiesInput ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | [`UpdateQuantitiesInput`](#updatequantitiesinput) | Yes | An object containing the quote UID and an array of items with their new quantities. Required fields: `quoteUid` (string), `items` (array of objects, each with `quoteItemUid` (string) and `quantity` (number, must be positive integer)). | ### Events Emits the [`quote-management/quantities-updated`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquantities-updated-emits-and-listens) event. ### Returns Returns [`NegotiableQuoteModel`](#negotiablequotemodel) or `null`. ## updateQuoteTemplateItemQuantities Changes the quantity of one or more items in an existing negotiable quote template. This allows updating item quantities, including optional `min/max` quantity constraints when the template uses `min/max` quantity settings. ```ts const updateQuoteTemplateItemQuantities = async ( params: UpdateQuoteTemplateItemQuantitiesParams ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `params` | `UpdateQuoteTemplateItemQuantitiesParams` | Yes | An object of type \`UpdateQuoteTemplateItemQuantitiesParams\` containing the template UID and an array of item quantity updates (item UID and new quantity for each item). | ### Events Emits the [`quote-management/quote-template-data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementquote-template-data-emits-and-listens) event. ### Returns Returns `void`. ## uploadFile Uploads a file attachment and returns a key for associating the file with a quote or quote template. ```ts const uploadFile = async ( file: File ): Promise<{ key: string }> ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `file` | `File` | Yes | The File object to upload and attach to a quote. Supports specification documents, purchase orders, or any supporting files that provide context for quote requests. | ### Events Emits the [`quote-management/file-upload-error`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/#quote-managementfile-upload-error-emits) event. ### Returns Returns `{ key: string }`. ## Type Definitions The following input types are used by functions in this drop-in. ### CloseNegotiableQuoteInput The `CloseNegotiableQuoteInput` type is used by [`closeNegotiableQuote`](#closenegotiablequote). ```ts interface CloseNegotiableQuoteInput { /** Array of quote UIDs to close (must not be empty) */ quoteUids: string[]; } ``` ### DuplicateQuoteInput The `DuplicateQuoteInput` type is used by [`duplicateQuote`](#duplicatequote). ```ts interface DuplicateQuoteInput { /** The unique identifier of the original quote to duplicate */ quoteUid: string; /** The unique identifier of the duplicated quote */ duplicatedQuoteUid: string; /** Optional flag indicating if the duplicated quote has out-of-stock items */ hasOutOfStockItems?: boolean; } ``` ### RemoveNegotiableQuoteItemsInput The `RemoveNegotiableQuoteItemsInput` type is used by [`removeNegotiableQuoteItems`](#removenegotiablequoteitems). ```ts interface RemoveNegotiableQuoteItemsInput { /** The unique identifier of the negotiable quote */ quoteUid: string; /** Array of quote item UIDs to remove (must not be empty) */ quoteItemUids: string[]; } ``` ### RenameNegotiableQuoteInput The `RenameNegotiableQuoteInput` type is used by [`renameNegotiableQuote`](#renamenegotiablequote). ```ts interface RenameNegotiableQuoteInput { /** The unique identifier of the negotiable quote */ quoteUid: string; /** The new name for the quote */ quoteName: string; /** Optional comment explaining the reason for the rename */ quoteComment?: string; } ``` ### RequestNegotiableQuoteInput The `RequestNegotiableQuoteInput` type is used by [`requestNegotiableQuote`](#requestnegotiablequote). ```ts interface RequestNegotiableQuoteInput { /** The unique identifier of the cart to create the quote from */ cartId: string; /** The name for the negotiable quote */ quoteName: string; /** The comment or message to include with the quote request */ comment: string; /** Whether to save as a draft (optional) */ isDraft?: boolean; /** Array of file attachment keys (optional) */ attachments?: { key: string }[]; } ``` ### SendForReviewInput The `SendForReviewInput` type is used by [`sendForReview`](#sendforreview). ```ts interface SendForReviewInput { /** The unique identifier of the negotiable quote */ quoteUid: string; /** Optional comment to include with the submission */ comment?: string; /** Optional array of file attachment keys */ attachments?: { key: string }[]; } ``` ### SetQuoteTemplateExpirationDateParams The `SetQuoteTemplateExpirationDateParams` type is used by [`setQuoteTemplateExpirationDate`](#setquotetemplateexpirationdate). ```ts interface SetQuoteTemplateExpirationDateParams { templateId: string; expirationDate: string; } ``` ### SetLineItemNoteInput The `SetLineItemNoteInput` type is used by [`setLineItemNote`](#setlineitemnote). ```ts interface SetLineItemNoteInput { /** The unique identifier of the negotiable quote */ quoteUid: string; /** The unique identifier of the quote line item */ itemUid: string; /** The note text to set for the line item */ note: string; /** Optional quantity for the line item */ quantity?: number; } ``` ### SetShippingAddressInput The `SetShippingAddressInput` type is used by [`setShippingAddress`](#setshippingaddress). ```ts interface SetShippingAddressInput { /** The unique identifier of the negotiable quote */ quoteUid: string; /** The ID of a saved customer address (use this OR addressData, not both) */ addressId?: number; /** New address data (use this OR addressId, not both) */ addressData?: AddressInput; } interface AddressInput { /** City name */ city: string; /** Optional company name */ company?: string; /** Two-letter country code (e.g., 'US') */ countryCode: string; /** First name */ firstname: string; /** Last name */ lastname: string; /** Postal/ZIP code */ postcode: string; /** Optional state/province name */ region?: string; /** Optional state/province ID */ regionId?: number; /** Whether to save this address to the customer's address book */ saveInAddressBook?: boolean; /** Street address lines (array) */ street: string[]; /** Phone number */ telephone: string; /** Additional custom fields for the address */ additionalInput?: Record; } ``` ### UpdateQuantitiesInput The `UpdateQuantitiesInput` type is used by [`updateQuantities`](#updatequantities). ```ts interface UpdateQuantitiesInput { /** The unique identifier of the negotiable quote */ quoteUid: string; /** Array of items with their new quantities */ items: QuantityItem[]; } interface QuantityItem { /** The unique ID of the quote item */ quoteItemUid: string; /** The new quantity for the item (must be greater than 0 and an integer) */ quantity: number; } ``` ## Data Models The following data models are used by functions in this drop-in. ### NegotiableQuoteModel The `NegotiableQuoteModel` object is returned by the following functions: [`duplicateQuote`](#duplicatequote), [`removeNegotiableQuoteItems`](#removenegotiablequoteitems), [`renameNegotiableQuote`](#renamenegotiablequote), [`requestNegotiableQuote`](#requestnegotiablequote), [`sendForReview`](#sendforreview), [`setLineItemNote`](#setlineitemnote), [`setShippingAddress`](#setshippingaddress), [`updateQuantities`](#updatequantities). ```ts interface NegotiableQuoteModel { uid: string; name: string; createdAt: string; salesRepName: string; expirationDate: string; updatedAt: string; status: NegotiableQuoteStatus; isVirtual: boolean; buyer: { firstname: string; lastname: string; }; email?: string; templateName?: string; totalQuantity: number; comments?: { uid: string; createdAt: string; author: { firstname: string; lastname: string; }; text: string; attachments?: { name: string; url: string; }[]; }[]; history?: NegotiableQuoteHistoryEntry[]; prices: { appliedDiscounts?: Discount[]; appliedTaxes?: Tax[]; discount?: Currency; grandTotal?: Currency; grandTotalExcludingTax?: Currency; shippingExcludingTax?: Currency; shippingIncludingTax?: Currency; subtotalExcludingTax?: Currency; subtotalIncludingTax?: Currency; subtotalWithDiscountExcludingTax?: Currency; totalTax?: Currency; }; items: CartItemModel[]; shippingAddresses?: ShippingAddress[]; canCheckout: boolean; canSendForReview: boolean; lockedForEditing?: boolean; canDelete: boolean; canClose: boolean; canUpdateQuote: boolean; readOnly: boolean; } ``` ### NegotiableQuoteTemplateModel The `NegotiableQuoteTemplateModel` object is returned by the following functions: [`createQuoteTemplate`](#createquotetemplate), [`getQuoteTemplateData`](#getquotetemplatedata), [`setQuoteTemplateExpirationDate`](#setquotetemplateexpirationdate). ```ts interface NegotiableQuoteTemplateModel { id: string; uid: string; name: string; createdAt: string; updatedAt: string; expirationDate?: string; status: NegotiableQuoteTemplateStatus; salesRepName: string; buyer: { firstname: string; lastname: string; }; comments?: QuoteTemplateComment[]; history?: NegotiableQuoteHistoryEntry[]; prices: { subtotalExcludingTax?: Currency; subtotalIncludingTax?: Currency; subtotalWithDiscountExcludingTax?: Currency; grandTotal?: Currency; appliedTaxes?: { amount: Currency; label: string; }[]; }; items: CartItemModel[]; shippingAddresses?: ShippingAddress[]; referenceDocuments?: { uid: string; name: string; identifier?: string; url: string; }[]; // Template-specific fields quantityThresholds?: { min?: number; max?: number; }; canAccept: boolean; canDelete: boolean; canReopen: boolean; canCancel: boolean; canSendForReview: boolean; canGenerateQuoteFromTemplate: boolean; canEditTemplateItems: boolean; } ``` ### NegotiableQuoteTemplatesListModel The `NegotiableQuoteTemplatesListModel` object is returned by the following functions: [`getQuoteTemplates`](#getquotetemplates). ```ts interface NegotiableQuoteTemplatesListModel { items: NegotiableQuoteTemplateListEntry[]; pageInfo: { currentPage: number; pageSize: number; totalPages: number; }; totalCount: number; paginationInfo?: PaginationInfo; sortFields?: { default: string; options: Array<{ label: string; value: string; }>; }; } ``` ### NegotiableQuotesListModel The `NegotiableQuotesListModel` object is returned by the following functions: [`negotiableQuotes`](#negotiablequotes). ```ts interface NegotiableQuotesListModel { items: NegotiableQuoteListEntry[]; pageInfo: { currentPage: number; pageSize: number; totalPages: number; }; totalCount: number; paginationInfo?: PaginationInfo; sortFields?: { default: string; options: Array<{ label: string; value: string; }>; }; } ``` ### StoreConfigModel The `StoreConfigModel` object is returned by the following functions: [`getStoreConfig`](#getstoreconfig). ```ts interface StoreConfigModel { quoteSummaryDisplayTotal: number; quoteSummaryMaxItems: number; quoteDisplaySettings: { zeroTax: boolean; subtotal: QuoteDisplayAmount; price: QuoteDisplayAmount; shipping: QuoteDisplayAmount; fullSummary: boolean; grandTotal: boolean; }; useConfigurableParentThumbnail: boolean; quoteMinimumAmount: number | null; quoteMinimumAmountMessage: string | null; } ``` {/* This documentation is auto-generated from the drop-in source repository: REPO_URL */} --- # Quote Management overview The Quote Management drop-in enables negotiable quote requests, quote lifecycle management, and tracking for Adobe Commerce storefronts. It also supports quote status updates, comments, and attachments. ## Supported Commerce features The following table provides an overview of the Adobe Commerce features that the Quote Management drop-in supports: | Feature | Status | | ------- | ------ | | Request negotiable quotes | Supported | | Quote management and tracking | Supported | | Quote status updates | Supported | | Quote comments and attachments | Supported | | Quote pricing summary | Supported | | Product list management | Supported | | Quote actions (print, copy, delete) | Supported | | Draft quote saving | Supported | | Quote expiration handling | Supported | | Customer authentication integration | Supported | | Permission-based access control | Supported | | Event-driven architecture | Supported | | Internationalization (i18n) support | Supported | | Responsive design | Supported | | Quote templates for repeat ordering | Supported | | Quote duplication | Supported | | Convert quotes to orders | Supported | ## Section topics The topics in this section will help you understand how to customize and use the Quote Management drop-in effectively within your B2B storefront. ### Quick Start Provides quick reference information and a getting started guide for the Quote Management drop-in. This topic covers package details, import paths, and basic usage examples to help you integrate Quote Management functionality into your site. ### Initialization Describes how to configure the Quote Management drop-in initializer with language definitions, permissions, and custom models. This customization allows you to align the drop-in with your B2B workflow requirements and brand standards. ### Containers Describes the structural elements of the Quote Management drop-in, focusing on how each container manages and displays content. Includes configuration options and customization settings to optimize the B2B user experience. ### Functions Describes the API functions available in the Quote Management drop-in. These functions allow developers to retrieve quote data, request new quotes, and manage quote lifecycle operations programmatically. ### Events Explains the event-driven architecture of the Quote Management drop-in, including available events and how to listen for them to integrate with other storefront components. ### Slots Describes the customizable content areas within Quote Management containers that can be replaced with custom components to tailor the user experience. ### Dictionary Provides the complete list of internationalization (i18n) keys used in the Quote Management drop-in for translating text content into different languages. ### Styles Describes how to customize the appearance of the Quote Management drop-in using CSS. Provides guidelines and examples for applying styles to various components within the drop-in to maintain brand consistency. --- # Quote Management initialization The **Quote Management initializer** configures the drop-in for managing negotiable quotes, quote templates, and quote workflows. Use initialization to set quote identifiers, customize data models, and enable internationalization for multi-language B2B storefronts. Version: 1.2.1 ## Configuration options The following table describes the configuration options available for the **Quote Management** initializer: | Parameter | Type | Req? | Description | |---|---|---|---| | `langDefinitions` | [`LangDefinitions`](#langdefinitions) | No | Language definitions for internationalization (i18n). Override dictionary keys for localization or branding. | | `quoteId` | `string` | No | Quote identifier used to load and display a specific negotiable quote. Pass this to initialize the drop-in with quote details on page load. | | `quoteTemplateId` | `string` | No | Quote template identifier used to load and display a specific quote template. Pass this to initialize the drop-in with template details on page load. | ## Default configuration The initializer runs with these defaults when no configuration is provided: ```javascript title="scripts/initializers/quote-management.js" // All configuration options are optional await initializers.mountImmediately(initialize, { langDefinitions: {}, // Uses built-in English strings models: {}, // Uses default data models // Drop-in-specific defaults: // quoteId: undefined // See configuration options below // quoteTemplateId: undefined // See configuration options below }); ``` ## Language definitions Override dictionary keys for localization or branding. The `langDefinitions` object maps locale keys to custom strings that override default text for the drop-in. ```javascript title="scripts/initializers/quote-management.js" const customStrings = { 'AddToCart': 'Add to Bag', 'Checkout': 'Complete Purchase', 'Price': 'Cost', }; const langDefinitions = { default: customStrings, }; await initializers.mountImmediately(initialize, { langDefinitions }); ``` > For complete dictionary customization including all available keys and multi-language support, see the [Quote Management Dictionary](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/dictionary/) page. ## Customizing data models Extend or transform data models by providing custom transformer functions. Use the `models` option to add custom fields or modify existing data structures returned from the backend. ### Available models The following models can be customized through the `models` configuration option: | Model | Description | |---|---| | [`NegotiableQuoteModel`](#negotiablequotemodel) | Transforms negotiable quote data from `GraphQL` including quote details, status, items, totals, comments, and history. Use this to add custom fields or modify existing quote data structures. | The following example shows how to customize the `NegotiableQuoteModel` model for the **Quote Management** drop-in: ```javascript title="scripts/initializers/quote-management.js" const models = { NegotiableQuoteModel: { transformer: (data) => ({ // Add urgency badge for expiring quotes (within 7 days) isExpiringSoon: data?.expirationDate && new Date(data.expirationDate) - Date.now() < 7 * 24 * 60 * 60 * 1000, // Custom status display for better UX statusDisplay: data?.status === 'SUBMITTED' ? 'Pending Review' : data?.status, // Add formatted expiration date expirationFormatted: data?.expirationDate ? new Date(data.expirationDate).toLocaleDateString() : null, }), }, }; await initializers.mountImmediately(initialize, { models }); ``` ## Drop-in configuration The **Quote Management initializer** configures the drop-in for managing negotiable quotes, quote templates, and quote workflows. Use initialization to set quote identifiers, customize data models, and enable internationalization for multi-language B2B storefronts. ```javascript title="scripts/initializers/quote-management.js" await initializers.mountImmediately(initialize, { langDefinitions: {}, quoteId: 'abc123', quoteTemplateId: 'abc123', }); ``` > Refer to the [Configuration options](#configuration-options) table for detailed descriptions of each option. ## Configuration types The following TypeScript definitions show the structure of each configuration object: ### langDefinitions Maps locale identifiers to dictionaries of key-value pairs. The `default` locale is used as the fallback when no specific locale matches. Each dictionary key corresponds to a text string used in the drop-in UI. ```typescript langDefinitions?: { [locale: string]: { [key: string]: string; }; }; ``` ## Model definitions The following TypeScript definitions show the structure of each customizable model: ### NegotiableQuoteModel ```typescript interface NegotiableQuoteModel { uid: string; name: string; createdAt: string; salesRepName: string; expirationDate: string; updatedAt: string; status: NegotiableQuoteStatus; isVirtual: boolean; buyer: { firstname: string; lastname: string; }; email?: string; templateName?: string; totalQuantity: number; comments?: { uid: string; createdAt: string; author: { firstname: string; lastname: string; }; text: string; attachments?: { name: string; url: string; }[]; }[]; history?: NegotiableQuoteHistoryEntry[]; prices: { appliedDiscounts?: Discount[]; appliedTaxes?: Tax[]; discount?: Currency; grandTotal?: Currency; grandTotalExcludingTax?: Currency; shippingExcludingTax?: Currency; shippingIncludingTax?: Currency; subtotalExcludingTax?: Currency; subtotalIncludingTax?: Currency; subtotalWithDiscountExcludingTax?: Currency; totalTax?: Currency; }; items: CartItemModel[]; shippingAddresses?: ShippingAddress[]; canCheckout: boolean; canSendForReview: boolean; lockedForEditing?: boolean; canDelete: boolean; canClose: boolean; canUpdateQuote: boolean; readOnly: boolean; } ``` --- # Quote Management Quick Start Get started with the Quote Management drop-in to enable B2B quote negotiation and management in your storefront. Version: 1.2.0 ## Quick example The Quote Management drop-in is included in the https://github.com/hlxsites/aem-boilerplate-commerce. This example shows the basic pattern: ```js // 1. Import initializer (handles all setup) // 2. Import the container you need // 3. Import the provider // 4. Render in your block export default async function decorate(block) { await provider.render(ItemsQuoted, { // Configuration options - see Containers page })(block); } ``` **New to drop-ins?** See the [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/) guide for complete step-by-step instructions. ## Quick reference **Import paths:** - Initializer: `import '../../scripts/initializers/quote-management.js'` - Containers: `import ContainerName from '@dropins/storefront-quote-management/containers/ContainerName.js'` - Provider: `import { render } from '@dropins/storefront-quote-management/render.js'` **Package:** `@dropins/storefront-quote-management` **Version:** 1.2.0 (verify compatibility with your Commerce instance) **Example container:** `ItemsQuoted` ## Learn more - [Containers](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/containers/) - Available UI components and configuration options - [Initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/initialization/) - Customize initializer settings and data models - [Functions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/functions/) - Control drop-in behavior programmatically - [Events](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/events/) - Listen to and respond to drop-in state changes - [Slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/slots/) - Extend containers with custom content --- # Quote Management Slots The Quote Management drop-in exposes slots for customizing specific UI sections. Use slots to replace or extend container components. For default properties available to all slots, see [Extending drop-in components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/extending/). Version: 1.2.0 | Container | Slots | |-----------|-------| | [`ItemsQuoted`](#itemsquoted-slots) | `ProductListTable`, `QuotePricesSummary` | | [`ItemsQuotedTemplate`](#itemsquotedtemplate-slots) | `ProductListTable`, `QuotePricesSummary` | | [`ManageNegotiableQuote`](#managenegotiablequote-slots) | `QuoteName`, `QuoteStatus`, `Banner`, `DuplicateQuoteWarningBanner`, `Details`, `ActionBar`, `QuoteContent`, `ItemsQuotedTab`, `CommentsTab`, `HistoryLogTab`, `ShippingInformationTitle`, `ShippingInformation`, `QuoteCommentsTitle`, `QuoteComments`, `AttachFilesField`, `AttachedFilesList`, `Footer` | | [`ManageNegotiableQuoteTemplate`](#managenegotiablequotetemplate-slots) | `TemplateName`, `TemplateStatus`, `Banner`, `Details`, `ActionBar`, `ReferenceDocuments`, `ItemsTable`, `ItemsQuotedTab`, `CommentsTab`, `HistoryLogTab`, `CommentsTitle`, `Comments`, `AttachFilesField`, `AttachedFilesList`, `HistoryLogTitle`, `HistoryLog`, `Footer`, `ShippingInformationTitle`, `ShippingInformation` | | [`QuoteSummaryList`](#quotesummarylist-slots) | `Heading`, `Footer`, `Thumbnail`, `ProductAttributes`, `QuoteSummaryFooter`, `QuoteItem`, `ItemTitle`, `ItemPrice`, `ItemTotal`, `ItemSku` | | [`QuoteTemplatesListTable`](#quotetemplateslisttable-slots) | `Name`, `State`, `Status`, `ValidUntil`, `MinQuoteTotal`, `OrdersPlaced`, `LastOrdered`, `Actions`, `EmptyTemplates`, `ItemRange`, `PageSizePicker`, `Pagination` | | [`QuotesListTable`](#quoteslisttable-slots) | `QuoteName`, `Created`, `CreatedBy`, `Status`, `LastUpdated`, `QuoteTemplate`, `QuoteTotal`, `Actions`, `EmptyQuotes`, `ItemRange`, `PageSizePicker`, `Pagination` | | [`RequestNegotiableQuoteForm`](#requestnegotiablequoteform-slots) | `ErrorBanner`, `SuccessBanner`, `Title`, `CommentField`, `QuoteNameField`, `AttachFileField`, `AttachedFilesList`, `RequestButton`, `SaveDraftButton` | ## ItemsQuoted slots The slots for the `ItemsQuoted` container allow you to customize its appearance and behavior. ```typescript interface ItemsQuotedProps { slots?: { ProductListTable?: SlotProps<{ items: NegotiableQuoteModel['items']; canEdit: boolean; readOnly?: boolean; onItemCheckboxChange?: ( item: CartItemModel, isSelected: boolean ) => void; onItemDropdownChange?: ( item: CartItemModel, action: string ) => void; onQuantityChange?: ( item: CartItemModel, newQuantity: number ) => void; onUpdate?: (e: SubmitEvent) => void; dropdownSelections?: Record; }>; QuotePricesSummary?: SlotProps<{ items: NegotiableQuoteModel['items']; prices: NegotiableQuoteModel['prices']; }>; }; } ``` ### QuotePricesSummary slot The `QuotePricesSummary` slot allows you to customize the quote prices summary section of the `ItemsQuoted` container. #### Example ```js await provider.render(ItemsQuoted, { slots: { QuotePricesSummary: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom QuotePricesSummary'; ctx.appendChild(element); } } })(block); ``` ## ItemsQuotedTemplate slots The slots for the `ItemsQuotedTemplate` container allow you to customize its appearance and behavior. ```typescript interface ItemsQuotedTemplateProps { slots?: { ProductListTable?: SlotProps<{ items: NegotiableQuoteTemplateModel['items']; canEdit: boolean; dropdownSelections: Record; handleItemDropdownChange: (item: CartItemModel, action: string) => void; handleQuantityChange: (item: CartItemModel, newQuantity: number) => void; handleUpdate: (e: SubmitEvent) => void; onItemDropdownChange?: (item: any, action: string) => void; }>; QuotePricesSummary?: SlotProps<{ items: NegotiableQuoteTemplateModel['items']; prices: NegotiableQuoteTemplateModel['prices']; }>; }; } ``` ### ProductListTable slot The `ProductListTable` slot allows you to customize the product list table section of the `ItemsQuotedTemplate` container. #### Example ```js await provider.render(ItemsQuotedTemplate, { slots: { ProductListTable: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ProductListTable'; ctx.appendChild(element); } } })(block); ``` ### QuotePricesSummary slot The `QuotePricesSummary` slot allows you to customize the quote prices summary section of the `ItemsQuotedTemplate` container. #### Example ```js await provider.render(ItemsQuotedTemplate, { slots: { QuotePricesSummary: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom QuotePricesSummary'; ctx.appendChild(element); } } })(block); ``` ## ManageNegotiableQuote slots The slots for the `ManageNegotiableQuote` container allow you to customize its appearance and behavior. ```typescript interface ManageNegotiableQuoteProps { slots?: { QuoteName?: SlotProps<{ quoteName?: string; quoteData?: NegotiableQuoteModel; }>; QuoteStatus?: SlotProps<{ quoteStatus?: string; quoteData?: NegotiableQuoteModel; }>; Banner?: SlotProps<{ quoteData?: NegotiableQuoteModel; }>; DuplicateQuoteWarningBanner?: SlotProps<{ outOfStockWarning?: boolean; }>; Details?: SlotProps<{ quoteData?: NegotiableQuoteModel; }>; ActionBar?: SlotProps<{ quoteData?: NegotiableQuoteModel; actionsBarDropdownValue?: string; }>; QuoteContent?: SlotProps<{ quoteData?: NegotiableQuoteModel; }>; ItemsQuotedTab?: SlotProps<{ quoteData?: NegotiableQuoteModel; }>; CommentsTab?: SlotProps<{ quoteData?: NegotiableQuoteModel; }>; HistoryLogTab?: SlotProps<{ quoteData?: NegotiableQuoteModel; }>; ShippingInformationTitle?: SlotProps<{ quoteData?: NegotiableQuoteModel; }>; ShippingInformation?: SlotProps<{ quoteData?: NegotiableQuoteModel; loading?: boolean; setLoading?: (loading: boolean) => void; }>; QuoteCommentsTitle?: SlotProps<{ quoteData?: NegotiableQuoteModel; }>; QuoteComments?: SlotProps<{ quoteData?: NegotiableQuoteModel; }>; AttachFilesField?: SlotProps<{ onFileChange: (files: File[]) => void; attachedFiles: AttachedFile[]; fileUploadError: string | undefined; disabled?: boolean; }>; AttachedFilesList?: SlotProps<{ files: AttachedFile[]; onRemove: (key: string) => void; disabled?: boolean; }>; Footer?: SlotProps<{ quoteData?: NegotiableQuoteModel; comment?: string; isSubmitting?: boolean; attachments?: AttachedFile[]; handleSendForReview: () => void; }>; }; } ``` ### QuoteName slot The `QuoteName` slot allows you to customize the quote name section of the `ManageNegotiableQuote` container. #### Example ```js await provider.render(ManageNegotiableQuote, { slots: { QuoteName: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom QuoteName'; ctx.appendChild(element); } } })(block); ``` ### QuoteStatus slot The `QuoteStatus` slot allows you to customize the quote status section of the `ManageNegotiableQuote` container. #### Example ```js await provider.render(ManageNegotiableQuote, { slots: { QuoteStatus: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom QuoteStatus'; ctx.appendChild(element); } } })(block); ``` ### Banner slot The Banner slot allows you to customize the banner section of the `ManageNegotiableQuote` container. #### Example ```js await provider.render(ManageNegotiableQuote, { slots: { Banner: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Banner'; ctx.appendChild(element); } } })(block); ``` ### DuplicateQuoteWarningBanner slot The `DuplicateQuoteWarningBanner` slot allows you to customize the duplicate quote warning banner section of the `ManageNegotiableQuote` container. #### Example ```js await provider.render(ManageNegotiableQuote, { slots: { DuplicateQuoteWarningBanner: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom DuplicateQuoteWarningBanner'; ctx.appendChild(element); } } })(block); ``` ### Details slot The Details slot allows you to customize the details section of the `ManageNegotiableQuote` container. #### Example ```js await provider.render(ManageNegotiableQuote, { slots: { Details: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Details'; ctx.appendChild(element); } } })(block); ``` ### ActionBar slot The `ActionBar` slot allows you to customize the action bar section of the `ManageNegotiableQuote` container. #### Example ```js await provider.render(ManageNegotiableQuote, { slots: { ActionBar: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ActionBar'; ctx.appendChild(element); } } })(block); ``` ### QuoteContent slot The `QuoteContent` slot allows you to customize the quote content section of the `ManageNegotiableQuote` container. #### Example ```js await provider.render(ManageNegotiableQuote, { slots: { QuoteContent: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom QuoteContent'; ctx.appendChild(element); } } })(block); ``` ### ItemsQuotedTab slot The `ItemsQuotedTab` slot allows you to customize the items quoted tab section of the `ManageNegotiableQuote` container. #### Example ```js await provider.render(ManageNegotiableQuote, { slots: { ItemsQuotedTab: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ItemsQuotedTab'; ctx.appendChild(element); } } })(block); ``` ### CommentsTab slot The `CommentsTab` slot allows you to customize the comments tab section of the `ManageNegotiableQuote` container. #### Example ```js await provider.render(ManageNegotiableQuote, { slots: { CommentsTab: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom CommentsTab'; ctx.appendChild(element); } } })(block); ``` ### HistoryLogTab slot The `HistoryLogTab` slot allows you to customize the history log tab section of the `ManageNegotiableQuote` container. #### Example ```js await provider.render(ManageNegotiableQuote, { slots: { HistoryLogTab: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom HistoryLogTab'; ctx.appendChild(element); } } })(block); ``` ### ShippingInformationTitle slot The `ShippingInformationTitle` slot allows you to customize the shipping information title section of the `ManageNegotiableQuote` container. #### Example ```js await provider.render(ManageNegotiableQuote, { slots: { ShippingInformationTitle: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ShippingInformationTitle'; ctx.appendChild(element); } } })(block); ``` ### QuoteCommentsTitle slot The `QuoteCommentsTitle` slot allows you to customize the quote comments title section of the `ManageNegotiableQuote` container. #### Example ```js await provider.render(ManageNegotiableQuote, { slots: { QuoteCommentsTitle: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom QuoteCommentsTitle'; ctx.appendChild(element); } } })(block); ``` ### QuoteComments slot The `QuoteComments` slot allows you to customize the quote comments section of the `ManageNegotiableQuote` container. #### Example ```js await provider.render(ManageNegotiableQuote, { slots: { QuoteComments: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom QuoteComments'; ctx.appendChild(element); } } })(block); ``` ## ManageNegotiableQuoteTemplate slots The slots for the `ManageNegotiableQuoteTemplate` container allow you to customize its appearance and behavior. ```typescript interface ManageNegotiableQuoteTemplateProps { slots?: { TemplateName?: SlotProps<{ templateName?: string; templateData?: NegotiableQuoteTemplateModel; templateDisplayName?: string; isRenameDisabled?: boolean; }>; TemplateStatus?: SlotProps<{ templateStatus?: string; templateData?: NegotiableQuoteTemplateModel; }>; Banner?: SlotProps<{ templateData?: NegotiableQuoteTemplateModel; }>; Details?: SlotProps<{ templateData?: NegotiableQuoteTemplateModel; }>; ActionBar?: SlotProps<{ templateData?: NegotiableQuoteTemplateModel; }>; ReferenceDocuments?: SlotProps<{ templateData?: NegotiableQuoteTemplateModel; referenceDocuments?: ReferenceDocument[]; isEditable?: boolean; onAddDocument?: () => void; onEditDocument?: (document: ReferenceDocument) => void; onRemoveDocument?: (document: ReferenceDocument) => void; referenceDocumentsTitle?: string; }>; ItemsTable?: SlotProps<{ templateData?: NegotiableQuoteTemplateModel; }>; ItemsQuotedTab?: SlotProps<{ templateData?: NegotiableQuoteTemplateModel; }>; CommentsTab?: SlotProps<{ templateData?: NegotiableQuoteTemplateModel; }>; HistoryLogTab?: SlotProps<{ templateData?: NegotiableQuoteTemplateModel; }>; CommentsTitle?: SlotProps<{ templateData?: NegotiableQuoteTemplateModel; }>; Comments?: SlotProps<{ templateData?: NegotiableQuoteTemplateModel; }>; AttachFilesField?: SlotProps<{ templateData?: NegotiableQuoteTemplateModel; onFileChange: (files: File[]) => void; attachedFiles: AttachedFile[]; fileUploadError: string | undefined; disabled: boolean; }>; AttachedFilesList?: SlotProps<{ templateData?: NegotiableQuoteTemplateModel; files: AttachedFile[]; onRemove: (key: string) => void; disabled: boolean; }>; HistoryLogTitle?: SlotProps<{ templateData?: NegotiableQuoteTemplateModel; }>; HistoryLog?: SlotProps<{ templateData?: NegotiableQuoteTemplateModel; }>; Footer?: SlotProps<{ templateData?: NegotiableQuoteTemplateModel; comment?: string; isSubmitting?: boolean; attachedFiles?: AttachedFile[]; referenceDocuments?: ReferenceDocument[]; hasUnsavedChanges?: boolean; handleSendForReview: () => void; showAcceptButton?: boolean; renameTemplateName?: string; renameReason?: string; }>; ShippingInformationTitle?: SlotProps<{ templateData?: NegotiableQuoteTemplateModel; }>; ShippingInformation?: SlotProps<{ templateData?: NegotiableQuoteTemplateModel; loading?: boolean; setLoading?: (loading: boolean) => void; }>; }; } ``` ### TemplateName slot The `TemplateName` slot allows you to customize the template name section of the `ManageNegotiableQuoteTemplate` container. #### Example ```js await provider.render(ManageNegotiableQuoteTemplate, { slots: { TemplateName: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom TemplateName'; ctx.appendChild(element); } } })(block); ``` ### TemplateStatus slot The `TemplateStatus` slot allows you to customize the template status section of the `ManageNegotiableQuoteTemplate` container. #### Example ```js await provider.render(ManageNegotiableQuoteTemplate, { slots: { TemplateStatus: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom TemplateStatus'; ctx.appendChild(element); } } })(block); ``` ### Banner slot The Banner slot allows you to customize the banner section of the `ManageNegotiableQuoteTemplate` container. #### Example ```js await provider.render(ManageNegotiableQuoteTemplate, { slots: { Banner: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Banner'; ctx.appendChild(element); } } })(block); ``` ### Details slot The Details slot allows you to customize the details section of the `ManageNegotiableQuoteTemplate` container. #### Example ```js await provider.render(ManageNegotiableQuoteTemplate, { slots: { Details: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Details'; ctx.appendChild(element); } } })(block); ``` ### ActionBar slot The `ActionBar` slot allows you to customize the action bar section of the `ManageNegotiableQuoteTemplate` container. #### Example ```js await provider.render(ManageNegotiableQuoteTemplate, { slots: { ActionBar: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ActionBar'; ctx.appendChild(element); } } })(block); ``` ### ItemsTable slot The `ItemsTable` slot allows you to customize the items table section of the `ManageNegotiableQuoteTemplate` container. #### Example ```js await provider.render(ManageNegotiableQuoteTemplate, { slots: { ItemsTable: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ItemsTable'; ctx.appendChild(element); } } })(block); ``` ### ItemsQuotedTab slot The `ItemsQuotedTab` slot allows you to customize the items quoted tab section of the `ManageNegotiableQuoteTemplate` container. #### Example ```js await provider.render(ManageNegotiableQuoteTemplate, { slots: { ItemsQuotedTab: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ItemsQuotedTab'; ctx.appendChild(element); } } })(block); ``` ### CommentsTab slot The `CommentsTab` slot allows you to customize the comments tab section of the `ManageNegotiableQuoteTemplate` container. #### Example ```js await provider.render(ManageNegotiableQuoteTemplate, { slots: { CommentsTab: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom CommentsTab'; ctx.appendChild(element); } } })(block); ``` ### HistoryLogTab slot The `HistoryLogTab` slot allows you to customize the history log tab section of the `ManageNegotiableQuoteTemplate` container. #### Example ```js await provider.render(ManageNegotiableQuoteTemplate, { slots: { HistoryLogTab: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom HistoryLogTab'; ctx.appendChild(element); } } })(block); ``` ### CommentsTitle slot The `CommentsTitle` slot allows you to customize the comments title section of the `ManageNegotiableQuoteTemplate` container. #### Example ```js await provider.render(ManageNegotiableQuoteTemplate, { slots: { CommentsTitle: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom CommentsTitle'; ctx.appendChild(element); } } })(block); ``` ### Comments slot The Comments slot allows you to customize the comments section of the `ManageNegotiableQuoteTemplate` container. #### Example ```js await provider.render(ManageNegotiableQuoteTemplate, { slots: { Comments: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Comments'; ctx.appendChild(element); } } })(block); ``` ### HistoryLogTitle slot The `HistoryLogTitle` slot allows you to customize the history log title section of the `ManageNegotiableQuoteTemplate` container. #### Example ```js await provider.render(ManageNegotiableQuoteTemplate, { slots: { HistoryLogTitle: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom HistoryLogTitle'; ctx.appendChild(element); } } })(block); ``` ### HistoryLog slot The `HistoryLog` slot allows you to customize the history log section of the `ManageNegotiableQuoteTemplate` container. #### Example ```js await provider.render(ManageNegotiableQuoteTemplate, { slots: { HistoryLog: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom HistoryLog'; ctx.appendChild(element); } } })(block); ``` ### ShippingInformationTitle slot The `ShippingInformationTitle` slot allows you to customize the shipping information title section of the `ManageNegotiableQuoteTemplate` container. #### Example ```js await provider.render(ManageNegotiableQuoteTemplate, { slots: { ShippingInformationTitle: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ShippingInformationTitle'; ctx.appendChild(element); } } })(block); ``` ## QuoteSummaryList slots The slots for the `QuoteSummaryList` container allow you to customize its appearance and behavior. ```typescript interface QuoteSummaryListProps { slots?: { Heading?: SlotProps<{ count: number; quoteId: string }>; Footer?: SlotProps<{ item: NegotiableQuoteItemModel }>; Thumbnail?: SlotProps<{ item: NegotiableQuoteItemModel; defaultImageProps: ImageProps; }>; ProductAttributes?: SlotProps<{ item: NegotiableQuoteItemModel }>; QuoteSummaryFooter?: SlotProps<{ displayMaxItems: boolean; }>; QuoteItem?: SlotProps; ItemTitle?: SlotProps<{ item: NegotiableQuoteItemModel }>; ItemPrice?: SlotProps<{ item: NegotiableQuoteItemModel }>; ItemTotal?: SlotProps<{ item: NegotiableQuoteItemModel }>; ItemSku?: SlotProps<{ item: NegotiableQuoteItemModel }>; }; } ``` ### Heading slot The Heading slot allows you to customize the heading section of the `QuoteSummaryList` container. #### Example ```js await provider.render(QuoteSummaryList, { slots: { Heading: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Heading'; ctx.appendChild(element); } } })(block); ``` ### Footer slot The Footer slot allows you to customize the footer section of the `QuoteSummaryList` container. #### Example ```js await provider.render(QuoteSummaryList, { slots: { Footer: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Footer'; ctx.appendChild(element); } } })(block); ``` ### Thumbnail slot The Thumbnail slot allows you to customize the thumbnail section of the `QuoteSummaryList` container. #### Example ```js await provider.render(QuoteSummaryList, { slots: { Thumbnail: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Thumbnail'; ctx.appendChild(element); } } })(block); ``` ### ProductAttributes slot The `ProductAttributes` slot allows you to customize the product attributes section of the `QuoteSummaryList` container. #### Example ```js await provider.render(QuoteSummaryList, { slots: { ProductAttributes: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ProductAttributes'; ctx.appendChild(element); } } })(block); ``` ### QuoteSummaryFooter slot The `QuoteSummaryFooter` slot allows you to customize the quote summary footer section of the `QuoteSummaryList` container. #### Example ```js await provider.render(QuoteSummaryList, { slots: { QuoteSummaryFooter: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom QuoteSummaryFooter'; ctx.appendChild(element); } } })(block); ``` ### QuoteItem slot The `QuoteItem` slot allows you to customize the quote item section of the `QuoteSummaryList` container. #### Example ```js await provider.render(QuoteSummaryList, { slots: { QuoteItem: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom QuoteItem'; ctx.appendChild(element); } } })(block); ``` ### ItemTitle slot The `ItemTitle` slot allows you to customize the item title section of the `QuoteSummaryList` container. #### Example ```js await provider.render(QuoteSummaryList, { slots: { ItemTitle: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ItemTitle'; ctx.appendChild(element); } } })(block); ``` ### ItemPrice slot The `ItemPrice` slot allows you to customize the item price section of the `QuoteSummaryList` container. #### Example ```js await provider.render(QuoteSummaryList, { slots: { ItemPrice: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ItemPrice'; ctx.appendChild(element); } } })(block); ``` ### ItemTotal slot The `ItemTotal` slot allows you to customize the item total section of the `QuoteSummaryList` container. #### Example ```js await provider.render(QuoteSummaryList, { slots: { ItemTotal: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ItemTotal'; ctx.appendChild(element); } } })(block); ``` ### ItemSku slot The `ItemSku` slot allows you to customize the item sku section of the `QuoteSummaryList` container. #### Example ```js await provider.render(QuoteSummaryList, { slots: { ItemSku: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ItemSku'; ctx.appendChild(element); } } })(block); ``` ## QuoteTemplatesListTable slots The slots for the `QuoteTemplatesListTable` container allow you to customize its appearance and behavior. ```typescript interface QuoteTemplatesListTableProps { slots?: { Name?: SlotProps<{ template: NegotiableQuoteTemplateListEntry }>; State?: SlotProps<{ template: NegotiableQuoteTemplateListEntry }>; Status?: SlotProps<{ template: NegotiableQuoteTemplateListEntry }>; ValidUntil?: SlotProps<{ template: NegotiableQuoteTemplateListEntry }>; MinQuoteTotal?: SlotProps<{ template: NegotiableQuoteTemplateListEntry }>; OrdersPlaced?: SlotProps<{ template: NegotiableQuoteTemplateListEntry }>; LastOrdered?: SlotProps<{ template: NegotiableQuoteTemplateListEntry }>; Actions?: SlotProps<{ template: NegotiableQuoteTemplateListEntry; onViewQuoteTemplate?: (id: string, name: string, status: string) => void; onGenerateQuoteFromTemplate?: (id: string, name: string) => void; }>; EmptyTemplates?: SlotProps; ItemRange?: SlotProps<{ startItem: number; endItem: number; totalCount: number; currentPage: number; pageSize: number; }>; PageSizePicker?: SlotProps<{ pageSize: number; pageSizeOptions: number[]; onPageSizeChange?: (pageSize: number) => void; }>; Pagination?: SlotProps<{ currentPage: number; totalPages: number; onChange?: (page: number) => void; }>; }; } ``` ### Name slot The Name slot allows you to customize the name section of the `QuoteTemplatesListTable` container. #### Example ```js await provider.render(QuoteTemplatesListTable, { slots: { Name: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Name'; ctx.appendChild(element); } } })(block); ``` ### State slot The State slot allows you to customize the state section of the `QuoteTemplatesListTable` container. #### Example ```js await provider.render(QuoteTemplatesListTable, { slots: { State: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom State'; ctx.appendChild(element); } } })(block); ``` ### Status slot The Status slot allows you to customize the status section of the `QuoteTemplatesListTable` container. #### Example ```js await provider.render(QuoteTemplatesListTable, { slots: { Status: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Status'; ctx.appendChild(element); } } })(block); ``` ### ValidUntil slot The `ValidUntil` slot allows you to customize the valid until section of the `QuoteTemplatesListTable` container. #### Example ```js await provider.render(QuoteTemplatesListTable, { slots: { ValidUntil: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ValidUntil'; ctx.appendChild(element); } } })(block); ``` ### MinQuoteTotal slot The `MinQuoteTotal` slot allows you to customize the min quote total section of the `QuoteTemplatesListTable` container. #### Example ```js await provider.render(QuoteTemplatesListTable, { slots: { MinQuoteTotal: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom MinQuoteTotal'; ctx.appendChild(element); } } })(block); ``` ### OrdersPlaced slot The `OrdersPlaced` slot allows you to customize the orders placed section of the `QuoteTemplatesListTable` container. #### Example ```js await provider.render(QuoteTemplatesListTable, { slots: { OrdersPlaced: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom OrdersPlaced'; ctx.appendChild(element); } } })(block); ``` ### LastOrdered slot The `LastOrdered` slot allows you to customize the last ordered section of the `QuoteTemplatesListTable` container. #### Example ```js await provider.render(QuoteTemplatesListTable, { slots: { LastOrdered: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom LastOrdered'; ctx.appendChild(element); } } })(block); ``` ### EmptyTemplates slot The `EmptyTemplates` slot allows you to customize the empty templates section of the `QuoteTemplatesListTable` container. #### Example ```js await provider.render(QuoteTemplatesListTable, { slots: { EmptyTemplates: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom EmptyTemplates'; ctx.appendChild(element); } } })(block); ``` ### ItemRange slot The `ItemRange` slot allows you to customize the item range section of the `QuoteTemplatesListTable` container. #### Example ```js await provider.render(QuoteTemplatesListTable, { slots: { ItemRange: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ItemRange'; ctx.appendChild(element); } } })(block); ``` ## QuotesListTable slots The slots for the `QuotesListTable` container allow you to customize its appearance and behavior. ```typescript interface QuotesListTableProps { slots?: { QuoteName?: SlotProps<{ quote: NegotiableQuoteListEntry }>; Created?: SlotProps<{ quote: NegotiableQuoteListEntry }>; CreatedBy?: SlotProps<{ quote: NegotiableQuoteListEntry }>; Status?: SlotProps<{ quote: NegotiableQuoteListEntry }>; LastUpdated?: SlotProps<{ quote: NegotiableQuoteListEntry }>; QuoteTemplate?: SlotProps<{ quote: NegotiableQuoteListEntry }>; QuoteTotal?: SlotProps<{ quote: NegotiableQuoteListEntry }>; Actions?: SlotProps<{ quote: NegotiableQuoteListEntry; onViewQuote?: (id: string, name: string, status: string) => void; }>; EmptyQuotes?: SlotProps; ItemRange?: SlotProps<{ startItem: number; endItem: number; totalCount: number; currentPage: number; pageSize: number; }>; PageSizePicker?: SlotProps<{ pageSize: number; pageSizeOptions: number[]; onPageSizeChange?: (pageSize: number) => void; }>; Pagination?: SlotProps<{ currentPage: number; totalPages: number; onChange?: (page: number) => void; }>; }; } ``` ### QuoteName slot The `QuoteName` slot allows you to customize the quote name section of the `QuotesListTable` container. #### Example ```js await provider.render(QuotesListTable, { slots: { QuoteName: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom QuoteName'; ctx.appendChild(element); } } })(block); ``` ### Created slot The Created slot allows you to customize the created section of the `QuotesListTable` container. #### Example ```js await provider.render(QuotesListTable, { slots: { Created: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Created'; ctx.appendChild(element); } } })(block); ``` ### CreatedBy slot The `CreatedBy` slot allows you to customize the created by section of the `QuotesListTable` container. #### Example ```js await provider.render(QuotesListTable, { slots: { CreatedBy: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom CreatedBy'; ctx.appendChild(element); } } })(block); ``` ### Status slot The Status slot allows you to customize the status section of the `QuotesListTable` container. #### Example ```js await provider.render(QuotesListTable, { slots: { Status: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Status'; ctx.appendChild(element); } } })(block); ``` ### LastUpdated slot The `LastUpdated` slot allows you to customize the last updated section of the `QuotesListTable` container. #### Example ```js await provider.render(QuotesListTable, { slots: { LastUpdated: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom LastUpdated'; ctx.appendChild(element); } } })(block); ``` ### QuoteTemplate slot The `QuoteTemplate` slot allows you to customize the quote template section of the `QuotesListTable` container. #### Example ```js await provider.render(QuotesListTable, { slots: { QuoteTemplate: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom QuoteTemplate'; ctx.appendChild(element); } } })(block); ``` ### QuoteTotal slot The `QuoteTotal` slot allows you to customize the quote total section of the `QuotesListTable` container. #### Example ```js await provider.render(QuotesListTable, { slots: { QuoteTotal: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom QuoteTotal'; ctx.appendChild(element); } } })(block); ``` ### EmptyQuotes slot The `EmptyQuotes` slot allows you to customize the empty quotes section of the `QuotesListTable` container. #### Example ```js await provider.render(QuotesListTable, { slots: { EmptyQuotes: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom EmptyQuotes'; ctx.appendChild(element); } } })(block); ``` ### ItemRange slot The `ItemRange` slot allows you to customize the item range section of the `QuotesListTable` container. #### Example ```js await provider.render(QuotesListTable, { slots: { ItemRange: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ItemRange'; ctx.appendChild(element); } } })(block); ``` ## RequestNegotiableQuoteForm slots The slots for the `RequestNegotiableQuoteForm` container allow you to customize its appearance and behavior. ```typescript interface RequestNegotiableQuoteFormProps { slots?: { ErrorBanner?: SlotProps<{ message: string, }>; SuccessBanner?: SlotProps<{ message: string, }>; Title?: SlotProps<{ text: string, }>; CommentField?: SlotProps<{ formErrors: Record; isFormDisabled: boolean; setFormErrors: (errors: Record) => void; }>; QuoteNameField?: SlotProps<{ formErrors: Record; isFormDisabled: boolean; setFormErrors: (errors: Record) => void; }>; AttachFileField?: SlotProps<{ onChange: (files: File[]) => void, formErrors: Record, isFormDisabled: boolean, attachedFiles: AttachedFile[] }>; AttachedFilesList?: SlotProps<{ files: AttachedFile[]; onRemove: (key: string) => void; disabled?: boolean; }>; RequestButton?: SlotProps<{ requestNegotiableQuote: typeof requestNegotiableQuote; formErrors: Record; isFormDisabled: boolean; setIsFormDisabled: (isFormDisabled: boolean) => void; }>; SaveDraftButton?: SlotProps<{ requestNegotiableQuote: typeof requestNegotiableQuote; formErrors: Record; isFormDisabled: boolean; setIsFormDisabled: (isFormDisabled: boolean) => void; }>; }; } ``` ### ErrorBanner slot The `ErrorBanner` slot allows you to customize the error banner section of the `RequestNegotiableQuoteForm` container. #### Example ```js await provider.render(RequestNegotiableQuoteForm, { slots: { ErrorBanner: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ErrorBanner'; ctx.appendChild(element); } } })(block); ``` ### SuccessBanner slot The `SuccessBanner` slot allows you to customize the success banner section of the `RequestNegotiableQuoteForm` container. #### Example ```js await provider.render(RequestNegotiableQuoteForm, { slots: { SuccessBanner: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom SuccessBanner'; ctx.appendChild(element); } } })(block); ``` ### Title slot The Title slot allows you to customize the title section of the `RequestNegotiableQuoteForm` container. #### Example ```js await provider.render(RequestNegotiableQuoteForm, { slots: { Title: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Title'; ctx.appendChild(element); } } })(block); ``` ### CommentField slot The `CommentField` slot allows you to customize the comment field section of the `RequestNegotiableQuoteForm` container. #### Example ```js await provider.render(RequestNegotiableQuoteForm, { slots: { CommentField: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom CommentField'; ctx.appendChild(element); } } })(block); ``` ### QuoteNameField slot The `QuoteNameField` slot allows you to customize the quote name field section of the `RequestNegotiableQuoteForm` container. #### Example ```js await provider.render(RequestNegotiableQuoteForm, { slots: { QuoteNameField: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom QuoteNameField'; ctx.appendChild(element); } } })(block); ``` ### RequestButton slot The `RequestButton` slot allows you to customize the request button section of the `RequestNegotiableQuoteForm` container. #### Example ```js await provider.render(RequestNegotiableQuoteForm, { slots: { RequestButton: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom RequestButton'; ctx.appendChild(element); } } })(block); ``` ### SaveDraftButton slot The `SaveDraftButton` slot allows you to customize the save draft button section of the `RequestNegotiableQuoteForm` container. #### Example ```js await provider.render(RequestNegotiableQuoteForm, { slots: { SaveDraftButton: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom SaveDraftButton'; ctx.appendChild(element); } } })(block); ``` --- # Quote Management styles Customize the Quote Management drop-in using CSS classes and design tokens. This page covers the Quote Management-specific container classes and customization examples. For comprehensive information about design tokens, responsive breakpoints, and styling best practices, see [Styling Drop-In Components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/). Version: 1.2.0 ## Customization example Add this to the CSS file of the specific https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/ where you're using the Quote Management drop-in. For a complete list of available design tokens (colors, spacing, typography, and more), see the [Design tokens reference](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/#design-tokens-reference). ```css title="styles/styles.css" del={2-3} ins={4-5} .attached-files-list { gap: var(--spacing-small, 8px); margin: var(--spacing-medium, 16px) 0; gap: var(--spacing-medium, 8px); margin: var(--spacing-large, 16px) 0; } ``` ## Container classes The Quote Management drop-in uses BEM-style class naming. Use the browser DevTools to inspect elements and find specific class names. ```css /* ActionsBar */ .quote-management-actions-bar {} .quote-management-actions-bar__button {} .quote-management-actions-bar__buttons {} .quote-management-actions-bar__container {} .quote-management-actions-bar__dropdown {} /* AttachedFilesList */ .attached-files-list {} .attached-files-list__error-icon {} .attached-files-list__item {} .attached-files-list__item--error {} .attached-files-list__item--success {} .attached-files-list__item--uploading {} .attached-files-list__item-error {} .attached-files-list__item-icon {} .attached-files-list__item-info {} .attached-files-list__item-main {} .attached-files-list__item-name {} .attached-files-list__item-size {} .attached-files-list__remove-button {} .attached-files-list__spinner {} .attached-files-list__success-icon {} /* ConfirmationModal */ .confirmation-modal__actions {} .confirmation-modal__banner {} .confirmation-modal__content {} .confirmation-modal__message {} .confirmation-modal__title {} .dropin-in-line-alert {} .dropin-modal {} .dropin-modal__body {} .dropin-modal__body--medium {} .dropin-modal__content {} .dropin-modal__header {} /* ItemsQuoted */ .quote-management-items-quoted {} /* LineItemNoteModal */ .dropin-in-line-alert {} .dropin-modal__close-button {} .dropin-modal__header-title-content {} .quote-management-line-item-note-modal {} .quote-management-line-item-note-modal__actions {} .quote-management-line-item-note-modal__cancel-button {} .quote-management-line-item-note-modal__confirm-button {} .quote-management-line-item-note-modal__content {} .quote-management-line-item-note-modal__details {} .quote-management-line-item-note-modal__details-table {} .quote-management-line-item-note-modal__discount {} .quote-management-line-item-note-modal__error-banner {} .quote-management-line-item-note-modal__error-text {} .quote-management-line-item-note-modal__form-field {} .quote-management-line-item-note-modal__helper-text {} .quote-management-line-item-note-modal__product-info {} .quote-management-line-item-note-modal__product-name {} .quote-management-line-item-note-modal__product-sku {} .quote-management-line-item-note-modal__quantity-input {} .quote-management-line-item-note-modal__stock {} .quote-management-line-item-note-modal__success-banner {} .quote-management-line-item-note-modal__table-error {} /* ManageNegotiableQuote */ .quote-management-manage-negotiable-quote {} .quote-management-manage-negotiable-quote__action-bar {} .quote-management-manage-negotiable-quote__attach-files {} .quote-management-manage-negotiable-quote__banner {} .quote-management-manage-negotiable-quote__detail {} .quote-management-manage-negotiable-quote__detail-content {} .quote-management-manage-negotiable-quote__detail-title {} .quote-management-manage-negotiable-quote__details {} .quote-management-manage-negotiable-quote__footer {} .quote-management-manage-negotiable-quote__header {} .quote-management-manage-negotiable-quote__item-actions {} .quote-management-manage-negotiable-quote__quote-actions {} .quote-management-manage-negotiable-quote__quote-comments-container {} .quote-management-manage-negotiable-quote__quote-comments-title {} .quote-management-manage-negotiable-quote__quote-name {} .quote-management-manage-negotiable-quote__quote-name-title {} .quote-management-manage-negotiable-quote__quote-name-wrapper {} .quote-management-manage-negotiable-quote__quote-status {} .quote-management-manage-negotiable-quote__rename-button {} .quote-management-manage-negotiable-quote__shipping-information-container {} .quote-management-manage-negotiable-quote__shipping-information-title {} /* ManageNegotiableQuoteTemplate */ .quote-management-manage-negotiable-quote-template {} .quote-management-manage-negotiable-quote-template__attach-files {} .quote-management-manage-negotiable-quote-template__banner {} .quote-management-manage-negotiable-quote-template__comments-container {} .quote-management-manage-negotiable-quote-template__comments-title {} .quote-management-manage-negotiable-quote-template__detail {} .quote-management-manage-negotiable-quote-template__detail-content {} .quote-management-manage-negotiable-quote-template__detail-title {} .quote-management-manage-negotiable-quote-template__details {} .quote-management-manage-negotiable-quote-template__file-error {} .quote-management-manage-negotiable-quote-template__footer {} .quote-management-manage-negotiable-quote-template__header {} .quote-management-manage-negotiable-quote-template__history-log-container {} .quote-management-manage-negotiable-quote-template__history-log-title {} .quote-management-manage-negotiable-quote-template__items-table {} .quote-management-manage-negotiable-quote-template__reference-documents {} .quote-management-manage-negotiable-quote-template__reference-documents-container {} .quote-management-manage-negotiable-quote-template__reference-documents-title {} .quote-management-manage-negotiable-quote-template__rename-button {} .quote-management-manage-negotiable-quote-template__shipping-information-container {} .quote-management-manage-negotiable-quote-template__shipping-information-title {} .quote-management-manage-negotiable-quote-template__template-name-title {} .quote-management-manage-negotiable-quote-template__template-name-wrapper {} .quote-management-manage-negotiable-quote-template__template-status {} /* OrderSummary */ .dropin-accordion-section__content-container {} .dropin-divider {} .quote-order-summary {} .quote-order-summary__caption {} .quote-order-summary__content {} .quote-order-summary__discount {} .quote-order-summary__divider-primary {} .quote-order-summary__divider-secondary {} .quote-order-summary__entry {} .quote-order-summary__heading {} .quote-order-summary__label {} .quote-order-summary__price {} .quote-order-summary__primary {} .quote-order-summary__secondary {} .quote-order-summary__shipping--edit {} .quote-order-summary__shipping--hide {} .quote-order-summary__shipping--state {} .quote-order-summary__shipping--zip {} .quote-order-summary__shippingLink {} .quote-order-summary__spinner {} .quote-order-summary__taxEntry {} .quote-order-summary__taxes {} .quote-order-summary__total {} /* OrderSummaryLine */ .quote-order-summary__label {} .quote-order-summary__label--bold {} .quote-order-summary__label--muted {} .quote-order-summary__price {} .quote-order-summary__price--bold {} .quote-order-summary__price--muted {} /* ProductListTable */ .quote-management-product-list-table-container {} .quote-management-product-list-table-container__submit-container {} .quote-management-product-list-table__bundle-option {} .quote-management-product-list-table__bundle-option-label {} .quote-management-product-list-table__bundle-option-value {} .quote-management-product-list-table__bundle-option-value-original-price {} .quote-management-product-list-table__bundle-option-values {} .quote-management-product-list-table__checkbox {} .quote-management-product-list-table__configurable-option {} .quote-management-product-list-table__configurable-option-label {} .quote-management-product-list-table__configurable-option-value {} .quote-management-product-list-table__discount-container {} .quote-management-product-list-table__note-content {} .quote-management-product-list-table__note-edit-icon {} .quote-management-product-list-table__note-item {} .quote-management-product-list-table__note-meta {} .quote-management-product-list-table__note-text {} .quote-management-product-list-table__notes-container {} .quote-management-product-list-table__notes-header {} .quote-management-product-list-table__notes-list {} .quote-management-product-list-table__notes-row-wrapper {} .quote-management-product-list-table__product-name {} .quote-management-product-list-table__product-name-container {} .quote-management-product-list-table__quantity {} .quote-management-product-list-table__quantity-input {} .quote-management-product-list-table__sku {} /* QuoteCommentsList */ .quote-management-quote-comments-list {} .quote-management-quote-comments-list__attachment-link {} .quote-management-quote-comments-list__attachments {} .quote-management-quote-comments-list__attachments-label {} .quote-management-quote-comments-list__author {} .quote-management-quote-comments-list__by {} .quote-management-quote-comments-list__date {} .quote-management-quote-comments-list__empty-state {} .quote-management-quote-comments-list__header {} .quote-management-quote-comments-list__item {} .quote-management-quote-comments-list__text {} /* QuoteHistoryLog */ .quote-management-quote-history-log {} .quote-management-quote-history-log__empty {} .quote-management-quote-history-log__entries {} .quote-management-quote-history-log__entry {} .quote-management-quote-history-log__entry-author {} .quote-management-quote-history-log__entry-change {} .quote-management-quote-history-log__entry-changes {} .quote-management-quote-history-log__entry-date {} .quote-management-quote-history-log__entry-header {} .quote-management-quote-history-log__entry-meta {} .quote-management-quote-history-log__entry-type {} /* QuotePricesSummary */ .quote-management-quote-prices-summary {} .quote-management-quote-prices-summary__accordion {} .quote-management-quote-prices-summary__entry {} .quote-management-quote-prices-summary__label {} .quote-management-quote-prices-summary__label--strong {} .quote-management-quote-prices-summary__value {} /* QuoteSummaryList */ .dropin-cart-item__quantity {} .quote-management-quote-summary-list {} .quote-management-quote-summary-list-accordion {} .quote-management-quote-summary-list-accordion__section {} .quote-management-quote-summary-list-footer__action {} .quote-management-quote-summary-list__background--secondary {} .quote-management-quote-summary-list__content {} .quote-management-quote-summary-list__heading {} .quote-management-quote-summary-list__heading--full-width {} .quote-management-quote-summary-list__heading-divider {} .quote-management-quote-summary-list__out-of-stock-message {} /* QuoteTemplatesListTable */ .quote-management-quote-templates-list-table {} .quote-management-quote-templates-list-table__actions-cell {} .quote-management-quote-templates-list-table__table {} .quote-templates-list-table__empty-state {} .quote-templates-list-table__footer {} .quote-templates-list-table__item-range {} .quote-templates-list-table__page-size-picker {} .quote-templates-list-table__pagination {} /* QuotesListTable */ .dropin-picker {} .dropin-picker__select {} .quote-management-quotes-list-table {} .quotes-list-table__empty-state {} .quotes-list-table__footer {} .quotes-list-table__item-range {} .quotes-list-table__page-size-picker {} .quotes-list-table__pagination {} /* ReferenceDocumentFormModal */ .dropin-in-line-alert {} .dropin-modal__close-button {} .quote-management-reference-document-form-modal {} .quote-management-reference-document-form-modal__actions {} .quote-management-reference-document-form-modal__cancel-button {} .quote-management-reference-document-form-modal__content {} .quote-management-reference-document-form-modal__error-banner {} .quote-management-reference-document-form-modal__error-text {} .quote-management-reference-document-form-modal__save-button {} .quote-management-reference-document-form-modal__success-banner {} /* ReferenceDocumentsList */ .quote-management-reference-documents-list {} .quote-management-reference-documents-list__add-button {} .quote-management-reference-documents-list__content {} .quote-management-reference-documents-list__document {} .quote-management-reference-documents-list__document-actions {} .quote-management-reference-documents-list__document-link {} .quote-management-reference-documents-list__edit-button {} .quote-management-reference-documents-list__empty {} .quote-management-reference-documents-list__header {} .quote-management-reference-documents-list__info-icon {} .quote-management-reference-documents-list__remove-button {} .quote-management-reference-documents-list__separator {} .quote-management-reference-documents-list__title {} /* RenameQuoteModal */ .dropin-in-line-alert {} .dropin-modal__close-button {} .dropin-modal__header-title-content {} .quote-management-rename-quote-modal {} .quote-management-rename-quote-modal__actions {} .quote-management-rename-quote-modal__cancel-button {} .quote-management-rename-quote-modal__content {} .quote-management-rename-quote-modal__error-banner {} .quote-management-rename-quote-modal__error-text {} .quote-management-rename-quote-modal__save-button {} .quote-management-rename-quote-modal__success-banner {} /* RequestNegotiableQuoteForm */ .request-negotiable-quote-form {} .request-negotiable-quote-form__actions {} .request-negotiable-quote-form__attach-file-field {} .request-negotiable-quote-form__error-banner {} .request-negotiable-quote-form__title {} /* ShippingAddressDisplay */ .quote-management-shipping-address-display {} .quote-management-shipping-address-display--empty {} .quote-management-shipping-address-display__field {} .quote-management-shipping-address-display__name {} .quote-management-shipping-address-display__no-address {} /* TabbedContent */ .quote-management-tabbed-content {} .quote-management-tabbed-content__active-tab-content {} .quote-management-tabbed-content__tab {} .quote-management-tabbed-content__tab--active {} .quote-management-tabbed-content__tabs {} ``` For the source CSS files, see the https://github.com/adobe-commerce/storefront-quote-management/tree/main/src. --- # Requisition List Containers The **Requisition List** drop-in provides pre-built container components for integrating into your storefront. Version: 1.4.0 ## What are Containers? Containers are pre-built UI components that combine functionality, state management, and presentation. They provide a complete solution for specific features and can be customized through props, slots, and CSS. ## Available Containers | Container | Description | | --------- | ----------- | | [RequisitionListForm](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/containers/requisition-list-form/) | Provides a form for creating or editing the requisition list's name and description. | | [RequisitionListGrid](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/containers/requisition-list-grid/) | Displays requisition lists in a grid layout with filtering, sorting, and selection capabilities. | | [RequisitionListHeader](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/containers/requisition-list-header/) | Displays header information for a requisition list including name, description, and action buttons. | | [RequisitionListSelector](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/containers/requisition-list-selector/) | Provides a selector interface for choosing a requisition list when adding products from the catalog. | | [RequisitionListView](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/containers/requisition-list-view/) | Displays the contents of a specific requisition list including items, quantities, and management actions. | | [ShareRequisitionListContent](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/containers/share-requisition-list-content/) | Provides the UI for sharing a requisition list with company colleagues via a generated link or by email. | | [SharedRequisitionList](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/containers/shared-requisition-list/) | Handles the recipient side of the sharing flow by loading a read-only preview from the token and letting the recipient import the list on demand. | > Each container is designed to work independently but can be composed together to create comprehensive user experiences. --- # RequisitionListForm Container Provides a form for creating or editing requisition list details including name and description. Version: 1.4.0 ## Configuration The `RequisitionListForm` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `mode` | `RequisitionListFormMode` | Yes | Sets the form mode to determine whether to create a new requisition list or update an existing one. Use `create` for new lists and `update` when modifying existing lists. Controls form behavior and validation rules. | | `requisitionListUid` | `string` | No | Specifies the unique identifier for the requisition list being updated. Required when mode is `update` to load and modify the existing list data from the backend. | | `defaultValues` | `RequisitionListFormValues` | No | Pre-populates form values for the requisition list. Use to prepopulate the form when creating from a template, duplicating an existing list, or restoring previously entered data. | | `onSuccess` | `function` | No | Callback function to handle successful form completion. Use to implement custom success handling, navigation, or notifications. | | `onError` | `function` | No | Callback function to handle errors when form submission fails. Use to implement custom error handling, logging, or user notifications. | | `onCancel` | `function` | Yes | Callback function to handle form cancellation when users cancel the form. Use to implement navigation back to the list view or close modal dialogs. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `RequisitionListForm` container: ```js await provider.render(RequisitionListForm, { mode: undefined, // Optional - omit to use drop-in state onCancel: (cancel) => console.log('Cancel', cancel), requisitionListUid: "abc-123", // Get from URL params or context })(block); ``` --- # RequisitionListGrid Container Displays requisition lists in a grid layout with filtering, sorting, and selection capabilities. Version: 1.4.0 ## Configuration The `RequisitionListGrid` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `routeRequisitionListDetails` | `function` | No | Generates the URL for navigating to the requisition list details page. Returns a URL string or performs navigation. Use to implement custom routing logic, add query parameters when users click on a list, or integrate with your application's routing system. | | `fallbackRoute` | `string` | No | Fallback URL to redirect when requisition lists are not enabled. Defaults to '/`customer/account`' | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container exposes the following slots for customization: | Slot | Type | Required | Description | |------|------|----------|-------------| | `Header` | `SlotProps` | No | Customize grid header section. | ## Usage The following example demonstrates how to use the `RequisitionListGrid` container: ```js await provider.render(RequisitionListGrid, { routeRequisitionListDetails: (uid) => `/customer/requisition-lists/${uid}`, slots: { // Add custom slot implementations here } })(block); ``` --- # RequisitionListHeader Container Displays header information for a requisition list including name, description, and action buttons. Version: 1.4.0 ## Configuration The `RequisitionListHeader` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `requisitionList` | `RequisitionList` | Yes | Provides the requisition list data object containing name, description, and metadata. Required to display the list information in the header. Contains all list details needed for rendering the header section. | | `routeRequisitionListGrid` | `function` | No | Generates the URL for navigating back to the requisition list grid view. Returns a URL string or performs navigation. Use to implement breadcrumb navigation, back buttons, or integrate with your application's routing system. | | `onUpdate` | `function` | No | Callback function to handle requisition list updates when the name or description changes. Use to refresh the parent component or show success notifications. | | `onAlert` | `function` | No | Callback function to handle alert notifications when alerts are displayed. | | `enrichConfigurableProducts` | `function` | No | Enriches the configurable products contained in requisition list items. Takes an array of items and returns the same array with configured product data attached. | | `currentCustomerEmail` | `string` | No | Email address of the currently authenticated customer. When provided, this user is excluded from the list of selectable share recipients inside the share modal. | | `routeSharedRequisitionList` | `(relativeUrl: string) => string` | No | Called with the relative share URL to format the final share link. The URL is built as `/?requisition_id=`, using the `requisition_list_share_storefront_path` store configuration (default: `customer/requisition-list-sharing`). Sharing is available only when `requisition_list_sharing_enabled` is `true`. Use this callback to prepend the domain, modify the path, or apply custom formatting. By default, the relative URL is used as-is. Example: `(url) => window.location.origin + url` for absolute URLs, or `(url) => url` to keep relative URLs. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `RequisitionListHeader` container: ```js await provider.render(RequisitionListHeader, { requisitionList: undefined, // Auto-populated from drop-in state, or provide explicitly routeRequisitionListGrid: () => '/customer/requisition-lists', onUpdate: (update) => console.log('Update', update), currentCustomerEmail: 'current.user@example.com', routeSharedRequisitionList: (url) => window.location.origin + url, })(block); ``` --- # RequisitionListSelector Container Provides a selector interface for choosing a requisition list when adding products from the catalog. Version: 1.4.0 ## Configuration The `RequisitionListSelector` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `canCreate` | `boolean` | No | Controls whether users can create new requisition lists from the selector dropdown. The default value is `true` if you don't set this parameter to `false`, which restricts users from adding products to the existing lists. The `false` setting is useful when list creation should happen through a separate flow or requires additional permissions. | | `sku` | `string` | Yes | Specifies the product SKU to add to the selected requisition list. Required to identify the exact product variant being added. Must match a valid product SKU in your catalog. | | `selectedOptions` | `string[]` | No | Provides an array of selected product option IDs for configurable products. Captures variant selections such as size, color, or other configurable attributes. Required for configurable products to identify the specific variant being added. | | `quantity` | `number` | No | Sets the quantity of the product to add to the requisition list. Defaults to 1 if not specified. Use to allow bulk additions or pre-populate quantities from previous orders or saved preferences. | | `matchBySKU` | `boolean` | No | Controls how the active state is determined: If set to `true`, it only checks the product SKU. If set to `false`, it checks both the SKU and the selected configurable option UIDs. By default, it uses SKU-only matching (true). Use `false` on product detail pages (PDP) for configurable products so the button is only active when the exact variant (same SKU and selected options) is in the requisition list. Use `true` on product listing pages (PLP), where specific variants can't be selected. | | `beforeAddProdToReqList` | `function` | No | Callback function to handle validation before the Add to Requisition List dropdown opens when the button is clicked. Use to validate if the product can be added directly (for example, check if a configurable product needs options selected) and redirect to the product detail page if needed. If the callback throws an error or rejects, the dropdown will not open, enabling patterns like redirecting complex products to their detail pages. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `RequisitionListSelector` container: ```js await provider.render(RequisitionListSelector, { sku: product.sku, quantity: pdpApi.getProductConfigurationValues()?.quantity || 1, selectedOptions: currentOptions, beforeAddProdToReqList: async () => { // Check if all required product options are selected const needsOptionSelection = !validateRequiredOptions(product, currentOptions); if (needsOptionSelection) { // Show inline alert if (inlineAlert) { inlineAlert.remove(); } inlineAlert = await UI.render(InLineAlert, { heading: labels.Global?.SelectProductOptionsBeforeRequisition || 'Please select product options', description: labels.Global?.SelectProductOptionsBeforeRequisitionDescription || 'Please select all required product options before adding to a requisition list.', icon: h(Icon, { source: 'Warning' }), type: 'warning', variant: 'secondary', 'aria-live': 'assertive', role: 'alert', onDismiss: () => { if (inlineAlert) { inlineAlert.remove(); inlineAlert = null; } }, })($alert); // Scroll the alert into view setTimeout(() => { $alert.scrollIntoView({ behavior: 'smooth', block: 'center', }); }, 100); // Throw error to prevent modal from opening throw new Error('Product options must be selected'); } } })(block); ``` --- # RequisitionListView Container Displays the contents of a specific requisition list, including items, quantities, and management actions. Provides functionality to view products, update quantities, delete items, add items to cart, and manage the requisition list itself. ![RequisitionListView container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins-b2b/requisition-list/requisition-list-view.png) *RequisitionListView container* Version: 1.4.0 ## Configuration The `RequisitionListView` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `requisitionListUid` | `string` | Yes | Specifies the UID of the requisition list to display. Must be a base64-encoded string. If an invalid UID is provided, renders the `NotFound` state. Fetches the requisition list data internally using this identifier. | | `skipProductLoading` | `boolean` | No | Controls whether to skip automatic product data fetching on component mount. Set to true in test environments to prevent API calls or when product data is loaded externally. | | `pageSize` | `number` | No | Sets the number of items displayed per page for pagination. Controls how many requisition list items appear in each page view. Defaults to DEFAULT_PAGE_SIZE if not specified. | | `selectedItems` | `Set` | Yes | Provides a Set of selected item UIDs for batch operations. Tracks which items are selected for actions like adding to cart or deleting. Required to enable multi-select functionality. | | `routeRequisitionListGrid` | `function` | No | Generates the URL for navigating back to the requisition list grid view. Use to implement breadcrumb navigation, back buttons, or custom routing logic that preserves query parameters or application state. | | `fallbackRoute` | `string` | No | Sets the fallback URL to redirect when requisition lists are not enabled or unavailable. Defaults to '/`customer/account`'. Use to provide graceful degradation when B2B features are disabled. | | `getProductData` | `function` | Yes | Fetches products by SKU from the catalog service. Takes an array of SKUs and returns an array of products with all their data. | | `enrichConfigurableProducts` | `function` | Yes | Enriches the configurable products contained in requisition list items. Takes an array of items and returns the same array with configured product data attached. | | `initialData` | `object` | No | Preloaded data for the model before backend data is fetched. Use for testing, SSR, or improving initial load. | | `currentCustomerEmail` | `string` | No | Email address of the currently authenticated customer. Passed through to the embedded `RequisitionListHeader` to exclude the current user from the share-by-email recipient list. | | `routeSharedRequisitionList` | `(relativeUrl: string) => string` | No | Receives the relative share URL and returns the final URL to display in the share modal's copy-link field. Passed through to the embedded `RequisitionListHeader`. Use to produce an absolute URL. Example: `(relativeUrl) => window.location.origin + relativeUrl`. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `RequisitionListView` container: ```js await provider.render(RequisitionListView, { requisitionListUid, routeRequisitionListGrid: () => `/customer/requisition-lists` currentCustomerEmail: 'current.user@example.com', routeSharedRequisitionList: (relativeUrl) => `${window.location.origin}${relativeUrl}`, })(block); ``` --- # ShareRequisitionListContent Container The `ShareRequisitionListContent` container renders the share UI for a requisition list. You can share by email (select company colleagues as recipients) or by copy link (generate a URL recipients use to import the list). Version: 1.4.0 ## Configuration | Parameter | Type | Req? | Description | |---|---|---|---| | `requisitionListUid` | `string` | Yes | UID of the requisition list to share. The container calls [`shareRequisitionListByToken`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/functions/#sharerequisitionlistbytoken) to build the copy-link URL and passes this UID to `onSubmit` for the email flow. | | `isSubmitting` | `boolean` | Yes | Controls the submitting state of the form. Set to `true` while the share-by-email request is in progress to disable the submit button and prevent duplicate submissions. | | `onSubmit` | `(customerUids: string[]) => Promise \| null>` | Yes | Called when the user clicks **Submit** with the selected recipient UIDs. Return `null` on success or an array of error objects on failure. Typically wraps [`shareRequisitionListByEmail`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/functions/#sharerequisitionlistbyemail). | | `currentCustomerEmail` | `string` | No | Email address of the currently authenticated customer. When provided, the container excludes this address from the selectable company recipients so the sender cannot share with themselves. | | `routeSharedRequisitionList` | `(relativeUrl: string) => string` | No | Receives the relative share URL (for example, `/customer/requisition-list-sharing?requisition_id=` from `requisition_list_share_storefront_path`) and returns the final URL to display. Use this to convert the relative URL to an absolute URL. If you omit this prop, the container uses the relative URL as-is. Example: `(relativeUrl) => window.location.origin + relativeUrl` | ## Slots This container does not expose any customizable slots. ## Usage [`RequisitionListHeader`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/containers/requisition-list-header/) embeds this container in a share modal. When `requisition_list_sharing_enabled` is `true` in store configuration, the header shows a **Share** action button that opens the modal. The following example renders the container on its own (outside the header modal): ```js await provider.render(ShareRequisitionListContent, { requisitionListUid: 'YOUR_LIST_UID', isSubmitting: false, onSubmit: async (customerUids) => { return shareRequisitionListByEmail('YOUR_LIST_UID', customerUids); }, currentCustomerEmail: 'current.user@example.com', routeSharedRequisitionList: (relativeUrl) => window.location.origin + relativeUrl, })(block); ``` Replace `YOUR_LIST_UID` with the UID of the list you want to share. > The container reads `requisition_list_sharing_enabled`, `requisition_list_share_max_recipients`, and `requisition_list_share_storefront_path` from the store configuration (loaded when the drop-in initializes). It also calls [`getCompanyUsers`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/functions/#getcompanyusers) to populate the email recipient list. If your Commerce instance does not yet expose these GraphQL fields, sharing is disabled. --- # SharedRequisitionList Container The `SharedRequisitionList` container handles the recipient side of requisition list sharing. When a shopper opens a shared link with a `requisition_id` token, render this container to show a read-only preview. The shopper imports the list only after they choose the import action. Version: 1.4.0 ## Configuration | Parameter | Type | Req? | Description | |---|---|---|---| | `token` | `string` | Yes | The share token from the URL query parameter (for example, `?requisition_id=`). The container loads preview data with [`getSharedRequisitionList`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/functions/#getsharedrequisitionlist) on mount and calls [`importSharedRequisitionList`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/functions/#importsharedrequisitionlist) only when the recipient confirms import. | | `routeRequisitionList` | `(uid: string, listName: string) => string \| void` | No | Called with the imported list UID and name after a successful import. Use this callback to navigate to the list detail page. If you omit it, the container shows an inline success alert instead. Pass a stable reference (module-level function or `useCallback` with no dependencies) because the container captures this callback at mount time. | > To show a success alert on the detail page after redirect, store the alert payload in `localStorage` before you navigate. The `useRequisitionListAlert` hook reads from `localStorage` on mount and shows the saved message automatically. ```js routeRequisitionList: (uid, listName) => { localStorage.setItem( 'requisitionListPendingAlert', JSON.stringify({ action: 'import', type: 'success', context: 'requisitionList', listName }) ); window.location.href = `/b2b/requisition-list?uid=${uid}`; }, ``` ## Slots This container does not expose any customizable slots. ## Usage Create a storefront page that matches the path in `requisition_list_share_storefront_path` (default: `customer/requisition-list-sharing`). Read the `requisition_id` query parameter from the URL and pass it as `token`. ```js const params = new URLSearchParams(window.location.search); const token = params.get('requisition_id') ?? ''; await provider.render(SharedRequisitionList, { token, routeRequisitionList: (uid, listName) => { localStorage.setItem( 'requisitionListPendingAlert', JSON.stringify({ action: 'import', type: 'success', context: 'requisitionList', listName, }) ); window.location.href = `/b2b/requisition-list?uid=${uid}`; }, })(block); ``` --- # Requisition List Dictionary The **Requisition List dictionary** contains all user-facing text, labels, and messages displayed by this drop-in. Customize the dictionary to: - **Localize** the drop-in for different languages and regions - **Customize** labels and messages to match your brand voice - **Override** default text without modifying source code for the drop-in Dictionaries use the **i18n (internationalization)** pattern, where each text string is identified by a unique key path. Version: 1.4.0 ## How to customize Override dictionary values during drop-in initialization. The drop-in deep-merges your custom values with the defaults. ```javascript await initialize({ langDefinitions: { en_US: { "RequisitionList": { "containerTitle": { "0": "Custom value", "1": "Custom value" } } } } }); ``` You only need to include the keys you want to change. For multi-language support and advanced patterns, see the [Dictionary customization guide](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/dictionaries/). ## Default keys and values Below are the default English (`en_US`) strings provided by the **Requisition List** drop-in: ```json title="en_US.json" { "RequisitionList": { "containerTitle": "Requisition Lists", "RequisitionListWrapper": { "name": "Name & Description", "itemsCount": "Items", "lastUpdated": "Latest activity", "actions": "Actions", "loginMsg": "Please login", "deleteRequisitionListTitle": "Are you sure you want to delete this Requisition List?", "deleteRequisitionListMessage": "Requisition List will be permanently deleted. This action can not be undone.", "confirmAction": "Confirm", "cancelAction": "Cancel", "emptyList": "No Requisition Lists found" }, "AddNewReqList": { "addNewReqListBtn": "Add new Requisition List" }, "RequisitionListItem": { "actionUpdate": "Update", "actionDelete": "Delete" }, "RequisitionListForm": { "actionCancel": "Cancel", "actionSave": "Save", "requiredField": "This is a required field.", "nameMinLength": "Name must be at least {min} characters long.", "nameInvalidCharacters": "Name contains invalid characters. Only letters, numbers, spaces, and basic punctuation are allowed.", "floatingLabel": "Requisition List Name *", "placeholder": "Requisition List Name", "label": "Description", "updateTitle": "Update Requisition List", "createTitle": "Create Requisition List", "addToRequisitionList": "Add to Requisition List" }, "RequisitionListSelector": { "addToNewRequisitionList": "Add to New Requisition List", "addToSelected": "Add to Selected List" }, "RequisitionListAlert": { "errorCreate": "Error creating requisition list.", "successCreate": "Requisition list created successfully.", "errorAddToCart": "Error adding item to cart.", "successAddToCart": "Item(s) added to cart successfully.", "errorUpdateQuantity": "Error updating quantity.", "successUpdateQuantity": "Item quantity updated successfully.", "errorUpdate": "Error updating requisition list.", "successUpdate": "Requisition list updated successfully.", "errorDeleteItem": "Error deleting item.", "successDeleteItem": "Item(s) deleted successfully.", "errorDeleteReqList": "Error deleting requisition list.", "successDeleteReqList": "Requisition list deleted successfully.", "errorMove": "Error moving item(s) to cart.", "successMove": "Item(s) successfully moved to cart.", "partialMoveSuccess": "{successCount} product(s) successfully added and {failedCount} product(s) couldn't be added to your shopping cart.", "errorAddToRequisitionList": "Error adding item(s) to requisition list.", "successAddToRequisitionList": "Item(s) successfully added to requisition list.", "errorMoveToList": "Error moving item(s) to requisition list.", "successMoveToList": "Item(s) successfully moved to {listName}.", "errorCopyToList": "Error copying item(s) to requisition list.", "successCopyToList": "Item(s) successfully copied to {listName}.", "errorImport": "Error importing requisition list.", "successImport": "Requisition list \"{listName}\" has been added to your account." }, "SharedRequisitionList": { "loading": "Loading shared requisition list...", "previewTitle": "Shared Requisition List", "senderLabel": "Shared by", "listNameLabel": "List name", "descriptionLabel": "Description", "itemsCountLabel": "Items", "importButton": "Import List", "importingButton": "Importing...", "errorPreview": "Unable to load the shared requisition list.", "skuHeader": "SKU", "qtyHeader": "Qty", "optionsHeader": "Options" }, "RequisitionListView": { "actionDelete": "Delete", "statusDeleting": "Deleting...", "actionDeleteSelected": "Delete Selected", "actionDeleteSelectedItems": "Delete selected items", "actionSelect": "Select", "actionSelectAll": "Select All", "actionSelectNone": "Select None", "actionAddToCart": "Add to Cart", "statusAddingToCart": "Adding...", "actionAddSelectedToCart": "Add Selected to Cart", "statusBulkAddingToCart": "Adding to Cart...", "actionUpdateQuantity": "Update", "statusUpdatingQuantity": "Updating...", "errorUpdateQuantity": "Error updating quantity", "successUpdateQuantity": "Item quantity updated successfully.", "actionBackToRequisitionListsOverview": "Back to requisition lists overview", "actionBackToRequisitionLists": "Back to Requisition Lists", "actionMoveToList": "Move to List", "moveToListTitle": "Move to Requisition List", "moveToListConfirm": "Move to requisition list", "actionCopyToList": "Copy to List", "copyToListTitle": "Copy to Requisition List", "copyToListConfirm": "Copy to requisition list", "actionRename": "Rename", "actionDeleteList": "Delete List", "actionShare": "Share", "shareDisabledReason": "You cannot share an empty requisition list.", "shareDisabledNoCompany": "Sharing is available only for company account users.", "shareListTitle": "Share Requisition List", "deleteListTitle": "Delete Requisition List?", "deleteListMessage": "Are you sure you want to delete this requisition list? This action cannot be undone.", "deleteItemsTitle": "Delete Item(s)?", "deleteItemsMessage": "Are you sure you want to delete the selected item(s) from this requisition list? This action cannot be undone.", "confirmAction": "Delete", "cancelAction": "Cancel", "emptyRequisitionList": " Requisition List is empty", "productListTable": { "headers": { "productName": "Product name", "sku": "SKU", "price": "Price", "quantity": "Quantity", "subtotal": "Subtotal", "actions": "Actions" }, "itemQuantity": "Item quantity", "outOfStock": "Out of stock", "onlyXLeftInStock": "Only {count} left in stock" }, "errorLoadPage": "Failed to load page", "errorLoadingProducts": "Failed to load product data", "notFoundTitle": "Requisition List Not Found", "notFoundMessage": "The requisition list you are looking for does not exist or you do not have access to it.", "notFoundActionLabel": "Back to Requisition Lists" }, "ShareRequisitionListContent": { "emailInstruction": "Select below the email addresses of the people with whom you'd like to share your list. Your name, email address and the details of your list will be shared with recipients. You can send your list with multiple recipients. Please note only registered users will be able to import lists.", "emailLabel": "Email addresses", "emailPlaceholder": "Select company users", "submitLabel": "Submit", "linkInstruction": "Copy the link below and send it to people with whom you'd like to share your list. Please note only registered users will be able to import lists.", "copyLink": "Copy Link", "linkCopied": "Copied!", "loadingUsers": "Loading users...", "loadingLink": "Generating share link...", "noUsersAvailable": "No company colleagues available to share with.", "usersLoadError": "Unable to load company users. Please try again.", "maxRecipientsValidation": "You can select up to {max} recipients.", "shareSuccessMessage": "You have shared your list with the below email addresses:" }, "RequisitionListsNotEnabled": { "title": "Requisition Lists Not Available", "message": "Requisition Lists are not available. Please contact your administrator for more information.", "actionLabel": "Go to My Account" }, "PageSizePicker": { "show": "Show", "itemsPerPage": "Items per page" }, "PaginationItemsCounter": { "itemsCounter": "Items {from}-{to} of {total}" } } } ``` --- # Requisition List Events and Data The **Requisition List** drop-in uses the [event bus](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/events/) to emit and listen to events for communication between drop-ins and external integrations. Version: 1.4.0 ## Events reference {/* EVENTS_TABLE_START */} | Event | Direction | Description | |-------|-----------|-------------| | [requisitionList/alert](#requisitionlistalert-emits-and-listens) | Emits and listens | Emitted when an alert or notification is triggered. | | [requisitionList/data](#requisitionlistdata-emits-and-listens) | Emits and listens | Emitted when data is available or changes. | | [requisitionList/initialized](#requisitionlistinitialized-emits-and-listens) | Emits and listens | Emitted when the component completes initialization. | | [requisitionLists/data](#requisitionlistsdata-emits-and-listens) | Emits and listens | Emitted when data is available or changes. | {/* EVENTS_TABLE_END */} ## Event details The following sections provide detailed information about each event, including its direction, event payload, and usage examples. ### `requisitionList/alert` (emits and listens) Emitted when the drop-in shows an alert or notification related to requisition list actions. #### Event payload ```typescript RequisitionListActionPayload ``` See [`RequisitionListActionPayload`](#requisitionlistactionpayload) for full type definition. #### Example ```js events.on('requisitionList/alert', (payload) => { console.log('requisitionList/alert event received:', payload); // Add your custom logic here }); ``` ### `requisitionList/data` (emits and listens) Triggered when data is available or changes. It emits and listens for updates to a single requisition list. #### Event payload ```typescript RequisitionList | null ``` See [`RequisitionList`](#requisitionlist) for full type definition. #### Example ```js events.on('requisitionList/data', (payload) => { console.log('requisitionList/data event received:', payload); // Add your custom logic here }); ``` ### `requisitionList/initialized` (emits and listens) Triggered when the component completes initialization. #### Event payload #### Example ```js events.on('requisitionList/initialized', (payload) => { console.log('requisitionList/initialized event received:', payload); // Add your custom logic here }); ``` ### `requisitionLists/data` (emits and listens) Triggered when data is available or changes. It emits and listens for updates to the collection of requisition lists. #### Event payload ```typescript RequisitionList[] | null ``` See [`RequisitionList`](#requisitionlist) for full type definition. #### Example ```js events.on('requisitionLists/data', (payload) => { console.log('requisitionLists/data event received:', payload); // Add your custom logic here }); ``` ## Data Models The following data models are used in event payloads for this drop-in. ### RequisitionList Used in: [`requisitionList/data`](#requisitionlistdata-emits-and-listens), [`requisitionLists/data`](#requisitionlistsdata-emits-and-listens). ```ts interface RequisitionList { uid: string; name: string; description: string; updated_at: string; items_count: number; items: Item[]; page_info?: PageInfo; } ``` ### RequisitionListActionPayload Used in: [`requisitionList/alert`](#requisitionlistalert-emits-and-listens). ```ts interface RequisitionListActionPayload { action: 'add' | 'delete' | 'update' | 'move' | 'moveToList' | 'copyToList' | 'create' | 'import'; type: 'success' | 'error'; context: 'product' | 'requisitionList'; skus?: string[]; // for product-related actions message?: string[]; // for uncontrolled/custom messages listName?: string; // for import success message substitution } ``` --- # Requisition List Functions The Requisition List drop-in provides **18 API functions** for managing requisition lists and their items, including creating lists, adding/removing products, managing list metadata, and sharing lists with company colleagues. Version: 1.4.0 | Function | Description | | --- | --- | | [`addProductsToRequisitionList`](#addproductstorequisitionlist) | Adds products to a requisition list. | | [`addRequisitionListItemsToCart`](#addrequisitionlistitemstocart) | Adds the chosen items from a requisition list to the logged-in user's cart. | | [`copyItemsBetweenRequisitionLists`](#copyitemsbetweenrequisitionlists) | Copies items from one requisition list to another for a logged-in user. | | [`createRequisitionList`](#createrequisitionlist) | Creates a new requisition list with the name and description provided for the logged-in user. | | [`deleteRequisitionList`](#deleterequisitionlist) | Deletes a requisition list identified by UID. | | [`deleteRequisitionListItems`](#deleterequisitionlistitems) | Deletes items from a requisition list. | | [`enrichConfigurableProducts`](#enrichconfigurableproducts) | Resolves the selected variants for configurable product items. | | [`getRequisitionList`](#getrequisitionlist) | Returns information about the requested requisition list for the logged-in user. | | [`getRequisitionLists`](#getrequisitionlists) | Returns the requisition lists for the logged-in user. | | [`getStoreConfig`](#getstoreconfig) | Returns details about the store configuration. | | [`moveItemsBetweenRequisitionLists`](#moveitemsbetweenrequisitionlists) | Moves items from one requisition list to another for a logged-in user. | | [`updateRequisitionList`](#updaterequisitionlist) | Updates an existing requisition list with the name and description provided for the logged-in user. | | [`updateRequisitionListItems`](#updaterequisitionlistitems) | Updates the items of an existing requisition list with the quantity and options provided for the logged-in user. | | [`getCompanyUsers`](#getcompanyusers) | Returns a list of active company users for the current company. | | [`getSharedRequisitionList`](#getsharedrequisitionlist) | Retrieves a shared requisition list by its share token. | | [`importSharedRequisitionList`](#importsharedrequisitionlist) | Imports a shared requisition list into the current customer's account using a share token. | | [`shareRequisitionListByEmail`](#sharerequisitionlistbyemail) | Shares a requisition list with one or more company users by email. | | [`shareRequisitionListByToken`](#sharerequisitionlistbytoken) | Generates a shareable token for a requisition list. | ## addProductsToRequisitionList Adds products to a requisition list. ```ts const addProductsToRequisitionList = async ( requisitionListUid: string, requisitionListItems: Array ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `requisitionListUid` | `string` | Yes | The unique identifier for the requisition list to which products will be added. | | `requisitionListItems` | `Array` | Yes | An array of product objects to add to the requisition list. Each object includes the product SKU, quantity, and optional configuration like parent SKU for configurable products, selected options (color, size), and entered options (custom text fields). | ### Events Emits the `requisitionList/data` event. ### Returns Returns [`RequisitionList`](#requisitionlist) or `null`. ## addRequisitionListItemsToCart Adds the chosen items from a requisition list to the logged-in user's cart. ```ts const addRequisitionListItemsToCart = async ( requisitionListUid: string, requisitionListItemUids: Array ): Promise | null> ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `requisitionListUid` | `string` | Yes | The unique identifier for the requisition list containing the items to add to the cart. | | `requisitionListItemUids` | `Array` | Yes | An array of requisition list item UIDs to add to the cart. These are the unique identifiers for specific items within the list, not product SKUs. | ### Events Does not emit any drop-in events. ### Returns Returns `Array | null`. ## createRequisitionList Creates a new requisition list with the name and description provided for the logged-in user. ```ts const createRequisitionList = async ( name: string, description?: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `name` | `string` | Yes | The display name for the new requisition list. This helps users identify and organize their lists (for example, `Office Supplies Q1`, `Weekly Inventory Restock`). | | `description` | `string` | No | An optional text description providing additional context about the requisition list's purpose or contents (for example, `Monthly recurring orders for maintenance supplies`). | ### Events Emits the `requisitionList/data` event. ### Returns Returns [`RequisitionList`](#requisitionlist) or `null`. ## deleteRequisitionList Deletes a requisition list identified by uid. ```ts const deleteRequisitionList = async ( requisitionListUid: string ): Promise<{ items: RequisitionList[]; page_info: any; status: any; } | null> ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `requisitionListUid` | `string` | Yes | The unique identifier for the requisition list to delete. This operation is permanent and removes the list and all its items. | ### Events Emits the `requisitionLists/data` event. ### Returns ```ts Promise<{ items: RequisitionList[]; page_info: any; status: any; } | null> ``` See [`RequisitionList`](#requisitionlist). ## deleteRequisitionListItems Deletes items from a requisition list. ```ts const deleteRequisitionListItems = async ( requisitionListUid: string, items: Array, pageSize: number, currentPage: number, enrichConfigurableProducts?: (items: Item[]) => Promise ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `requisitionListUid` | `string` | Yes | The unique identifier for the requisition list from which items will be removed. | | `items` | `Array` | Yes | An array of requisition list item UIDs to remove. These are the unique identifiers returned in the list's items array, not product SKUs. | | `pageSize` | `number` | Yes | The number of items to return per page in the updated requisition list response. | | `currentPage` | `number` | Yes | The page number for pagination (1-indexed). Used to retrieve a specific page of items after deletion. | | `enrichConfigurableProducts` | `items: Item[]` | No | See function signature above | ### Events Emits the `requisitionList/data` event. ### Returns Returns [`RequisitionList`](#requisitionlist) or `null`. ## enrichConfigurableProducts Resolves the selected variants for configurable product items in a requisition list. The function uses each item's selected option labels to retrieve the matching simple product so the item can display the selected variant's SKU, price, and thumbnail. ```ts const enrichConfigurableProducts = async ( items: Item[] ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `items` | `Item[]` | Yes | The requisition list items to enrich. Non-configurable items are returned unchanged. | ### Events Does not emit any drop-in events. ### Returns Returns the items with selected configurable product details attached. See the [`RequisitionListItemModel`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/initialization/#requisitionlistitemmodel). ## getRequisitionList Returns information about the requested requisition list for the logged-in user. ```ts const getRequisitionList = async ( requisitionListID: string, currentPage?: number, pageSize?: number, enrichConfigurableProducts?: (items: Item[]) => Promise ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `requisitionListID` | `string` | Yes | The unique identifier for the requisition list to retrieve. Returns the list metadata and all items. | | `currentPage` | `number` | No | The page number for pagination (1-indexed). Defaults to page 1 if not specified. | | `pageSize` | `number` | No | The number of items to return per page. Controls pagination of the requisition list items. | | `enrichConfigurableProducts` | `items: Item[]` | No | See function signature above | ### Events Emits the `requisitionList/data` event. ### Returns Returns [`RequisitionList`](#requisitionlist) or `null`. ## getRequisitionLists Returns the requisition lists for the logged-in user. ```ts const getRequisitionLists = async ( currentPage?: number, pageSize?: number, listItemsPageSize?: number ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `currentPage` | `number` | No | The page number for pagination (1-indexed). Used to navigate through multiple pages of requisition lists. | | `pageSize` | `number` | No | The number of requisition lists to return per page. Controls how many lists appear on each page. | | `listItemsPageSize` | `number` | No | Sets how many items are loaded per list in each GraphQL request. The default is `100`. If a list has more than 100 items, additional requests are automatically made so users (like on a PDP “already on list” view) can see all items. | ### Events Emits the `requisitionLists/data` event. ### Returns Returns an array of [`RequisitionList`](#requisitionlist) objects or `null`. ## getStoreConfig Returns details about the store configuration. ```ts const getStoreConfig = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## updateRequisitionList Updates an existing requisition list with the name and description provided for the logged-in user. ```ts const updateRequisitionList = async ( requisitionListUid: string, name: string, description?: string, pageSize?: number, currentPage?: number, enrichConfigurableProducts?: (items: Item[]) => Promise ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `requisitionListUid` | `string` | Yes | The unique identifier for the requisition list to update. | | `name` | `string` | Yes | The new display name for the requisition list. Updates the list's title. | | `description` | `string` | No | The new text description for the requisition list. Updates the list's purpose or context information. | | `pageSize` | `number` | No | The number of items to return per page in the updated requisition list response. | | `currentPage` | `number` | No | The page number for pagination (1-indexed) in the updated requisition list response. | | `enrichConfigurableProducts` | `items: Item[]` | No | See function signature above | ### Events Emits the `requisitionList/data` event. ### Returns Returns [`RequisitionList`](#requisitionlist) or `null`. ## updateRequisitionListItems Updates the items of an existing requisition list with the quantity and options provided for the logged-in user. ```ts const updateRequisitionListItems = async ( requisitionListUid: string, requisitionListItems: Array, pageSize: number, currentPage: number, enrichConfigurableProducts?: (items: Item[]) => Promise ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `requisitionListUid` | `string` | Yes | The unique identifier for the requisition list containing the items to update. | | `requisitionListItems` | `Array` | Yes | An array of requisition list items to update. Each object includes the item UID and the fields to modify (such as quantity, selected options, or entered options). | | `pageSize` | `number` | Yes | The number of items to return per page in the updated requisition list response. | | `currentPage` | `number` | Yes | The page number for pagination (1-indexed) in the updated requisition list response. | | `enrichConfigurableProducts` | `items: Item[]` | No | See function signature above | ### Events Emits the `requisitionList/data` event. ### Returns Returns [`RequisitionList`](#requisitionlist) or `null`. ## moveItemsBetweenRequisitionLists Moves items from one requisition list to another for a logged-in user. ```ts const moveItemsBetweenRequisitionLists = async ( sourceRequisitionListUid: string, destinationRequisitionListUid: string, requisitionListItemUids: string[], pageSize: number, currentPage: number ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `sourceRequisitionListUid` | `string` | Yes | The unique identifier for the requisition list from which items will be moved. | | `destinationRequisitionListUid` | `string` | Yes | The unique identifier for the requisition list to which items will be moved. | | `requisitionListItemUids` | `string[]` | Yes | An array of requisition list item UIDs to move. These are the unique identifiers for specific items within the source list. | | `pageSize` | `number` | Yes | The number of items to return per page in the updated requisition list responses. | | `currentPage` | `number` | Yes | The page number for pagination (1-indexed) in the updated requisition list responses. | ### Events Emits the `requisitionList/data` event for the source list after items are moved. ### Returns Returns an object with the following structure, or `null` if the operation fails: ```ts interface MoveItemsResult { sourceList: RequisitionList | null; destinationList: RequisitionList | null; } ``` - `sourceList`: The updated source requisition list after items have been moved, or `null` if not available. - `destinationList`: The updated destination requisition list after items have been added, or `null` if not available. ## copyItemsBetweenRequisitionLists Copies items from one requisition list to another for a logged-in user. ```ts const copyItemsBetweenRequisitionLists = async ( sourceRequisitionListUid: string, destinationRequisitionListUid: string, requisitionListItemUids: string[] ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `sourceRequisitionListUid` | `string` | Yes | The unique identifier for the requisition list from which items will be copied. | | `destinationRequisitionListUid` | `string` | Yes | The unique identifier for the requisition list to which items will be copied. | | `requisitionListItemUids` | `string[]` | Yes | An array of requisition list item UIDs to copy. These are the unique identifiers for specific items within the source list. | ### Events Does not emit any drop-in events. ### Returns Returns an object with the following structure, or `null` if the operation fails: ```ts interface CopyItemsResult { destinationList: RequisitionList | null; } ``` - `destinationList`: The updated destination requisition list after items have been copied, or `null` if not available. ## getCompanyUsers Returns a list of active company users for the current company. Used to populate the recipient picker in the share-by-email flow. ```ts const getCompanyUsers = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns an array of `CompanyUser` objects. ```ts interface CompanyUser { id: string; firstname: string; lastname: string; email: string; } ``` ## getSharedRequisitionList Retrieves a shared requisition list by its share token. The caller must be authenticated and belong to the same company as the sender. Returns the sender's display name alongside the read-only requisition list data. ```ts const getSharedRequisitionList = async ( token: string, currentPage?: number, pageSize?: number, enrichConfigurableProducts?: (items: Item[]) => Promise ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `token` | `string` | Yes | The share token from the URL (e.g. from `?requisition_id=`). | | `currentPage` | `number` | No | The page number for pagination (1-indexed). Defaults to page 1 if not specified. | | `pageSize` | `number` | No | The number of items to return per page. | | `enrichConfigurableProducts` | `(items: Item[]) => Promise` | No | Optional function to enrich configurable product items with variant data before returning. | ### Events Does not emit any drop-in events. ### Returns Returns `SharedRequisitionListResult | null`. Returns `null` if the token is valid but the requisition list cannot be resolved. ```ts interface SharedRequisitionListResult { senderName: string; requisitionList: RequisitionList; } ``` ## importSharedRequisitionList Imports a shared requisition list into the current customer's account using a share token. After a successful import, the recipient has full edit rights on the list. ```ts const importSharedRequisitionList = async ( token: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `token` | `string` | Yes | The share token extracted from the URL (e.g. from `?requisition_id=`). | ### Events Does not emit any drop-in events. ### Returns Returns `ImportSharedRequisitionListResult`. ```ts interface ImportSharedRequisitionListResult { requisitionList: RequisitionList | null; userErrors: ImportSharedRequisitionListUserError[]; } interface ImportSharedRequisitionListUserError { message: string; code: string; } ``` ## shareRequisitionListByEmail Shares a requisition list with one or more company users by email. An email notification is sent to each recipient with a link to import the list. ```ts const shareRequisitionListByEmail = async ( requisitionListUid: string, customerUids: string[] ): Promise | null> ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `requisitionListUid` | `string` | Yes | The unique identifier of the requisition list to share. | | `customerUids` | `string[]` | Yes | An array of company user IDs to share the list with. Obtain these from [`getCompanyUsers`](#getcompanyusers). | ### Events Does not emit any drop-in events. ### Returns Returns `null` on success, or an array of error objects if the share failed. ```ts interface ShareRequisitionListByEmailError { message: string; code: string; } ``` ## shareRequisitionListByToken Generates a shareable token for a requisition list. The token is used to build a share URL that recipients can open to import the list into their own account. ```ts const shareRequisitionListByToken = async ( requisitionListUid: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `requisitionListUid` | `string` | Yes | The unique identifier of the requisition list to generate a share token for. | ### Events Does not emit any drop-in events. ### Returns Returns `ShareRequisitionListByTokenResult`. ```ts interface ShareRequisitionListByTokenResult { token: string | null; errorMessage: string | null; } ``` ## Data Models The following data models are used by functions in this drop-in. ### RequisitionList The `RequisitionList` object is returned by the following functions: [`addProductsToRequisitionList`](#addproductstorequisitionlist), [`createRequisitionList`](#createrequisitionlist), [`deleteRequisitionList`](#deleterequisitionlist), [`deleteRequisitionListItems`](#deleterequisitionlistitems), [`getRequisitionList`](#getrequisitionlist), [`getRequisitionLists`](#getrequisitionlists), [`updateRequisitionList`](#updaterequisitionlist), [`updateRequisitionListItems`](#updaterequisitionlistitems), [`moveItemsBetweenRequisitionLists`](#moveitemsbetweenrequisitionlists), [`copyItemsBetweenRequisitionLists`](#copyitemsbetweenrequisitionlists). ```ts interface RequisitionList { uid: string; name: string; description: string; updated_at: string; items_count: number; items: Item[]; page_info?: PageInfo; } ``` {/* This documentation is auto-generated from the drop-in source repository: REPO_URL */} --- # Requisition List overview The Requisition List drop-in lets B2B customers manage requisition lists on Adobe Commerce storefronts. It supports multiple lists per account. Company users can add products to a list from product detail pages and product list pages. ## Supported Commerce features The following table provides an overview of the Adobe Commerce features that the Requisition List drop-in supports: | Feature | Status | | ------- | ------ | | Create and manage requisition lists | Supported | | Multiple requisition lists per account | Supported | | Add products from product pages | Supported | | Add products from list pages | Supported | | Requisition list item management | Supported | | Update item quantities | Supported | | Delete items and lists | Supported | | Add list items to cart | Supported | | Move items between lists | Supported | | Copy items between lists | Supported | | Batch item operations | Supported | | Requisition list grid view | Supported | | Customer authentication required | Supported | | GraphQL API integration | Supported | | Share requisition list via email | Supported | | Share requisition list via link and import | Supported | > **Quick Order and lists** Quick Order in the storefront adds lines to the cart. There is no out-of-the-box API or UI flow to bulk-add SKUs from Quick Order straight into an existing requisition or favorites list. If you need that path, plan a custom integration rather than assuming the Quick Order surface updates requisition lists. --- # Requisition List initialization The **Requisition List initializer** configures the drop-in for managing saved product lists and recurring orders. Use initialization to customize how requisition list data is displayed and enable internationalization for multi-language B2B storefronts. Version: 1.5.0 ## Configuration options The following table describes the configuration options available for the **Requisition List** initializer: | Parameter | Type | Req? | Description | |---|---|---|---| | `langDefinitions` | [`LangDefinitions`](#langdefinitions) | No | Language definitions for internationalization (i18n). Override dictionary keys for localization or branding. | ## Default configuration The initializer runs with these defaults when no configuration is provided: ```javascript title="scripts/initializers/requisition-list.js" // All configuration options are optional await initializers.mountImmediately(initialize, { langDefinitions: {}, // Uses built-in English strings models: {}, // Uses default data models }); ``` ## Language definitions Override dictionary keys for localization or branding. The `langDefinitions` object maps locale keys to custom strings that override default text for the drop-in. ```javascript title="scripts/initializers/requisition-list.js" const customStrings = { 'AddToCart': 'Add to Bag', 'Checkout': 'Complete Purchase', 'Price': 'Cost', }; const langDefinitions = { default: customStrings, }; await initializers.mountImmediately(initialize, { langDefinitions }); ``` > For complete dictionary customization including all available keys and multi-language support, see the [Requisition List Dictionary](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/dictionary/) page. ## Customizing data models Extend or transform data models by providing custom transformer functions. Use the `models` option to add custom fields or modify existing data structures returned from the backend. ### Available models The following models can be customized through the `models` configuration option: | Model | Description | |---|---| | [`RequisitionListModel`](#requisitionlistmodel) | Transforms requisition list data from `GraphQL` including list details, items, quantities, and metadata. Use this to add custom fields or modify existing requisition list data structures. | | [`RequisitionListItemModel`](#requisitionlistitemmodel) | Transforms requisition list item data including product details, quantities, and custom options. Use this to add custom fields or modify item data structures. | The following example shows how to customize the `RequisitionListModel` model for the **Requisition List** drop-in: ```javascript title="scripts/initializers/requisition-list.js" const models = { RequisitionListModel: { transformer: (data) => ({ // Add formatted last updated date lastUpdatedDisplay: data?.updated_at ? new Date(data.updated_at).toLocaleDateString() : null, // Add total items summary itemsSummary: `${data?.items_count || 0} items`, // Add list description preview (first 50 chars) descriptionPreview: data?.description ? data.description.substring(0, 50) + '...' : null, }), }, }; await initializers.mountImmediately(initialize, { models }); ``` ## Configuration types The following TypeScript definitions show the structure of each configuration object: ### langDefinitions Maps locale identifiers to dictionaries of key-value pairs. The `default` locale is used as the fallback when no specific locale matches. Each dictionary key corresponds to a text string used in the drop-in UI. ```typescript langDefinitions?: { [locale: string]: { [key: string]: string; }; }; ``` ## Model definitions The following TypeScript definitions show the structure of each customizable model: ### RequisitionListModel ```typescript export interface RequisitionList { uid: string; name: string; description: string; updated_at: string; items_count: number; items: Item[]; page_info?: PageInfo; } export interface PageInfo { page_size: number; current_page: number; total_pages: number; } ``` ### RequisitionListItemModel ```typescript export interface Item { uid: string; sku: string; product: Product; quantity: number; customizable_options?: { uid: string; is_required: boolean; label: string; sort_order: number; type: string; values: { uid: string; label: string; price: { type: string; units: string; value: number }; value: string; }[]; }[]; bundle_options?: { uid: string; label: string; type: string; values: { uid: string; label: string; original_price: { value: number; currency: string }; priceV2: { value: number; currency: string }; quantity: number; }[]; }[]; configurable_options?: { option_uid: string; option_label: string; value_uid: string; value_label: string; }[]; links?: { uid: string; price?: number; sample_url?: string; sort_order?: number; title?: string; }[]; samples?: { url?: string; sort_order?: number; title?: string; }[]; gift_card_options?: { amount?: { value?: number; currency?: string; }; custom_giftcard_amount?: { value?: number; currency?: string; }; message?: string; recipient_email?: string; recipient_name?: string; sender_name?: string; sender_email?: string; }; } export interface Product { sku: string; parent_sku: string; name: string; shortDescription: string; metaDescription: string; metaKeyword: string; metaTitle: string; description: string; addToCartAllowed: boolean; url: string; urlKey: string; externalId: string; images: { url: string; label: string; roles: string[]; }[]; } ``` --- # Requisition List Quick Start Get started with the Requisition List drop-in to enable reusable product lists for repeat B2B ordering. Version: 1.4.0 ## Quick example The Requisition List drop-in is included in the https://github.com/hlxsites/aem-boilerplate-commerce. This example shows the basic pattern: ```js // 1. Import initializer (handles all setup) // 2. Import the container you need // 3. Import the provider // 4. Render in your block export default async function decorate(block) { await provider.render(RequisitionListForm, { // Configuration options - see Containers page })(block); } ``` **New to drop-ins?** See the [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/) guide for complete step-by-step instructions. ## Quick reference **Import paths:** - Initializer: `import '../../scripts/initializers/requisition-list.js'` - Containers: `import ContainerName from '@dropins/storefront-requisition-list/containers/ContainerName.js'` - Provider: `import { render } from '@dropins/storefront-requisition-list/render.js'` **Package:** `@dropins/storefront-requisition-list` **Version:** 1.2.0 (verify compatibility with your Commerce instance) **Example container:** `RequisitionListForm` ## Learn more - [Containers](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/containers/) - Available UI components and configuration options - [Initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/initialization/) - Customize initializer settings and data models - [Functions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/functions/) - Control drop-in behavior programmatically - [Events](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/events/) - Listen to and respond to drop-in state changes - [Slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/slots/) - Extend containers with custom content --- # Requisition List Slots The Requisition List drop-in exposes slots for customizing specific UI sections. Use slots to replace or extend container components. For default properties available to all slots, see [Extending drop-in components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/extending/). Version: 1.4.0 | Container | Slots | |-----------|-------| | [`RequisitionListGrid`](#requisitionlistgrid-slots) | `Header` | ## RequisitionListGrid slots The slots for the `RequisitionListGrid` container allow you to customize its appearance and behavior. ```typescript interface RequisitionListGridProps { slots?: { Header?: SlotProps; }; } ``` ### Header slot The Header slot allows you to customize the header section of the `RequisitionListGrid` container. #### Example ```js await provider.render(RequisitionListGrid, { slots: { Header: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Header'; ctx.appendChild(element); } } })(block); ``` --- # Requisition List styles Customize the Requisition List drop-in using CSS classes and design tokens. This page covers the Requisition List-specific container classes and customization examples. For comprehensive information about design tokens, responsive breakpoints, and styling best practices, see [Styling Drop-In Components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/). Version: 1.4.0 ## Customization example Add this to the CSS file of the specific https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/ where you're using the Requisition List drop-in. For a complete list of available design tokens (colors, spacing, typography, and more), see the [Design tokens reference](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/#design-tokens-reference). ```css title="styles/styles.css" del={2-2} ins={3-3} .requisition-list-view__batch-actions { --batch-actions-background: #f0f4f8; --batch-actions-background: var(--color-brand-800); } ``` ## Container classes The Requisition List drop-in uses BEM-style class naming. Use the browser DevTools to inspect elements and find specific class names. ```css /* BatchActions */ .requisition-list-view__batch-actions {} .requisition-list-view__batch-actions-buttons {} .requisition-list-view__batch-actions-count-badge {} .requisition-list-view__batch-actions-delete-icon {} .requisition-list-view__batch-actions-left {} .requisition-list-view__batch-actions-select-label {} .requisition-list-view__batch-actions-select-toggle {} .requisition-list-view__batch-actions-select-toggle--active {} .requisition-list-view__bulk-actions {} /* EmptyList */ .empty-list {} /* NotFound */ .not-found {} /* PageSizePicker */ .page-size-picker {} .page-size-picker__label {} .page-size-picker__select {} /* PaginationItemsCounter */ .pagination-items-counter {} /* ProductListTable */ .requisition-list-view-product-list-table-container {} .requisition-list-view-product-list-table-container__submit-container {} .requisition-list-view-product-list-table__checkbox {} .requisition-list-view-product-list-table__discount-container {} .requisition-list-view-product-list-table__index-container {} .requisition-list-view-product-list-table__item-container {} .requisition-list-view-product-list-table__item-details {} .requisition-list-view-product-list-table__low-stock {} .requisition-list-view-product-list-table__out-of-stock {} .requisition-list-view-product-list-table__product-configurable-name {} .requisition-list-view-product-list-table__product-name {} .requisition-list-view-product-list-table__quantity {} .requisition-list-view-product-list-table__sku {} .requisition-list-view-product-list-table__thumbnail {} /* RequisitionListActions */ .requisition-list-actions {} .requisition-list-actions--selectable {} .requisition-list-actions__title {} /* RequisitionListForm */ .requisition-list-form {} .requisition-list-form__actions {} .requisition-list-form__form {} .requisition-list-form__notification {} .requisition-list-form__title {} .requisition-list-form_progress-spinner {} /* RequisitionListGridWrapper */ .dropin-button--tertiary {} .requisition-list-empty-list {} .requisition-list-grid-wrapper__actions {} .requisition-list-grid-wrapper__add-new {} .requisition-list-grid-wrapper__content {} .requisition-list-grid-wrapper__list-header {} .requisition-list-grid-wrapper__name__description {} .requisition-list-grid-wrapper__name__title {} .requisition-list-grid-wrapper__pagination {} .requisition-list-grid-wrapper__pagination-picker {} .requisition-list__alert-wrapper {} /* RequisitionListHeader */ .requisition-list-header {} .requisition-list-header__action-link {} .requisition-list-header__action-link--disabled {} .requisition-list-header__actions {} .requisition-list-header__back {} .requisition-list-header__back-arrow {} .requisition-list-header__back-link {} .requisition-list-header__description {} .requisition-list-header__main {} .requisition-list-header__title {} .requisition-list-header__title-section {} /* RequisitionListModal */ .dropin-modal {} .dropin-modal__body--full {} .dropin-modal__body--medium {} .dropin-modal__content {} .dropin-modal__header-title {} .dropin-modal__header-title-content {} .requisition-list-modal {} .requisition-list-modal--overlay {} .requisition-list-modal__buttons {} .requisition-list-modal__spinner {} /* RequisitionListPicker */ .dropin-card--secondary {} .dropin-card__content {} .requisition-list-picker__form {} .requisition-list-picker__actions {} .requisition-list-picker__available-lists {} /* RequisitionListSelector */ .requisition-list-actions {} .requisition-list-modal {} /* RequisitionListView */ .requisition-list-view__container {} .requisition-list-view__loading {} .requisition-list-view__pagination {} .requisition-list-view__pagination-picker {} /* ShareRequisitionListContent */ .share-requisition-list-content {} .share-requisition-list-content__actions {} .share-requisition-list-content__divider-secondary {} .share-requisition-list-content__field {} .share-requisition-list-content__instruction {} .share-requisition-list-content__link-row {} .share-requisition-list-content__loading {} .share-requisition-list-content__multi-select {} .share-requisition-list-content__recipient {} .share-requisition-list-content__recipient-list {} .share-requisition-list-content__success {} /* SharedRequisitionList */ .shared-requisition-list__alert-wrapper {} .shared-requisition-list__container {} .shared-requisition-list__loading {} .shared-requisition-list__preview {} .shared-requisition-list__preview-details {} .shared-requisition-list__preview-row {} .shared-requisition-list__preview-label {} .shared-requisition-list__preview-value {} .shared-requisition-list__actions {} .shared-requisition-list__table-wrapper {} ``` For the source CSS files, see the https://github.com/adobe-commerce/storefront-requisition-list/tree/main/src. --- # Event Bus --- # Analytics events reference When you substitute a drop-in with a custom block, that block no longer automatically emits the [Adobe Client Data Layer (ACDL)](https://experienceleague.adobe.com/developer/commerce/storefront/setup/analytics/instrumentation/#adobe-client-data-layer-acdl) events the drop-in would have sent. This reference describes how the analytics event lifecycle works and lists the events each drop-in emits, so your custom implementation can reproduce them. > This page covers ACDL analytics events, a different system from the event bus used for drop-in-to-drop-in communication. For that system, see [Events](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/events/) and [Common events reference](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/common-events/). For ACDL configuration and AEP forwarding, see [Analytics instrumentation](https://experienceleague.adobe.com/developer/commerce/storefront/setup/analytics/instrumentation/) and [Adobe Experience Platform](https://experienceleague.adobe.com/developer/commerce/storefront/setup/analytics/adobe-experience-platform/). ## How analytics events work Analytics events follow the same lifecycle regardless of which drop-in emits them: 1. **Initial context** - On page load, the storefront's core scripts set store- and page-level context (`pageContext`, `storefrontInstanceContext`, `shopperContext`, and so on). This context is required by every event that follows. See [Scripts](#scripts). 2. **Event-specific context** - Immediately before an event fires, the emitting drop-in sets the context specific to that event - for example, `productContext` before `product-page-view`. 3. **Event emission** - The drop-in pushes the event to the ACDL. The event's `eventInfo` snapshot merges the current initial context with the event-specific context. If you substitute a drop-in, your custom implementation needs to set the same event-specific context and emit the same event at the same point in the user journey. You only need to reproduce the initial context from [Scripts](#scripts) if you're also bypassing the storefront's core scripts. > **channelContext** `channelContext` is only required when instrumenting events for AEP collection. If you're not forwarding events to AEP, you can omit it. ## Drop-ins Jump to the events for a specific drop-in: - [Scripts](#scripts) - initial context, required by every event below - [storefront-pdp](#storefront-pdp) - [storefront-cart](#storefront-cart) - [storefront-auth](#storefront-auth) - [storefront-order](#storefront-order) - [storefront-recommendations](#storefront-recommendations) - [storefront-product-discovery](#storefront-product-discovery) --- ## Scripts The storefront's core scripts set the initial store- and page-level context before any drop-in-specific instrumentation runs. This context is required by every event listed below, in addition to each event's own event-specific context. ### page-view Trigger: Page load, eager phase Fires as soon as the page loads, seeding the data layer with initial page and cart context. **Contexts set:** - `pageContext` - `shoppingCartContext` (seed) ### Delayed phase initialization Trigger: Page load, delayed phase (gated on analytics config + consent) No event is emitted at this step. Instead, the delayed phase sets the context required for AEP event forwarding. This only runs when analytics configuration and user consent allow it. **Contexts set:** - `storefrontInstanceContext` - `shopperContext` - `eventForwardingContext` - `aepContext` --- ## storefront-pdp ### product-page-view Trigger: Page load, after product data is fetched **Contexts set:** - `productContext` - `channelContext` --- ## storefront-cart ### add-to-cart Trigger: User adds a product to cart, after the mutation succeeds **Contexts set:** - `shoppingCartContext` - `productContext` - `changedProductsContext` - `channelContext` ### remove-from-cart Trigger: User removes or reduces the quantity of a cart item, after the mutation succeeds **Contexts set:** - `shoppingCartContext` - `productContext` - `changedProductsContext` - `channelContext` ### open-cart Trigger: User's first add-to-cart when the cart was previously empty **Contexts set:** - `shoppingCartContext` - `changedProductsContext` - `channelContext` ### shopping-cart-view Trigger: User opens the cart or mini-cart panel **Contexts set:** - `shoppingCartContext` - `channelContext` ### initiate-checkout Trigger: User clicks **Proceed to Checkout** **Contexts set:** - `shoppingCartContext` - `channelContext` --- ## storefront-auth ### sign-in Trigger: User submits the login form successfully **Contexts set:** - `accountContext` - `channelContext` ### sign-out Trigger: User clicks the logout button **Contexts set:** - `channelContext` ### create-account Trigger: User submits the account creation form successfully **Contexts set:** - `accountContext` - `channelContext` --- ## storefront-order ### place-order Trigger: User completes order placement successfully **Contexts set:** None. This event reads the existing ACDL state at emit time instead of requiring new context to be set. --- ## storefront-recommendations ### recs-api-request-sent Trigger: Page load, when the recommendations API request fires **Contexts set:** None ### recs-api-response-received Trigger: Page load, when the recommendations API response returns **Contexts set:** - `recommendationsContext` ### recs-unit-impression-render Trigger: Page load, when a recommendation unit mounts into the DOM **Contexts set:** - `recommendationsContext` ### recs-unit-view Trigger: When a recommendation unit scrolls into the viewport **Contexts set:** - `recommendationsContext` ### recs-item-click Trigger: User clicks a recommendation product **Contexts set:** - `recommendationsContext` ### recs-item-add-to-cart-click Trigger: User clicks Add to Cart on a recommendation product **Contexts set:** - `recommendationsContext` --- ## storefront-product-discovery ### search-request-sent Trigger: A search API call is made (on page load for PLP, or on user typing in search) **Contexts set:** - `searchInputContext` - `channelContext` ### search-response-received Trigger: The search API response returns **Contexts set:** - `searchResultsContext` - `channelContext` ### search-results-view Trigger: Page load, when search results finish rendering **Contexts set:** - `channelContext` ### search-product-click Trigger: User clicks a search result product **Contexts set:** - `channelContext` ### category-results-view Trigger: Page load, when category results finish rendering **Contexts set:** - `channelContext` --- ## Additional notes - The contexts listed for each event are set in addition to the initial context from [Scripts](#scripts), which is required by every event. - `channelContext` is only required when instrumenting events for AEP collection. - These events are pushed to the ACDL (`window.adobeDataLayer`), not the drop-in event bus (`@dropins/tools/event-bus.js`). See [Events](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/events/) for the distinction between the two systems. --- # Branding Drop-In Components Branding with design tokens (CSS custom properties that define reusable design values such as color, type scale, spacing, shape, and layout.) is the quickest way to customize your storefront. ## Big picture The following diagram shows a small branding change. When we override the default value of a single shape token, we override the default border-radius of the `Button` in the storefront's library components (Foundational UI pieces such as buttons and inputs that are composed into larger drop-in experiences.), which changes the look and feel of drop-in components that use it. ![Flowchart showing how CSS variables and design tokens from your project map into Button styling and other library components used by drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/images/brand/howtobrand.svg) *How to override the drop-in design tokens.* These token values come from the Adobe Commerce design system (The set of design tokens, base components, and conventions used to style Commerce storefront drop-ins.) and are picked up automatically by drop-in UI components. ## Examples This example shows six design tokens with new values for three color and three shape tokens from the boilerplate's `styles/styles.css` file. ## Step-by-step The following steps show how to override default token values to match your brand (Your storefront’s visual identity, including colors, typography, spacing, and shape choices.) colors, typography, spacing, shapes, and layouts (grids). :::tip **Tip:** Work on one category at a time. For example, start with **typography**, then move on to **spacing**, **shapes**, **grids**, and finally **colors** (because they are typically the hardest to map to design tokens). Using this process ensures each brand category is completed and reviewed before moving on to the next. ::: ### 1. Open the `styles/styles.css` file. From the root of your project, open the `styles/styles.css` file. - scripts/ - **styles/** _CSS files for drop-in component design tokens, fonts, deferred styles_ - fonts.css _-- Default font styles_ - lazy-styles.css _-- Global styles loaded after LCP_ - **styles.css** _-- Global design tokens and CSS classes for site_ - tools/ ### 2. Override typography tokens. We suggest starting with typography overrides. Mapping a brand's typography to the available design tokens is typically straightforward. For example, https://www.nasa.gov/nasa-brand-center/brand-guidelines/#Typography specifies three font families: - **Inter** for large display and heading text - **Public Sans** for interfaces and body text - **DM Mono** for numbers and small labels :::tip **Tip:** Download the fonts. For better performance, we recommend downloading your brand fonts and adding them to the `fonts/` directory. Then, update the `styles/fonts.css` file to import them for use in the design tokens. Use the default Roboto font as an example for adding your brand's fonts. ::: After installing the fonts, you can map them to the storefront design tokens. The following example shows how you might override the default typography design tokens to match NASA's brand guidelines. ```css :root, .dropin-design { --type-body-font-family: 'Public Sans', sans-serif; --type-display-font-family: 'Inter', sans-serif; --type-details-font-family: 'DM Mono', monospace; --type-display-1-font: normal normal 300 60px/72px var(--type-display-font-family); /* Hero title */ --type-display-1-letter-spacing: 0.04em; --type-display-2-font: normal normal 300 48px/56px var(--type-display-font-family); /* Banner title */ --type-display-2-letter-spacing: 0.04em; --type-display-3-font: normal normal 300 34px/40px var(--type-display-font-family); /* Desktop & tablet section title */ --type-display-3-letter-spacing: 0.04em; --type-headline-1-font: normal normal 400 24px/32px var(--type-display-font-family); /* Desktop & tablet page title */ --type-headline-1-letter-spacing: 0.04em; --type-headline-2-default-font: normal normal 300 20px/24px var(--type-display-font-family); /* Rail title */ --type-headline-2-default-letter-spacing: 0.04em; --type-headline-2-strong-font: normal normal 400 20px/24px var(--type-display-font-family); /* Mobile page and section title */ --type-headline-2-strong-letter-spacing: 0.04em; --type-body-1-default-font: normal normal 300 16px/24px var(--type-body-font-family); /* Normal text paragraph */ --type-body-1-default-letter-spacing: 0.04em; --type-body-1-strong-font: normal normal 400 16px/24px var(--type-body-font-family); --type-body-1-strong-letter-spacing: 0.04em; --type-body-1-emphasized-font: normal normal 700 16px/24px var(--type-body-font-family); --type-body-1-emphasized-letter-spacing: 0.04em; --type-body-2-default-font: normal normal 300 14px/20px var(--type-body-font-family); --type-body-2-default-letter-spacing: 0.04em; --type-body-2-strong-font: normal normal 400 14px/20px var(--type-body-font-family); --type-body-2-strong-letter-spacing: 0.04em; --type-body-2-emphasized-font: normal normal 700 14px/20px var(--type-body-font-family); --type-body-2-emphasized-letter-spacing: 0.04em; --type-button-1-font: normal normal 400 20px/26px var(--type-body-font-family); /* Primary button text */ --type-button-1-letter-spacing: 0.08em; --type-button-2-font: normal normal 400 16px/24px var(--type-body-font-family); /* Small buttons */ --type-button-2-letter-spacing: 0.08em; --type-details-caption-1-font: normal normal 400 12px/16px var(--type-details-font-family); --type-details-caption-1-letter-spacing: 0.08em; --type-details-caption-2-font: normal normal 300 12px/16px var(--type-details-font-family); --type-details-caption-2-letter-spacing: 0.08em; --type-details-overline-font: normal normal 700 12px/20px var(--type-details-font-family); --type-details-overline-letter-spacing: 0.16em; } ``` ### 3. Continue with spacing, shapes, layouts, and colors. Use the same process for overriding the spacing, shapes, grids, and color token values. Apply deeper styling (Visual customization of drop-ins through CSS overrides, token changes, and layout adjustments.) changes only after you have mapped the core tokens. With a company's brand guidelines, you can start discovering how to map brand categories to the design-token values you need to override. But it's not always straightforward. Mapping brand colors to the color token options can be challenging. This is when you will need to work closely with the design team to make decisions about which design tokens to override and how to map your brand colors to the available options. ## Summary The process of branding drop-in components is typically fast and easy. Focus on one brand category at a time and work with your designers to solve the less obvious brand-to-token overrides. --- # Commerce blocks and drop-ins ## Related documentation - [Commerce Blocks Configuration](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/blocks/) - Learn how to configure Commerce blocks using Document Authoring - [Drop-in components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/introduction/) - Overview of all available drop-in components The Adobe Commerce boilerplate includes 30 Commerce blocks that wrap drop-in components to provide ready-to-use e-commerce functionality. These blocks integrate drop-ins with Edge Delivery Services, making it easy to add commerce features to your storefront without writing custom code. ## Drop-ins used in Commerce blocks The following table shows which drop-in components are used by each Commerce block: | Drop-in | Commerce blocks | |---------|---------------------------| | **storefront-account** | Account Sidebar, Addresses, Customer Information, Orders List | | **storefront-auth** | Confirm Account, Create Account, Create Password, Forgot Password, Login, Search Order, Wishlist | | **storefront-cart** | Cart, Gift Options, Mini Cart, Order Product List | | **storefront-checkout** | Checkout | | **storefront-order** | Create Return, Customer Details, Order Comments, Order Cost Summary, Order Product List, Order Returns, Order Status, Returns List, Search Order, Shipping Status | | **storefront-payment-services** | Checkout | | **storefront-pdp** | Product Details | | **storefront-product-discovery** | Product List Page | | **storefront-recommendations** | Product Recommendations | | **storefront-wishlist** | Cart, Wishlist, Product Details, Product List Page, Product Recommendations | > The `@dropins/tools` package is a utility library required by all drop-in components, providing shared functionality like `fetch-graphql`, `event-bus`, and `initializer` utilities. It is not a drop-in component itself, but rather a dependency used by Commerce blocks that integrate drop-ins. --- # Common events reference Drop-ins use common events for cross-component communication, authentication management, localization, and error handling. These events provide a standard way for your storefront to communicate with drop-ins and coordinate behavior across the application. > For conceptual information about the event system, see the [Events guide](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/events/). For drop-in-specific events, refer to each drop-in's individual Events page. ## Events overview | Event | Category | Used By | Description | |-------|----------|---------|-------------| | [authenticated](#authenticated) | Authentication | Most B2C & B2B drop-ins | Authentication state changes | | [error](#error) | Error Handling | Most drop-ins | Error notifications | | [locale](#locale) | Localization | All drop-ins | Language/locale changes | --- ## authenticated Category: Authentication Direction: Emitted by external source, Listened to by drop-ins Used By: Cart, Checkout, Order, User Account, User Auth, Wishlist, and most B2B drop-ins Fires when the user's authentication state changes (login, logout, token refresh, session expiration). Drop-ins listen to this event to update their internal state and UI based on the current authentication status. ### When to emit Emit this event from your storefront when: - An authentication token is refreshed - The authentication state is restored (e.g., page refresh with active session) - A session expires - A user logs out - A user successfully logs in ### Data payload ```typescript boolean ``` The payload is a simple boolean value: - `true` = User is authenticated - `false` = User is not authenticated or has logged out ### Usage examples **Emit when authentication changes:** ```javascript // User logged in events.emit('authenticated', true); // User logged out events.emit('authenticated', false); ``` **Listen for authentication changes:** ```javascript const authListener = events.on('authenticated', (isAuthenticated) => { if (isAuthenticated) { console.log('User authenticated'); // Update UI, load user-specific data, etc. } else { console.log('User logged out'); // Clear user data, redirect to login, etc. } }); // Later, when you want to stop listening authListener.off(); ``` --- ## error Category: Error Handling Direction: Emitted by drop-ins, Listened to by external code Used By: Most drop-ins for error reporting Fires when a drop-in encounters an error (API failure, validation error, network timeout). Your storefront should listen to this event to display error messages, log errors, or trigger error recovery logic. ### When emitted Drop-ins emit this event when: - API requests fail - Critical operations fail - Network errors occur - Unexpected errors occur - Validation fails ### Data payload ```typescript { message: string; code?: string; details?: any; source?: string; } ``` ### Usage examples **Listen for errors from drop-ins:** ```javascript const errorListener = events.on('error', (error) => { console.error('Drop-in error:', error.message); // Display error to user showErrorNotification(error.message); // Log to error tracking service if (window.Sentry) { Sentry.captureException(error); } // Handle specific error codes if (error.code === 'AUTH_EXPIRED') { redirectToLogin(); } }); // Later, when you want to stop listening errorListener.off(); ``` **Emit errors from custom code:** ```javascript try { // Your custom logic await customOperation(); } catch (err) { events.emit('error', { message: 'Custom operation failed', code: 'CUSTOM_ERROR', details: err, source: 'MyCustomComponent' }); } ``` --- ## locale Category: Localization Direction: Emitted by external source, Listened to by drop-ins Used By: All drop-ins with internationalization support Fires when the application's language or locale changes. Drop-ins listen to this event to update their text content, date formatting, currency display, and other locale-specific elements. ### When to emit Emit this event from your storefront when: - A user selects a different language - The application detects and applies a locale based on user preferences - The locale is programmatically changed ### Data payload ```typescript string ``` The locale string should follow standard locale format (e.g., `en-US`, `fr-FR`, `de-DE`). ### Usage examples **Emit when locale changes:** ```javascript // User selects a new language events.emit('locale', 'fr-FR'); // Or based on browser detection const userLocale = navigator.language || 'en-US'; events.emit('locale', userLocale); ``` **Listen for locale changes:** ```javascript const localeListener = events.on('locale', (newLocale) => { console.log('Locale changed to:', newLocale); // Update UI text, reload translations, etc. updateTranslations(newLocale); }); // Later, when you want to stop listening localeListener.off(); ``` --- # Creating Drop-In Components This topic describes how to use the `drop-template` repository to create drop-in components for Adobe Commerce Storefronts. ## What are drop-in component templates? Drop-in templates are GitHub Templates that allow you to quickly create drop-in components with the same structure, branches, files, and best practices built in. The `dropin-template` repository provides the starting point for creating new drop-ins quickly and consistently. For more information on GitHub Templates, you can refer to the following resource: https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template. ## How to use the Adobe Commerce drop-in template :::note Supported Node versions are: Maintenance (v20) and Active (v22). ::: To create a new drop-in component using the Adobe Commerce drop-in template, follow these steps: 1. **Navigate to the Template Repository**: Go to https://github.com/adobe-commerce/dropin-template. 1. **Create a New Repository**: Click on the **Use this template** button to create a new repository based on the template. This will generate a new repository with the same directory structure and files as the template. 1. **Clone Your New Repository**: You can now clone the newly created repository to your local machine using `git clone`. 1. **Getting Started**: Follow the instructions below to install the dependencies, generate a configuration file, update your Mesh endpoint, generate your source files, and launch your development environment. **Troubleshooting:** - If you don't see the **Use this template** button, make sure you are logged into GitHub. - If you get a "Permission denied" error, check your SSH keys or use HTTPS. ## Getting started ### 1. Install dependencies Before you begin, make sure you have all the necessary dependencies installed. Run the following command to install all required packages: ```bash npm install ``` **Troubleshooting:** If you see errors about missing Node.js, install it from [nodejs.org](https://nodejs.org/). ### 2. Generate new config Before you can start developing, you need to generate the `.elsie.js` config file. The Elsie CLI uses this file to generate new components, containers, and API functions in specified directories within your project. To create a new configuration file, run the following command. Replace `` with the name of your new drop-in. ```bash npx elsie generate config --name ``` After generating the `.elsie.js` config, open it and take a look. Below is an annotated version describing the main properties: ```javascript module.exports = { name: 'Login', // The name of your frontend. This name can be changed at any time. api: { root: './src/api', // Directory where the CLI will add all your generated API functions. importAliasRoot: '@/login/api', }, components: [ { id: 'Components', root: './src/components', // Directory where the CLI will add all your generated components. importAliasRoot: '@/login/components', cssPrefix: 'elsie', default: true, }, ], containers: { root: './src/containers', // Directory where the CLI will add all your generated containers. importAliasRoot: '@/login/containers', }, }; ``` **Troubleshooting:** If `npx` is not found, ensure Node.js and npm are installed. :::tip[More Info] For more details on _Elsie CLI_ commands and their usage, visit this documentation page: https://experienceleague.adobe.com/developer/commerce/storefront/sdk/get-started/cli/. ::: ### 3. Explore the project structure Understand where to find and place your code. - .storybook/ *-- Best-practice Storybook configurations right out of the box* - examples/ - html-host/ *-- Preconfigured HTML UI for testing your drop-in components* - example.css - favicon.ico - index.html - styles.css - src/ - api/ *-- By default, the Elsie CLI adds your API functions here* - data/ *-- Contains data models and type definitions* - docs/ *-- Provides an MDX template to document your frontend* - i18n/ *-- Internationalization setup with starter en_US.json file* - render/ *-- Contains rendering utilities and provider functions* - types/ *-- TypeScript type definitions and interfaces* - tests/ *-- Unit tests and testing utilities* - elsie.js *-- Configuration file for creating components, containers and functions* - .env.sample *-- Preconfigured settings for a development-only mesh endpoint* - .eslintrc.js *-- Preconfigured linting* - .gitignore - .jest.config.js *-- Preconfigured unit testing* - LICENSE *-- Adobe Drop-in Template License* - package.json *-- Preconfigured dependencies* - prettier.config.js *-- Preconfigured formatting* - README.md *-- Quick instructional overview of frontend development tasks* - storybook-stories.js *-- Additional storybook settings* - tsconfig.js *-- Preconfigured for TypeScript* ### 4. Update mesh/backend endpoint (for development only) For development purposes, you will need to rename your `.env.sample` file to `.env` and update the new `.env` file with the correct mesh/backend endpoint. This file is used to store environment-specific configurations. ```sh ENDPOINT="your-endpoint" ``` **Troubleshooting:** If you see network errors when running the dev server, check your endpoint URL. ### 5. Start the development server ```bash npm run dev ``` Congratulations! You just launched your frontend development environment. It's a preconfigured HTML page (`examples > html-host > index.html`) that loads your frontend components for testing during development: ![Frontend Development Environment](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/src/content/docs/sdk/images/frontend.png) *Frontend development environment* Now you're ready to start building a composable frontend. Stop the server with `Ctrl + C` and let's get started. ### 6. Generate a new UI component UI components in this codebase are primarily responsible for rendering the UI, handling presentation, and managing styling. To generate a new UI component, use the following command. Replace `` with the name of your component. ```bash npx elsie generate component --pathname ``` **Make sure to use Pascal casing for the component name.** For a login form, you might choose: ```bash npx elsie generate component --pathname LoginForm ``` Let's take a quick look at the files that are generated for you: ```console ~/composable-login [main] » npx elsie generate component --pathname LoginForm 🆕 src/components/LoginForm/LoginForm.css created 🆕 src/components/LoginForm/LoginForm.stories.tsx created 🆕 src/components/LoginForm/LoginForm.test.tsx created 🆕 src/components/LoginForm/LoginForm.tsx created 🆕 src/components/LoginForm/index.ts created 🆕 src/components/index.ts created ~/composable-login [main] » ``` These files were not only generated with the appropriate names, but they are completely preconfigured to work together as a unit. For example, the `LoginForm` component was automatically imported into `src/components/index.ts` to let you start referencing the component throughout your project. And if you run `npm run dev` again, you'll see your new component in the Storybook UI, configured with an example and best practices to help you get started with Storybook. ### 7. Generate a new frontend container Containers handle business logic, state management, API calls, and data fetching using the components. They do not contain CSS or styling logic. To create a new frontend container, use this command. Replace `` with the desired name of your frontend container. **Make sure to use Pascal casing for the container name.** ```bash npx elsie generate container --pathname ``` For a login form, you might choose: ```bash npx elsie generate container --pathname LoginContainer ``` ### 8. Generate a new API function The API layer provides core functionalities like fetching, handling events, and GraphQL operations. This API is primarily consumed by a container. If you need to add a new API function, run the following command. Replace `` with the desired name for your API function. **Make sure to use camel casing for the API name.** ```bash npx elsie generate api --pathname ``` For a login form, you might want to add `login` and `logout` functions as follows: ```bash npx elsie generate api --pathname login ``` ```bash npx elsie generate api --pathname logout ``` **Location:** Generated files will be placed in `src/components/`, `src/containers/`, and `src/api/` respectively ## Adding a shared component to your project After creating your drop-in component, let's add a shared component from the Storefront SDK. These components are designed to be reusable and customizable, making it easier to build consistent and high-quality user interfaces. Follow the steps below to add a shared component to your drop-in component project. ### 1. Install the `@adobe-commerce/elsie` package Run the following command to install the Storefront SDK package: ```bash npm install @adobe-commerce/elsie ``` ### 2. Use a shared component from the SDK In your generated UI component, import a shared component from the Storefront SDK package and render it. For example, you can add the `Button` component as follows: ```javascript import { Button } from '@adobe-commerce/elsie'; function MyUiComponent() { return ( ``` Example usage: ```javascript const $action_1 = document.getElementById('action-1'); $action_1.addEventListener('click', () => { console.log("action-1 has been clicked"); myFunction(); // or pkg.myFunction(); }); ``` #### 2. Data/debug display (Middle) Real-time data and response visualization: ```html
⏳ Loading...
``` Example usage: ```javascript // Display event data const $data = document.getElementById('data'); events.on('', (data) => { $data.innerText = JSON.stringify(data, null, 2); }); // Update loading state $data.innerText = '⏳ Loading...'; ``` #### 3. Container display (Bottom) Where your drop-in components are rendered: ```html

Frontend Containers

``` Example usage: ```javascript const $my_container = document.getElementById('my-container'); provider.render(Container, { // Your container props })($my_container); ``` :::tip[More Info] For more details on the usage of _event bus_, _initializers_, and _render_, visit this documentation page: https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/. ::: ### Styling the sandbox The Sandbox environment is styled using two stylesheets: - `style.css` which is the base styling file that handles root-level styles and variables as well as global element styles. - `example.css` which is specifically for styling sandbox UI components. ## Best practices and accessibility - Use meaningful names for components and API functions. - Write tests for every component and function. - Keep components small and focused. - Document your code and update the MDX docs in `src/docs/`. - Use Storybook to visually test components. - Commit early and often; use branches for new features. - Use clear, simple language in UI and documentation. - Ensure all components are keyboard accessible. - Add ARIA labels where appropriate. - Test with screen readers. **Common pitfalls:** - Forgetting to create and update `.env` with the correct endpoint. - Not running `npm install` after cloning. - Skipping tests before building for production. ## Summary and next steps You've learned how to: - Set up a drop-in component project - Generate and configure components, API functions, and containers - Run and test your frontend locally - Build for production **Next Steps:** - Explore advanced component patterns - Integrate with real backend APIs - Contribute to the [drop-in template repo](https://github.com/adobe-commerce/dropin-template) --- # Dictionary Customization Guide Every drop-in includes a **dictionary** with all user-facing text. Customize it to localize for different languages, match your brand voice, or override default text. The drop-in **deep-merges** your custom values with defaults—you only specify what you want to change. > **Which guide do I need?** - **Using the boilerplate?** → See [Labels](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/labeling/) for the placeholder system - **Want conceptual understanding?** → You're in the right place (deep-merge behavior, multi-language patterns, advanced use cases) - **Need specific drop-in keys?** → See individual dictionary pages: [Cart](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/dictionary/), [Checkout](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/dictionary/), [Product Details](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/dictionary/), etc. ## Quick start 1. **Find the dictionary keys:** Check your drop-in's dictionary page ([Cart](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/dictionary/), [Checkout](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/dictionary/), and so on). 2. **Create your overrides:** ```javascript title="src/config/custom-dictionary.js" export const customDictionary = { Cart: { MiniCart: { heading: "Shopping Basket ({count})", // Only override what you want cartLink: "View Basket" } } }; ``` 3. **Pass it to `initialize()`:** ```javascript title="scripts/initializers/cart.js" import { initializers } from '@dropins/tools/initializer.js'; import { initialize } from '@dropins/storefront-cart/api.js'; import { customDictionary } from '../config/custom-dictionary.js'; const langDefinitions = { default: { ...customDictionary, }, }; await initializers.mountImmediately(initialize, { langDefinitions }); ``` > **Partial overrides only:** You don't need the entire dictionary. The drop-in deep-merges your values with the defaults, so specify only what you're changing. ## How deep merge works Understanding the merge behavior is critical: **✅ Override specific keys, keep all the defaults:** ```javascript // Your dictionary: { Cart: { MiniCart: { heading: "My Cart" } } } // Result (merged with defaults): { Cart: { MiniCart: { heading: "My Cart", // ← Your value cartLink: "View Cart", // ← Default kept checkoutLink: "Checkout" // ← Default kept } } } ``` **✅ Nested objects merge recursively:** ```javascript { Cart: { PriceSummary: { promoCode: { errors: { invalid: "That code didn't work" // Only this changes // All other errors stay default } } } } } ``` ## Multi-language support Create dictionaries for each locale and load them dynamically: ### Setup ```javascript title="scripts/config/dictionaries/cart-en.js" export const cartEN = { Cart: { MiniCart: { heading: "Cart ({count})" } } }; ``` ```javascript title="scripts/config/dictionaries/cart-fr.js" export const cartFR = { Cart: { MiniCart: { heading: "Panier ({count})" } } }; ``` ### Load by locale ```javascript title="scripts/initializers/cart.js" const userLocale = navigator.language.replace('-', '_'); const translations = { en_US: cartEN, fr_FR: cartFR }; const selectedLang = translations[userLocale] || cartEN; const langDefinitions = { default: { ...selectedLang, }, }; await initializers.mountImmediately(initialize, { langDefinitions }); ``` :::note **Dynamic language switching**: To switch languages after initialization, you'll need to re-initialize the drop-in with the new `langDefinitions`. Store your translations and re-run the initialization code with the selected language. ::: > Test the text length in all languages—longer translations may affect the UI layout. ## Advanced patterns ### Organize by drop-in ``` src/config/dictionaries/ cart.js checkout.js user-auth.js ``` ```javascript title="scripts/initializers/cart.js" const langDefinitions = { default: { ...cartDictionary, }, }; await initializers.mountImmediately(initialize, { langDefinitions }); ``` ### Use JSON ```json title="scripts/config/dictionaries/en_US.json" { "Cart": { "MiniCart": { "heading": "Cart ({count})" } } } ``` ```javascript title="scripts/initializers/cart.js" const langDefinitions = { default: { ...enUS, }, }; await initializers.mountImmediately(initialize, { langDefinitions }); ``` ### Load from CMS ```javascript title="scripts/initializers/cart.js" const translations = await fetch('/api/translations/cart/en_US') .then(res => res.json()); const langDefinitions = { default: { ...translations, }, }; await initializers.mountImmediately(initialize, { langDefinitions }); ``` --- ## Best practices 1. **Keep placeholders** - Values with `{count}`, `{price}`, and so on, must keep these placeholders for dynamic data injection 2. **Use version control** - Track all custom dictionaries in Git 3. **Start small** - Override a few keys, test them, then iterate 4. **Document the changes** - Add comments explaining why certain values were customized 5. **Test the text length** - Longer translations can break the UI layouts 6. **Check for updates** - New drop-in versions may add dictionary keys ## Troubleshooting **Custom values not appearing:** - Verify that `langDefinitions` is passed to `initialize()` - Check that the locale key matches exactly (`en_US` not `en-US`) - Ensure that the dictionary structure matches the defaults - Check the console for initialization errors **Missing dynamic values (counts, prices):** ```javascript // ❌ Bad heading: "Shopping Cart" // ✅ Good - keep {count} placeholder heading: "Shopping Cart ({count})" ``` **Language not switching:** Some components need to re-render after `setLang()`. Try refreshing the page or re-initializing the drop-in. --- **Related:** [Initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/initialization/) • [Labels](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/labeling/) • [Branding](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/branding/) --- # Events Drop-in components implement an event-driven architecture that uses the `@dropins/tools/event-bus.js` module to facilitate communication between components. This event system enables drop-ins to respond to application state changes, maintain loose coupling between components, and keep their state synchronized across your storefront. > **Looking for the API reference?** For detailed API documentation including methods like `events.on()`, `events.emit()`, and advanced features like scoping, see the [Event Bus API Reference](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/events/). ## Event system architecture The system uses a publish-subscribe pattern where components can: 1. **Subscribe** to specific events using `events.on()` 2. **Emit** events using `events.emit()` 3. **Unsubscribe** using `subscription.off()` This pattern allows drop-ins to communicate without having direct dependencies on each other, making your storefront more modular and maintainable. ### Multiple storefront routes > The bus coordinates drop-ins on the same loaded document. It does not send events from one full page navigation to the next. When the shopper moves from a cart route to a checkout route, the checkout document loads a new bus instance; cart continuity comes from Commerce (server-side cart) and from your storefront wiring that rehydrates the cart on the new page. The Commerce boilerplate persists the cart id when `cart/data` fires (`persistCartDataInSession` in the https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/scripts/initializers/index.js) and imports the https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/scripts/initializers/cart.js on startup. The checkout block listens on that new page's bus; see https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/commerce-checkout/commerce-checkout.js for `cart/initialized` and related handlers. For a shorter introduction, read [How drop-ins coordinate on a page](https://experienceleague.adobe.com/developer/commerce/storefront/get-started/architecture/drop-ins-on-a-page/). For synchronous reads from code, see [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/) (`getCartDataFromCache`). ```mermaid %%{init: {'theme':'base', 'themeVariables': { 'edgeLabelBackground':'#ffffff'}}}%% graph LR Cart(Cart Drop-in) Checkout(Checkout Drop-in) Auth(User Auth Drop-in) EventBus(Event Bus) Custom(Custom Code) Cart -->|emits cart/updated| EventBus EventBus -->|cart/updated| Checkout Auth -->|emits authenticated| EventBus EventBus -->|authenticated| Cart EventBus -->|authenticated| Checkout Custom -->|emits locale| EventBus EventBus -->|locale| Cart style EventBus fill:#fef3c7,stroke:#f59e0b,stroke-width:3px style Cart fill:#dbeafe,stroke:#3b82f6,stroke-width:2px style Checkout fill:#e0e7ff,stroke:#6366f1,stroke-width:2px style Auth fill:#f3e8ff,stroke:#a855f7,stroke-width:2px style Custom fill:#fce7f3,stroke:#ec4899,stroke-width:2px ``` Emits Only) DropinB(Drop-in B Listens Only) DropinC(Drop-in C Emits and Listens) EventBus(Event Bus) External1(Other Components) External2(Other Components) DropinA -->|emits| EventBus EventBus -.->|listens| External1 External2 -->|emits| EventBus EventBus -.->|listens| DropinB DropinC -->| emits| EventBus EventBus -.->|listens| DropinC linkStyle 0 stroke-width:2px linkStyle 1 stroke-width:1.5px linkStyle 2 stroke-width:2px linkStyle 3 stroke-width:1.5px linkStyle 4 stroke-width:2px linkStyle 5 stroke-width:1.5px style DropinA fill:#dbeafe,stroke:#3b82f6,stroke-width:2px style DropinB fill:#e0e7ff,stroke:#6366f1,stroke-width:2px style DropinC fill:#f3e8ff,stroke:#a855f7,stroke-width:2px style EventBus fill:#fef3c7,stroke:#f59e0b,stroke-width:3px style External1 fill:#f1f5f9,stroke:#64748b,stroke-width:1.5px style External2 fill:#f1f5f9,stroke:#64748b,stroke-width:1.5px `} caption="Three types of event flow: emits only (blue), listens only (indigo), and bidirectional (purple)."> > Each drop-in's event documentation clearly indicates which events it emits and which it listens to. This helps you understand the data flow in your storefront. ## Event subscription Components subscribe to events to listen for and respond to the changes elsewhere in the application. ### Subscription syntax To subscribe to an event, provide: 1. The **event name** (as a string) 2. An **event handler** callback function that receives the payload 3. Optional **configuration** parameters ```javascript const subscription = events.on('event-name', handler, options); ``` ### Subscription options Event subscriptions support an optional configuration parameter: - **`eager: true`**: The handler executes immediately if the event has been emitted previously - **`eager: false`** (default): The handler only responds to future emissions of the event See [Best Practices](#best-practices) for detailed guidance on using eager mode effectively. ### Example: Subscribing to an event Listen to an initialization event: ```javascript // Subscribe to the event const subscription = events.on('cart/initialized', (data) => { console.log('Cart initialized with data:', data); // Handle the cart data updateUI(data); }); // Later, unsubscribe when no longer needed subscription.off(); ``` ## Event emission Components emit events to share information with other components, drop-ins, or external systems. ### Emission syntax To emit an event, provide: 1. The **event name** (as a string) 2. The **payload** containing the data to share ```javascript events.emit('event-name', payload); ``` ### Example: Emitting an event Emit an event when state changes: ```javascript function updateCartQuantity(itemId, quantity) { // Update the cart const updatedCart = performCartUpdate(itemId, quantity); // Notify other components about the change events.emit('cart/updated', updatedCart); } ``` --- ## Common events reference These three events are shared across multiple drop-ins. Your storefront code emits `authenticated` and `locale`; drop-ins emit `error` for your code to handle. | Event | Category | Used By | Description | |-------|----------|---------|-------------| | [authenticated](#authenticated) | Authentication | Most B2C & B2B drop-ins | Authentication state changes | | [error](#error) | Error Handling | Most drop-ins | Error notifications | | [locale](#locale) | Localization | All drop-ins | Language/locale changes | ### authenticated Category: Authentication Direction: Emitted by an external source, listened to by drop-ins Used By: Cart, Checkout, Order, User Account, User Auth, Wishlist, and most B2B drop-ins Fires when the user's authentication state changes (login, logout, token refresh, session expiration). Drop-ins listen to this event to update their internal state and UI. #### When to emit Emit this event from your storefront when: - An authentication token is refreshed - The authentication state is restored (for example, page refresh with active session) - A session expires - A user logs out - A user successfully logs in #### Data payload ```typescript boolean ``` `true` = user is authenticated. `false` = user is not authenticated or has logged out. #### Usage examples ```javascript // User logged in events.emit('authenticated', true); // User logged out events.emit('authenticated', false); ``` ```javascript const authListener = events.on('authenticated', (isAuthenticated) => { if (isAuthenticated) { // Update UI, load user-specific data, etc. } else { // Clear user data, redirect to login, etc. } }); // Stop listening when no longer needed authListener.off(); ``` --- ### error Category: Error Handling Direction: Emitted by drop-ins, external code listens Used By: Most drop-ins for error reporting Fires when a drop-in encounters an error (API failure, validation error, network timeout). Listen to this event to display error messages, log errors, or trigger error recovery logic. #### When emitted Drop-ins emit this event when: - API requests fail - Critical operations fail - Network errors occur - Validation fails #### Data payload ```typescript { message: string; code?: string; details?: any; source?: string; } ``` #### Usage examples ```javascript const errorListener = events.on('error', (error) => { console.error('Drop-in error:', error.message); showErrorNotification(error.message); if (error.code === 'AUTH_EXPIRED') { redirectToLogin(); } }); errorListener.off(); ``` --- ### locale Category: Localization Direction: Emitted by external source, listened to by drop-ins Used By: All drop-ins with internationalization support Fires when the application's language or locale changes. Drop-ins listen to update their text content, date formatting, currency display, and other locale-specific elements. #### When to emit Emit this event from your storefront when: - A user selects a different language - The application detects and applies a locale based on user preferences - The locale is programmatically changed #### Data payload ```typescript string ``` The locale string should follow standard format (for example, `en-US`, `fr-FR`, `de-DE`). #### Usage examples ```javascript // User selects a new language events.emit('locale', 'fr-FR'); // Or based on browser detection const userLocale = navigator.language || 'en-US'; events.emit('locale', userLocale); ``` ```javascript const localeListener = events.on('locale', (newLocale) => { updateTranslations(newLocale); }); localeListener.off(); ``` --- ## Best practices ### Use type-safe event names Import event types when available to ensure you're using the correct event names: ```typescript // TypeScript will validate the event name events.on('cart/initialized', (data) => { // ... }); ``` ### Use eager mode wisely Set `eager: true` when you need the current state immediately: ```javascript // Good: Getting initial state on component mount events.on('cart/data', (data) => { initializeComponent(data); }, { eager: true }); // Good: Only responding to future changes events.on('cart/updated', (data) => { updateComponent(data); }, { eager: false }); ``` ### Keep handlers focused Event handlers should be small and focused on a single responsibility: ```javascript // Good: Focused handler events.on('cart/updated', (cart) => { updateCartBadge(cart.itemCount); }); // Avoid: Handler doing too much events.on('cart/updated', (cart) => { updateCartBadge(cart.itemCount); updateMiniCart(cart); recalculateTotals(cart); logAnalytics(cart); // Too many responsibilities }); ``` ### Use state management helpers Use `events.lastPayload('')` to retrieve the most recent state without waiting for the next event: ```javascript // Get current authentication state const isAuthenticated = events.lastPayload('authenticated'); if (isAuthenticated) { console.log('User is authenticated'); } // Get current locale const currentLocale = events.lastPayload('locale'); console.log('Current locale:', currentLocale); ``` ### Handle errors gracefully Always include error listeners in production applications to gracefully handle failures and provide helpful feedback to users. --- ## Event sources: External vs. Internal Events can originate from different sources in your storefront: **External events** are fired by: - Your storefront application code (authentication, locale changes) - Other drop-ins (cart updates affecting checkout) - Third-party integrations (payment processors, analytics) **Internal events** are fired by: - Components within the same drop-in (checkout steps communicating with each other) - Drop-in initialization and state management Understanding whether an event is external or internal helps you determine: - Where to emit the event in your custom code - Which events you need to handle from your storefront - How drop-ins coordinate internally vs. with the broader application The following diagram illustrates this using the Checkout drop-in as an example: Integrations) end subgraph EventBus["Event Bus"] EB(Central Event Bus) end subgraph Checkout["Checkout"] direction TB Container1(Address Form) Container2(Shipping Methods) Container3(Payment Form) Container4(Order Summary) end Storefront -->|authenticated, locale| EB Cart -->|cart/initialized, cart/updated, cart/data| EB ThirdParty -->|payment/complete| EB EB -.->|External Events| Container1 EB -.->|External Events| Container2 EB -.->|External Events| Container3 EB -.->|External Events| Container4 Container2 ==>|Internal Events| Container4 Container3 ==>|Internal Events| Container4 linkStyle 3 stroke:#6366f1,stroke-width:1.5px linkStyle 4 stroke:#6366f1,stroke-width:1.5px linkStyle 5 stroke:#6366f1,stroke-width:1.5px linkStyle 6 stroke:#6366f1,stroke-width:1.5px linkStyle 7 stroke:#6366f1,stroke-width:3px linkStyle 8 stroke:#6366f1,stroke-width:3px style EventBus fill:#fef3c7,stroke:#f59e0b,stroke-width:2px style Checkout fill:#e0e7ff,stroke:#6366f1,stroke-width:2px style Storefront fill:#fce7f3,stroke:#ec4899,stroke-width:1.5px style Cart fill:#fce7f3,stroke:#ec4899,stroke-width:1.5px style ThirdParty fill:#fce7f3,stroke:#ec4899,stroke-width:1.5px style EB fill:#fef3c7,stroke:#f59e0b,stroke-width:2px style Container1 fill:#e0e7ff,stroke:#6366f1,stroke-width:1.5px style Container2 fill:#e0e7ff,stroke:#6366f1,stroke-width:1.5px style Container3 fill:#e0e7ff,stroke:#6366f1,stroke-width:1.5px style Container4 fill:#e0e7ff,stroke:#6366f1,stroke-width:1.5px `} caption="External events (thin dashed arrows) flow from outside sources through the Event Bus to the drop-in. Internal events (thick solid arrows) coordinate between containers within the same drop-in."> The Checkout drop-in: - **Listens to external events**: `authenticated`, `cart/initialized`, `cart/updated`, `cart/merged`, `cart/reset`, `cart/data`, `locale` - **Uses internal events**: `checkout/initialized`, `checkout/updated`, `shipping/estimate` (for coordinating between its own containers) ## Event declaration Events are strongly typed using TypeScript declaration merging to provide type safety and autocomplete support. Each drop-in declares its events by extending the `Events` interface from the event bus. ### Basic declaration Here's a simplified example of how events are declared: ```typescript title="event-bus.d.ts" declare module '@adobe-commerce/event-bus' { interface Events { 'dropin/initialized': DataModel | null; 'dropin/updated': DataModel | null; 'dropin/data': DataModel; authenticated: boolean; locale: string; error: { source: string; type: string; error: Error }; } } ``` ### Complete declaration example In practice, drop-ins declare their events with imports and type extensions. Here's a more comprehensive example from the Checkout drop-in: ```typescript title="event-bus.d.ts" declare module '@adobe-commerce/event-bus' { interface Events { 'cart/initialized': CartModel | null; 'cart/updated': CartModel | null; 'cart/reset': void; 'cart/merged': { oldCartItems: any[] }; 'checkout/initialized': CheckoutData | null; 'checkout/updated': CheckoutData | null; 'checkout/values': ValuesModel; 'shipping/estimate': ShippingEstimate; authenticated: boolean; error: { source: string; type: string; error: Error }; } interface Cart extends CartModel {} } ``` This pattern allows TypeScript to provide autocomplete and type checking for both event names and their payloads throughout your application. --- ## Next steps - Review the [Event Bus API Reference](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/events/) for detailed API methods and code examples - Check individual drop-in event pages for component-specific events - Try drop-in tutorials for practical event usage examples --- # Extend, substitute, or create? You can build on the drop-ins Adobe provides in more than one way, and the path you pick changes how much is supported for you and how much you maintain yourself. The sections below step through the tradeoffs. Start with the recommended path, then read the others if your requirements rule it out. ## Choose your approach ### EXTEND When you Extend (Customize existing drop-ins through supported extension points such as slots, events, styling, transformers, and configuration.) a drop-in, you keep the Adobe package and add behavior or UI through the extension levers the product exposes. This is the path that Adobe is set up to support and keep compatible across releases. > **Recommended approach** Most needs are met with the levers in Extension methods below—without replacing the whole package or writing a new drop-in from scratch. **Extend a drop-in if you need to:** - Change how drop-ins look or behave - Add custom content or UI elements - Integrate third-party services (like payment methods) - Respond to drop-in events with custom logic - Modify how data is displayed or processed **Note**: You can integrate third-party services using slots without replacing the entire drop-in. For example, integrate Stripe or PayPal payment methods into the Checkout drop-in rather than replacing the entire checkout flow. #### Extension methods: - **Slots (An extension point inside a drop-in where custom UI or behavior can be added, replaced, or removed.)** - Inject custom HTML/components at predefined points - **Styling (Visual customization of drop-ins through CSS overrides, token changes, and layout adjustments.)** - Override CSS, modify layouts, replace components - **Events (Data or lifecycle signals emitted by drop-ins that custom code can listen to in order to run additional behavior.)** - Listen to data events and add custom behavior - **Configuration (Settings used to change behavior without rewriting core implementation logic.)** - Modify drop-in settings and options - **Transformers (Functions that modify or shape data before a drop-in displays it.)** - Change how drop-ins process and display data #### Benefits: - Fully supported by Adobe - Automatic compatibility with updates - Lower maintenance overhead - Access to new features and bug fixes ### SUBSTITUTE Use Substitute (Replace an Adobe drop-in with a third-party implementation and own compatibility and maintenance responsibility.) when you will put a third-party solution (An external service or component used in place of a native Adobe drop-in implementation.) in place of the Adobe drop-in for that part of the experience, so you own integration, updates, and API compatibility—not when you only need a contained integration (for example, a payment provider you wire in while still extending the Checkout drop-in). > **Proceed with caution** If you substitute, you are responsible for keeping compatibility with Commerce APIs and keeping up with changes yourself. **Replace an Adobe drop-in with a full third-party solution if you have:** - Complete solutions from a single provider (not just payment methods) - Specialized functionality that doesn't align with Adobe's approach - Legacy system integration requirements - Provider-specific workflows requiring their complete UI and logic #### Risks and responsibilities: - **Maintenance burden** - You own all updates, bug fixes, and compatibility - **API changes** - Must adapt to Commerce API changes independently - **Feature gaps** - May miss out on new Commerce features - **Support limitations** - Adobe cannot provide support for third-party code #### Handling analytics instrumentation: A substituted drop-in no longer emits the ACDL events that analytics, personalization, product recommendations, and Live Search rely on. You'll need to publish those events yourself: - [Analytics events reference](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/analytics-events/) - the event lifecycle and the events each drop-in emits - [Instrument analytics events without drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/how-tos/instrument-analytics-events/) - a tutorial showing how to publish those events with a helper script ### CREATE The SDK (The Drop-in SDK used to build custom drop-ins and related integration logic.) is what you use to Create (Build a new drop-in from scratch when extension and substitution are not suitable for the required experience.) a new drop-in package. Reserve that for cases where you need a whole new feature surface that the existing family of drop-ins does not cover, and you can commit to owning it over time. > **Early access considerations** The drop-in SDK is in early access, with limited third-party support. Before you invest, contact Adobe to discuss your use case in the https://discordapp.com/channels/1131492224371277874/1220042081209421945. **Create a drop-in if you:** - Have a use case that no existing drop-in addresses - Are building entirely new functionality for multiple storefronts or brands - Have the resources and expertise for long-term maintenance ## Boundaries and limitations Drop-ins work best for certain types of functionality. Understanding these boundaries helps you choose the right approach: #### Drop-ins excel at: - Commerce-specific UI components (product displays, cart management, checkout flows) - Data-driven interfaces that connect to Commerce APIs - Reusable functionality across multiple storefronts - Components that benefit from Commerce's styling and theming system #### Consider alternatives for these use cases: - Simple static content (use HTML/CSS instead) - Third-party integrations with existing UI (use vendor scripts) - Highly merchant-specific logic (use application-level code) - Temporary A/B testing variants (use feature flags) - Single-use, non-reusable customizations ## Need a new extension point? If existing drop-ins don't provide the slots or events you need: 1. **Document your use case** - Explain what you're trying to achieve 1. **Identify the gap** - What specific slot or event is missing? 1. **Submit a request** - Share your requirements in the https://discordapp.com/channels/1131492224371277874/1220042081209421945 ## FAQs **Q: Why does Adobe recommend extending over building new drop-ins?** Extending is fully supported, maintains compatibility with updates, and reduces maintenance overhead. Most customization needs can be met through extension without the risks associated with building from scratch or substituting drop-ins. **Q: When is it acceptable to substitute a drop-in?** Substitution is acceptable when you need complete solutions from a single provider, have specialized functionality that doesn't align with Adobe's approach, need legacy system integration, or require provider-specific workflows with their complete UI and logic. However, you become responsible for maintaining compatibility with Commerce APIs and handling all updates independently. **Q: Is the drop-in SDK ready for production use?** No. The SDK is currently in early access with limited third-party support and no timeline for full support. Contact Adobe before investing in custom drop-in development. **Q: What extensibility options are available beyond slots?** While slots are the primary mechanism, you can also: - Use configuration options to customize behavior - Change how drop-ins look or behave (styling and layouts) - Respond to drop-in events with custom logic - Modify transformers to change how drop-ins process data **Q: How do I know if my use case requires a new drop-in?** Follow the decision flow in this guide. Most needs can be met by extending existing drop-ins. Only consider building new drop-ins if you have a use case that no existing drop-in addresses, are building entirely new functionality for multiple storefronts or brands, and have the resources and expertise for long-term maintenance. **Q: What happens if I substitute a drop-in and Commerce APIs change?** You're responsible for updating your substitute to maintain compatibility. Adobe cannot provide support for third-party substitutes, and you may miss out on new features or security updates. --- # Extending Drop-In Components Drop-in components are designed to be flexible and extensible. This guide provides an overview of how to extend drop-in components to add new features, integrate with third-party services, and customize the user experience. ## Extend drop-ins with Commerce APIs The following steps describe how to add existing Commerce API services to a drop-in. For example, the Commerce API provides the necessary endpoints to fetch and update gift messages through GraphQL, but the checkout drop-in doesn't provide this feature out of the box. We will extend the checkout drop-in by adding a UI for gift messages, use the Commerce GraphQL API to update the message data on the cart, and extend the cart drop-in to include the message data when it fetches the cart. ### Step-by-step ### 1. Add your UI to the drop-in The first step is to create a UI for the feature and add it to the checkout drop-in. You can implement the UI however you want, as long as it can be added to the HTML DOM. For this example, we'll implement a web component (`GiftOptionsField`) that provides the form fields needed to enter a gift message. Here's an example implementation of the UI component: ```js title='gift-options-field.js' const sdkStyle = document.querySelector('style[data-dropin="sdk"]'); const checkoutStyle = document.querySelector('style[data-dropin="checkout"]'); class GiftOptionsField extends HTMLElement { static observedAttributes = ['cartid', 'giftmessage', 'fromname', 'toname', 'loading']; constructor() { super(); this.attachShadow({ mode: 'open' }); this._submitGiftMessageHandler = (event) => { event.preventDefault(); } } set submitGiftMessageHandler(callback) { this._submitGiftMessageHandler = callback; } connectedCallback() { this._formTemplate = document.createElement('template'); this._formTemplate.innerHTML = `

Gift Message

`; this.render(); } attributeChangedCallback(name, oldValue, newValue) { const toName = this.shadowRoot.querySelector('input[name="toName"]'); const fromName = this.shadowRoot.querySelector('input[name="fromName"]'); const giftMessage = this.shadowRoot.querySelector('textarea[name="giftMessage"]'); const cartId = this.shadowRoot.querySelector('input[name="cartId"]'); switch (name) { case 'cartid': cartId.value = newValue; break; case 'giftmessage': giftMessage.value = newValue; break; case 'fromname': fromName.value = newValue; break; case 'toname': toName.value = newValue; break; case 'loading': if (newValue) { toName?.setAttribute('disabled', ''); fromName?.setAttribute('disabled', ''); giftMessage?.setAttribute('disabled', ''); } else { toName?.removeAttribute('disabled'); fromName?.removeAttribute('disabled'); giftMessage?.removeAttribute('disabled'); } break; } } render() { this.shadowRoot.innerHTML = ''; this.shadowRoot.appendChild(this._formTemplate.content.cloneNode(true)); this.shadowRoot.querySelector('input[name="cartId"]').value = this.getAttribute('cartId'); this.shadowRoot.querySelector('#gift-options-form').addEventListener('submit', this._submitGiftMessageHandler?.bind(this)); const submitWrapper = this.shadowRoot.querySelector('.submit-wrapper'); const fromNameWrapper = this.shadowRoot.querySelector('.fromName-wrapper'); const toNameWrapper = this.shadowRoot.querySelector('.toName-wrapper'); const giftMessageWrapper = this.shadowRoot.querySelector('.giftMessage-wrapper'); UI.render(Input, { type: "text", name: "toName", placeholder: "To name", floatingLabel: "To name", value: this.getAttribute('toName'), disabled: !!this.hasAttribute('loading') })(toNameWrapper); UI.render(Input, { type: "text", name: "fromName", placeholder: "From name", floatingLabel: "From name", value: this.getAttribute('fromName'), disabled: !!this.hasAttribute('loading') })(fromNameWrapper); UI.render(TextArea, { name: "giftMessage", placeholder: "Message", value: this.getAttribute('giftMessage'), disabled: !!this.hasAttribute('loading') })(giftMessageWrapper); UI.render(Button, { variant: "primary", children: "Add Message", type: "submit", enabled: true, size: "medium", disabled: !!this.hasAttribute('loading') })(submitWrapper); this.shadowRoot.appendChild(sdkStyle.cloneNode(true)); this.shadowRoot.appendChild(checkoutStyle.cloneNode(true)); } } customElements.define('gift-options-field', GiftOptionsField); ``` ### 2. Render the UI into the checkout drop-in Next, we need to render the `GiftOptionsField` component into the checkout page by creating the `gift-options-field` https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements. ```js const GiftOptionsField = document.createElement('gift-options-field'); GiftOptionsField.setAttribute('loading', 'true'); ``` Then, insert the custom element into the layouts defined on the checkout page. The following example updates the render function for mobile and desktop to insert the `giftOptionsField` element into the layouts. ```js title='commerce-checkout.js' function renderMobileLayout(block) { root.replaceChildren( heading, giftOptionsField, ... ); block.replaceChildren(root); } function renderDesktopLayout(block) { main.replaceChildren( heading, giftOptionsField, ... ); block.replaceChildren(block); } ``` ### 3. Add handler for gift message submission Now that we have the UI in place, we need to add a handler to save the gift message data. We'll use the `fetchGraphl()` function from the API to send a GraphQL mutation to set the gift message on the cart. ```js title='commerce-checkout.js' giftOptionsField.submitGiftMessageHandler = async (event) => { event.preventDefault(); const form = event.target; const formData = new FormData(form); const cartId = formData.get('cartId'); const fromName = formData.get('fromName'); const toName = formData.get('toName'); const giftMessage = formData.get('giftMessage'); giftOptionsField.setAttribute('loading', 'true'); console.log('form data', cartId, fromName, toName, giftMessage); const giftMessageInput = { from: fromName, to: toName, message: giftMessage, } fetchGraphQl(` mutation SET_GIFT_OPTIONS($cartId: String!, $giftMessage: GiftMessageInput!) { setGiftOptionsOnCart(input: { cart_id: $cartId, gift_message: $giftMessage printed_card_included: false }) { cart { id gift_message { from to message } } } } `, { variables: { cartId, giftMessage: giftMessageInput, }, }).then(() => { refreshCart(); giftOptionsField.removeAttribute('loading'); }); }; ``` ### 4. Extend the data payload for the drop-in To extend the data payload of a drop-in, first you need to update the GraphQL fragment used by the cart drop-in to request the additional field. This is done by modifying the `build.mjs` script at the root of your storefront project. In the following example, the `CART_FRAGMENT` fragment is extended to include the gift message data whenever the cart drop-in requests the cart data from GraphQL: ```js title='build.mjs' /* eslint-disable import/no-extraneous-dependencies */ // Extend the cart fragment to include the gift message overrideGQLOperations([ { // The name of the drop-in to extend npm: '@dropins/storefront-cart', // Additional fields to include in the cart results (gift_message) operations: [ `fragment CART_FRAGMENT on Cart { gift_message { from to message } }` ], }, ]); ``` When you run the install command, the `build.mjs` script generates a new GraphQL query for the cart drop-in that includes the `gift_message` data. ### 5. Add new data to the payload Map the new GraphQL data to the payload data that the cart events provide to listeners so they can access the gift message values. Configure the cart drop-in's initializer to add the new cart data to the existing cart payload. This is done by defining a transformer function on the CartModel. This function receives the GraphQL data and returns an object that gets merged with the rest of the cart payload. As an example, here is how it might be configured: ```js title='cart.js' /* eslint-disable import/no-cycle */ initializeDropin(async () => { await initializers.mountImmediately(initialize, { models: { CartModel: { transformer: (data) => { const { gift_message: giftMessage } = data; return { giftMessage, } } } } }); })(); ``` Now when the cart emits an event with cart data, the `giftMessage` data is included. ### 6. Retrieve the data and render it Get the data from the cart event and use it to populate the gift message fields on the checkout page. Here's an example of how you might do this: ```js title='commerce-checkout.js' // Event listener to hydrate the new fields with the cart data events.on('cart/data', data => { if (!data) return; const { id, orderAttributes, giftMessage } = data; // Update gift options fields giftOptionsField.setAttribute('cartId', id); if(giftMessage) { giftOptionsField.setAttribute('giftmessage', giftMessage.message); giftOptionsField.setAttribute('fromname', giftMessage.from); giftOptionsField.setAttribute('toname', giftMessage.to); } giftOptionsField.removeAttribute('loading'); }, { eager: true }); ``` ### 7. Summary After just a few changes, we were able to add a new feature to the checkout drop-in that allows users to add a gift message to their order. We added a new UI component, integrated the Commerce API to fetch and update gift messages, and extended the data payload for the drop-in to include the gift message data. You can apply these same concepts to any drop-in. ## Extendable fragments by drop-in Each drop-in exports one or more `GraphQL` fragments that you can extend using `overrideGQLOperations` in your `build.mjs` file. Extending a fragment adds custom fields to the drop-in's existing queries without replacing them. | Drop-in package | Fragment name | GraphQL type | Description | |---|---|---|---| | `@dropins/storefront-cart` | `CART_FRAGMENT` | `Cart` | Extends cart queries with additional fields on the `Cart` type. | | `@dropins/storefront-checkout` | `CHECKOUT_DATA_FRAGMENT` | `Cart` | Extends checkout queries with additional fields on the `Cart` type. | | `@dropins/storefront-order` | `GUEST_ORDER_FRAGMENT` | `CustomerOrder` | Extends unauthenticated guest order detail queries with additional fields on the `CustomerOrder` type. | | `@dropins/storefront-order` | `CUSTOMER_ORDER_FRAGMENT` | `CustomerOrder` | Extends authenticated order detail queries with additional fields on the `CustomerOrder` type. | | `@dropins/storefront-account` | `CUSTOMER_ORDER_FRAGMENT` | `CustomerOrder` | Extends the orders list query with additional fields on the `CustomerOrder` type. | ### Extending order and account queries To add custom fields to order data, extend the fragments for the order and account drop-ins in your `build.mjs` file. The following example shows how to add a `custom_attribute` field to both the order detail and orders list pages: ```js title='build.mjs' overrideGQLOperations([ { npm: '@dropins/storefront-order', operations: [ `fragment GUEST_ORDER_FRAGMENT on CustomerOrder { custom_attribute }`, `fragment CUSTOMER_ORDER_FRAGMENT on CustomerOrder { custom_attribute }`, ], }, { npm: '@dropins/storefront-account', operations: [ `fragment CUSTOMER_ORDER_FRAGMENT on CustomerOrder { custom_attribute }`, ], }, ]); ``` After updating `build.mjs`, run `npm install` to apply the fragment extensions. This needs to be re-run whenever you install new packages since it patches files inside `node_modules`. > You can extend multiple drop-ins in a single `overrideGQLOperations` call. Each entry in the array targets a different drop-in package. > The `returns` field on `CustomerOrder` cannot be extended via fragments because it requires query-specific arguments (like `pageSize`). To extend return data, use the model transformer approach instead. ### Mapping extended data with model transformers After extending a `GraphQL` fragment, the response includes the new fields, but they are not automatically displayed. Use model transformers in the drop-in initializer to map the new fields into the drop-in data model. To make `custom_attribute` available in the order drop-in's data model, add a model transformer in the initializer: ```js title='scripts/initializers/order.js' await initializers.mountImmediately(initialize, { models: { OrderDataModel: { transformer: (data) => ({ customAttribute: data?.custom_attribute, }), }, }, }); ``` The transformer function receives the `GraphQL` response data and returns an object that the system merges into the existing data model. Only the fields you return are overridden; all other data renders normally. > Each drop-in has its own set of customizable models. See the initialization page of each drop-in for the full list of available models: - [Order initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/initialization/#customizing-data-models) - [User Account initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/user-account/initialization/#customizing-data-models) - [Cart initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/initialization/#customizing-data-models) - [Checkout initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/initialization/#customizing-data-models) ## Extend drop-ins with third-party components The following steps guide you through adding a third-party component to a drop-in. We'll add a fictitious ratings & reviews component to the product details drop-in as an example. ### Prerequisites - Third-party component API key. You typically need an API key to fetch data for the component. - Familiarity with https://www.aem.live/docs/configuration. ### What you'll learn - How to configure third-party API keys for use in drop-ins. - How to use the `EventBus` to emit events and listen for events from the third-party component. - How to delay loading large data sets from third-party components to improve page performance. ### Step-by-step ### 1. Add your third-party API key Add your API key to your commerce configuration in your project's `config.json` file. ```json { "public": { "default": { "commerce-core-endpoint": "MY_ENDPOINT", // other config... "third-party-api-key": "THIRD_PARTY_API_KEY" } } } ``` ### 2. Fetch the API key To fetch the API key, you need to import the `getConfigValue` function from the `configs.js` file. This function reads the API key from the config file and returns the value. You can then use this value to fetch data from the third-party service. ```js export default async function decorate(block) { // Fetch API key from the config file const thirdPartyApiKey = await getConfigValue('third-party-api-key'); // Fetch the component data setRatingsJson(product, thirdPartyApiKey); } ``` ### 3. Fetch the component data After the page loads, your third-party component likely needs to fetch some data. In our case, our ratings & reviews component needs to fetch data from its rating service to display the star-rating for the product. After your API key is fetched (`thirdPartyApiKey`), you can trigger a call to the service's endpoint and use the EventBus to emit an event when the data is received. ```js function setRatingsJson(product, thirdPartyApiKey) { try { fetch(`https://api.rating.service.com/products/${thirdPartyApiKey}/${product.externalId}/bottomline`).then(e => e.ok ? e.json() : {}).then(body => { const { average_score, total_reviews } = body?.response?.bottomline || {}; setHtmlProductJsonLd({ aggregateRating: { '@type': 'AggregateRating', ratingValue: average_score || 0, reviewCount: total_reviews || 0, } }); events.emit('eds/pdp/ratings', {average: average_score, total: total_reviews}); }); } catch (error) { console.log(`Error fetching product ratings: ${error}`); setHtmlProductJsonLd({ aggregateRating: { '@type': 'AggregateRating', ratingValue: 0, reviewCount: 0, } }); events.emit('eds/pdp/ratings', {average: 0, total: 0}); } } ``` ### 4. Render the component To ensure the least amount of CLS, we'll make sure we don't render the component until after its data is returned. To do this, we need to add an event listener for the third-party component's event. This strategy, along with reserving a predefined space for the component, will minimize CLS. Here's an example implementation for our third-party ratings component: ```js events.on('eds/pdp/ratings', ({ average, total }) => { // Title slot logic const titleSlotElement = document.querySelector('.title-slot'); // Optionally reserve space for the star rating to avoid CLS // e.g., setting a placeholder element or CSS min-height // Render star rating titleSlotElement.innerHTML = ` Average Rating: ${average.toFixed(1)} (${total} reviews) `; }); ``` ### 5. Delay loading large data sets Components like ratings & reviews typically load large blocks of text to display a product's reviews. In such cases, we need to ensure that those reviews are not loaded until the user scrolls near the reviews section or clicks a "View All Reviews" button. This strategy keeps the First Contentful Paint (FCP) and Cumulative Layout Shift (CLS) scores low. The following example uses an Intersection Observer to load reviews only when a user scrolls near the reviews section or clicks "View All Reviews". ```js // Trigger the delayed load when the user scrolls near the reviews section or clicks "View All Reviews" const reviewsSection = document.getElementById('reviews-section'); const loadReviews = () => { // Fetch or render the full reviews only when needed fetch(`/path/to/full-reviews?apiKey=${YOUR_API_KEY}&productId=${PRODUCT_ID}`) .then(response => response.json()) .then(data => { reviewsSection.innerHTML = data.reviewsHtml; }) .catch(console.error); }; // Event listener approach for a "View All Reviews" button document.getElementById('view-reviews-btn').addEventListener('click', loadReviews); // OR intersection observer approach to load when user scrolls near the section const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { loadReviews(); observer.disconnect(); } }); }, { threshold: 0.1 }); observer.observe(reviewsSection); ``` ### 6. Summary Throughout this tutorial, we examined the key steps of integrating a fictitious third-party component. We learned how to configure API keys, fetch data, and delay loading data sets to improve page performance. You can apply these same concepts to any drop-in. --- # Introduction to Drop-In Components At this point in the onboarding path, you should already have a locally running boilerplate storefront. This page explains the drop-in system you'll be working with — what every drop-in is made of, which drop-ins are available, and what you can customize. The next page, [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/), shows the code pattern you write in each block. ## What is a drop-in component? A drop-in component is a ready-made npm package that provides the complete user interface and Commerce logic for one shopper job — cart, checkout, product detail, user sign-in, and so on. The boilerplate ships with all B2C drop-ins pre-installed. You wire them up; you do not build them from scratch. ## Anatomy of a drop-in Every drop-in has three parts. Understanding these three parts is the key to reading and writing Commerce block code. | Part | What it is | Where you find it | |---|---|---| | npm package | The published code for that drop-in | `node_modules/@dropins/storefront-*` and `package.json` | | Initializer | A JavaScript file that configures the drop-in once — sets the GraphQL endpoint, loads placeholder text, and registers the drop-in | `scripts/initializers/.js` | | Containers | The individual UI panels that the drop-in exposes for you to place on a page | Imported from `@dropins/storefront-*/containers/` | When you open a Commerce block file in the boilerplate, you will see all three of these parts: an import of the initializer, an import of a container, and a call that renders the container into a `div` on the page. [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/) shows exactly how those three lines fit together. > **The boilerplate already has this wired** If you are working from the Commerce boilerplate, the initializer files already exist in `scripts/initializers/` and the npm packages are already installed. You can open any Commerce block to see a real example before you write your own. ## Available drop-ins The tables below list every available drop-in. Click a drop-in name to open its reference page, which includes its containers, props, slots, and events. ### B2C drop-ins | Drop-in | Description | | ------- | ----------- | | [Cart](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/) | Summary of items in the cart; view and manage cart contents, update quantities, and proceed to checkout. | | [Checkout](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/) | Streamlined process for completing a purchase: shipping and payment information, order review, and confirmation. | | [Order](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/) | Tools and containers to manage and display order-related data across pages; supports customer accounts and guest workflows. | | [Payment Services](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/payment-services/) | Renders the credit card form and Apple Pay button for payment details; supports credit/debit cards and Apple Pay. | | [Personalization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/personalization/) | Displays content conditionally based on Adobe Commerce customer groups, segments, and cart price rules. | | [Product Details](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/) | Detailed product information: SKUs, pricing, descriptions, options; supports internationalization and accessibility. | | [Product Discovery](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-discovery/) | Search results, category listings, and faceted navigation so customers can find and explore products. | | [Recommendations](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/recommendations/) | Suggests products from browsing patterns (e.g. "Customers who viewed this also viewed"); manageable from Adobe Commerce Admin. | | [User Account](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/user-account/) | Personalized experience: order history, account settings, and other account-related features. | | [User Authentication](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/user-auth/) | Sign up, sign in, and log out; supports account confirmation, password reset, and optional ReCAPTCHA. | | [Wishlist](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/wishlist/) | Lets guests and registered customers save products to purchase later. | ### B2B drop-ins Business-to-business (B2B) drop-ins cover workflows such as company administration, negotiable quotes, and purchase orders. You wire them up with the same three parts as in [Anatomy of a drop-in](#anatomy-of-a-drop-in): npm package, initializer, and containers. Enable B2B features on your Commerce instance so the APIs and company data these packages expect are available. | Drop-in | Description | | ------- | ----------- | | [Company Management](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-management/) | Company profile management, role-based permissions, legal address and contact information. | | [Company Switcher](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/company-switcher/) | Switch between multiple companies a user is associated with; company context and GraphQL header management. | | [Purchase Order](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/purchase-order/) | Purchase order workflows, approval rules, and purchase order history for B2B transactions. | | [Quote Management](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quote-management/) | Negotiable quotes: request, negotiation, approval, and tracking for B2B customers. | | [Quick Order](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/quick-order/) | Bulk ordering by SKU, search, and CSV upload; Grid Ordering for configurable products on PDP. | | [Requisition List](https://experienceleague.adobe.com/developer/commerce/storefront/dropins-b2b/requisition-list/) | Create and manage requisition lists for repeat and bulk ordering; multiple lists per account. | ## What you can customize Each drop-in exposes several layers of customization. Most projects need only the first two. The rest exist for cases where configuration alone is not enough. The table below shows every approach with links to the details. | Approach | Description | | -------- | ----------- | | [Design tokens](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/branding/) | Override Adobe Commerce design tokens (colors, typography, spacing, shapes) for quick, global brand changes. | | [CSS classes](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/) | Override or add CSS classes to restyle specific areas of a drop-in beyond what tokens provide. | | [Slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/slots/) | Use built-in extension points to add or replace UI and behavior in drop-in components. | | [Content enrichment](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/content-customizations/enrichment/) | Add content above or below commerce blocks by product SKU, category, and the physical position on the page. | | [Localization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/labeling/) | Use the placeholder system to override default drop-in text and support multiple languages. | | [Dictionaries](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/dictionaries/) | Customize drop-in dictionaries (deep-merge) for localization, branding, and multi-language support. | | [Extending](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/extending/) | Add new features and Commerce API integrations to existing drop-ins (for example, gift messages in checkout). | | [Layouts](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/layouts/) | Configure where drop-in containers appear on the page via HTML fragments and block layout. | | [Localizing links](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/linking/) | Manage localized internal links in the boilerplate so users stay within their chosen locale. | | [Extend, substitute, or create?](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/extend-or-create/) | Decide when to extend an existing drop-in, substitute with a third-party solution, or create a new one. | ## What's next You now know what every drop-in is made of, and which ones are available. The next step is writing the code. [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/) shows the three-line pattern — import the initializer, import a container, render it with a complete working example. --- # Labeling and Localizing Drop-In Components The Commerce Boilerplate provides a placeholder system that lets merchants handle labeling (Customizing UI text labels for tone, branding, or clarity while staying in the same language.) without code. Learn to implement placeholder files (JSON files that store storefront UI labels by drop-in and locale so merchants can change text without changing code.) to override default text in drop-in components. > **Merchant vs Developer guides** **Merchants translating content:** See [Commerce localization tasks](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/quick-start/content-localization-commerce-tasks/) for step-by-step guidance on localizing (Adapting UI text and formatting for specific languages and regions, including translated labels and locale-specific conventions.) placeholder files for different locales. **Developers implementing the system:** This guide explains how placeholder files integrate with drop-in dictionaries using `langDefinitions` language objects (Objects such as `langDefinitions` that map translation keys to localized UI text values.). For advanced customization beyond the placeholder system, see the [Dictionary customization guide](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/dictionaries/). ## Big picture Labeling drop-in components in the storefront involves two files: 1. The **placeholders files** that provide the default drop-in component UI labels that merchants can quickly update as needed. 2. The **drop-in block** (examples, `product-details.js`, `cart.js`) where you add code to fetch, map, and override the drop-in component dictionary at runtime. The following diagram shows the process for adding and overriding labels and text for drop-in components within the boilerplate template. ![Diagram of dictionary and placeholder files flowing from developers and merchants into localized text shown in Commerce drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/images/DropinDictionaries.svg) *How localization and labeling works in storefronts.* 1. **Placeholder files**. Merchants can change the storefront labels by changing the values in the placeholder JSON files, which are organized by drop-in components—`cart.json`, `checkout.json`, `pdp.json`, and so on. 1. **Import function**. You need to import the `fetchPlaceholders` function from the boilerplate's `commerce.js` file. 1. **Fetch placeholders.** Use the `fetchPlaceholders` function to retrieve the `placeholders` key-value pairs from the content folder. 1. **Override default dictionary**. Override the `default` property from the `langDefinitions` object with the keys and values from the `placeholder` object. 1. **Initialize dictionary**. Use the `register` function to update the dictionary at runtime. ## Step-by-step In the boilerplate code, the UI text labels in drop-in components come from the placeholder files. By using these files as the source for all storefront UI labels, merchants can easily change labels without involving developers. There are two things to be aware of when using the `fetchPlaceholders()` function: 1. **During initialization**: You must provide the path to the drop-in’s placeholders file. This file will be fetched and merged into the existing placeholders object. Subsequent calls to `fetchPlaceholders()` without a path will return the merged object containing all fetched labels. 2. **After initialization**: You can call `fetchPlaceholders()` without a path to retrieve all initialized placeholders as a single object. This object can be accessed from a Block or anywhere else in the project. ### 1. Import `fetchPlaceholders` function In the drop-in block (for example, `product-details.js`, `cart.js`), import the `fetchPlaceholders` function from the boilerplate's `commerce.js` file. ```javascript ``` ### 2. Initialize placeholders with path During initialization, you must use the `fetchPlaceholders()` function using an argument to the path to your drop-in's placeholders file. This fetches and merges the placeholders into the global object. ```javascript // Initialize placeholders for this drop-in const placeholders = await fetchPlaceholders('placeholders/cart.json'); const langDefinitions = { default: { ...placeholders, }, }; // Register Initializers initializers.mountImmediately(initialize, { langDefinitions, //... }); ``` > **Locale key**: The boilerplate uses `default` as the locale key for the primary language. Under the hood, this maps to the drop-in's `en_US` locale. For multi-language implementations, see the [Dictionary customization guide](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/dictionaries/). ### 3. Fetch placeholders after initialization After initialization, you can use the `fetchPlaceholders` function without a path to retrieve all merged placeholders. The following diagram and code snippet shows how to fetch the placeholders. ![Spreadsheet-style placeholder table in a commerce block with label keys mapped to custom storefront copy for drop-in text](https://experienceleague.adobe.com/developer/commerce/storefront/images/LabelUsage.svg) *Using placeholder labels in your EDS commerce block* ```javascript // Retrieve the placeholders language object const labels = await fetchPlaceholders(); export default async function decorate(block) { const $elem = document.createElement('div'); $elem.innerText = labels.Cart.PriceSummary.shipping.label; } ``` ### 4. Test the changes After you've updated the drop-in component dictionary with the new `langDefinitions` object, test the changes in the storefront to ensure the new labels are displayed correctly. If the labels are not displaying as expected, review the mapping between the placeholder keys and the drop-in component dictionary keys. Make sure the keys match exactly. If the keys don't match, the drop-in component will use the default dictionary values. --- # Commerce block layouts A drop-in component's layout is defined by an HTML fragment that controls where the drop-in's containers appear on the page. You can customize the layout as you would with any HTML, by using CSS and adding, removing, or rearranging the elements in the HTML. In this topic, we'll customize the product details layout by adding the Product Recommendations block. ## Big picture This screenshot shows the product details page with the Product Recommendations block below the product gallery container. ![Add Product Recommendations block to the page](https://experienceleague.adobe.com/developer/commerce/storefront/images/ProductDetailsLayout.png) *Add Product Recommendations block to the page* ## Customize commerce block layouts For this use case, we'll customize the product details layout by adding the Product Recommendations block inside the product details block, instead of below it. ### 1. Add an Edge Delivery block to a commerce page For example, add a Product Recommendations block to the product details page so that it can be rendered on the page, then referenced and moved to the layout (in code): ![Add Product Recommendations block to the page](https://experienceleague.adobe.com/developer/commerce/storefront/images/PrexBlockPDP.png) *Add Product Recommendations block to the page* ### 2. Add an element to the layout and reference it Add an HTML element to the commerce block's layout where you want the Edge Delivery block (or other content) to appear. In this example, we want the Product Recommendations block to appear in the left column of the product-details layout, below the product gallery. So we add a `div` element to the left column with a class of `product-details__prex`. ```js ins={12} export default async function decorate(block) { // eslint-disable-next-line no-underscore-dangle const product = events._lastEvent?.['pdp/data']?.payload ?? null; const labels = await fetchPlaceholders(); // Layout const fragment = document.createRange().createContextualFragment(` `); ``` Then, we reference the `div` element in the layout as follows: ```js // Reference the element const $prex = fragment.querySelector('.product-details__prex'); ``` ### 3. Move the Edge Delivery block to the layout Within the `eds/lcp` lifecycle event, query the Edge Delivery block's class selector from the rendered block and append it to right element in the layout. In this example, we select the Product Recommendations block using the `.product-recommendations` class, then move it to the element you want in the layout (`$prex`). ```js ins={9-12} events.on( 'eds/lcp', () => { if (product) { setJsonLdProduct(product); setMetaTags(product); document.title = product.name; } const $productRecommendations = document.querySelector('.product-recommendations'); if ($productRecommendations) { $prex.appendChild($productRecommendations); } }, { eager: true }, ); ``` --- # Localizing links Learn how the boilerplate automatically localizes internal links for multistore/multilingual storefronts. The system keeps users within their chosen locale as they navigate the site. > **Merchant guide** For merchant-friendly guidance on link localization and using `#nolocal` in store switchers, see [Commerce localization tasks - Link localization](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/quick-start/content-localization-commerce-tasks/#link-localization). ## decorateLinks The `decorateLinks` function automatically prepends all content links with the root path for each language. This ensures users stay within their current locale as they navigate the site. **How it works:** - On `/en-ca/` pages: `/products/` becomes `/en-ca/products/` - On `/fr/` pages: `/products/` becomes `/fr/products/` - Links with `#nolocal` hash are not modified (useful for store switcher links) This function is enabled by default in the Commerce Boilerplate via `scripts/script.js`. ```js /** * Decorates the main element. * @param {Element} main The main element */ export function decorateMain(main) { decorateLinks(main); // enables localization of links decorateButtons(main); decorateIcons(main); buildAutoBlocks(main); decorateSections(main); decorateBlocks(main); } ``` ## rootLink The `rootLink` function prepends the appropriate language root path to a given link. Use it within a block to localize links from a drop-in for loading scripts, styles or links to other pages within a drop-in. This approach ensures consistency across languages and store views. ```js export async function decorateMyBlock(block) { const atag = document.createElement('a'); atag.innerText = 'My Link'; atag.href = rootLink('/my-path'); // returns the localized url for '/my-path' // ... } ``` --- # Using drop-ins Drop-in components add Commerce functionality to your storefront. The https://github.com/hlxsites/aem-boilerplate-commerce includes all drop-ins pre-installed—no package installation needed. ## How to use drop-ins Three steps: import the initializer (A JavaScript module that configures a drop-in when imported, such as setting endpoints, registering dictionaries, and preparing runtime behavior.), import the container (A pre-built UI module that renders drop-in functionality and manages logic, state, and data for a feature.), and render it. Most blocks use a single container. ### 1. Import the initializer Import the initializer for the drop-in. This configures the GraphQL endpoint, loads placeholder text, and registers the drop-in. ```js title="blocks/commerce-login/commerce-login.js" // Import initializer (side-effect import handles all setup) ``` > **Initializers** Initializers run once when imported. They configure the drop-in for use throughout your application. ### 2. Import the container Import the container you need and the render provider (The render function exported by a drop-in package that mounts containers into a storefront block.). Import maps in `head.html` resolve paths to the optimized code. ```js title="blocks/commerce-login/commerce-login.js" // Import the container // Import the provider ``` ### 3. Render the container Render the container in your block decorate function (The JavaScript module that runs for a block after the page loads. It imports the initializer, then calls provider.render() to mount the drop-in UI into the block region of the page.). Pass configuration options to customize behavior. ```js title="blocks/commerce-login/commerce-login.js" export default async function decorate(block) { await authRenderer.render(SignIn, { routeForgotPassword: () => rootLink('/customer/forgot-password'), routeRedirectOnSignIn: () => rootLink('/customer/account'), })(block); } ``` **Complete example:** ```js title="blocks/commerce-login/commerce-login.js" export default async function decorate(block) { await authRenderer.render(SignIn, { routeForgotPassword: () => rootLink('/customer/forgot-password'), routeRedirectOnSignIn: () => rootLink('/customer/account'), })(block); } ``` > **Container documentation** See the Containers documentation for each drop-in for available configuration options. ## Drop-in specific guides Each drop-in has its own Quick Start page with package names, versions, and drop-in-specific requirements: - [Cart](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/quick-start/) - [Checkout](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/quick-start/) - [Order](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/quick-start/) - [Payment Services](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/payment-services/installation/) - [Personalization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/personalization/quick-start/) - [Product Details](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/quick-start/) - [Product Discovery](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-discovery/quick-start/) - [Recommendations](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/recommendations/quick-start/) - [User Account](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/user-account/quick-start/) - [User Auth](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/user-auth/quick-start/) - [Wishlist](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/wishlist/quick-start/) ## Advanced patterns These patterns show how to handle more complex scenarios in your blocks. ### Multiple containers in one block Most blocks use a single container, but complex blocks can render multiple containers together. Use `Promise.all()` to render them in parallel for better performance. The Cart block demonstrates this pattern: ```js title="blocks/commerce-cart/commerce-cart.js" export default async function decorate(block) { // Create layout structure const fragment = document.createRange().createContextualFragment(` `); const $list = fragment.querySelector('.cart__list'); const $summary = fragment.querySelector('.cart__order-summary'); block.appendChild(fragment); // Helper to create product links const createProductLink = (product) => getProductLink(product.url.urlKey, product.topLevelSku); // Render multiple containers in parallel await Promise.all([ provider.render(CartSummaryList, { routeProduct: createProductLink, enableRemoveItem: true, })($list), provider.render(OrderSummary, { routeCheckout: () => rootLink('/checkout'), })($summary), ]); } ``` ### Nesting containers with slots Render containers inside other container slots for advanced composition. Use conditional logic to control which slots render: ```js title="blocks/commerce-cart/commerce-cart.js" export default async function decorate(block) { const { 'enable-estimate-shipping': enableEstimateShipping = 'false' } = readBlockConfig(block); const createProductLink = (product) => getProductLink(product.url.urlKey, product.topLevelSku); await provider.render(OrderSummary, { routeProduct: createProductLink, routeCheckout: () => rootLink('/checkout'), slots: { EstimateShipping: async (ctx) => { if (enableEstimateShipping === 'true') { const wrapper = document.createElement('div'); await provider.render(EstimateShipping, {})(wrapper); ctx.replaceWith(wrapper); } }, Coupons: (ctx) => { const coupons = document.createElement('div'); provider.render(Coupons)(coupons); ctx.appendChild(coupons); }, }, })(block); } ``` ### Combining multiple drop-ins Use multiple drop-ins in a single block when functionality overlaps. The Cart block combines Cart and Wishlist: ```js title="blocks/commerce-cart/commerce-cart.js" // Import event bus // Import initializers for both drop-ins // Import from both drop-ins export default async function decorate(block) { // Create notification area const fragment = document.createRange().createContextualFragment(` `); const $notification = fragment.querySelector('.cart__notification'); block.appendChild(fragment); // Wishlist route const routeToWishlist = '/wishlist'; // Helper to create product links const createProductLink = (product) => getProductLink(product.url.urlKey, product.topLevelSku); // Render cart with wishlist functionality in slots await provider.render(CartSummaryList, { routeProduct: createProductLink, slots: { Footer: (ctx) => { // Add wishlist toggle to each cart item const $wishlistToggle = document.createElement('div'); $wishlistToggle.classList.add('cart__action--wishlist-toggle'); wishlistRender.render(WishlistToggle, { product: ctx.item, removeProdFromCart: Cart.updateProductsFromCart, })($wishlistToggle); ctx.appendChild($wishlistToggle); }, }, })(block); // Listen for wishlist events events.on('wishlist/alert', ({ action, item }) => { wishlistRender.render(WishlistAlert, { action, item, routeToWishlist, })($notification); setTimeout(() => { $notification.innerHTML = ''; }, 5000); }); } ``` ### Using API functions without containers Call API functions directly for programmatic control without rendering UI: ```js title="blocks/custom-block/custom-block.js" export default async function decorate(block) { // Get cached cart data synchronously const cachedCart = Cart.getCartDataFromCache(); console.log('Cached cart:', cachedCart); // Fetch fresh cart data const freshCart = await Cart.getCartData(); console.log('Fresh cart:', freshCart); // Add products programmatically const button = block.querySelector('.add-to-cart-button'); if (button) { button.addEventListener('click', async () => { try { await Cart.addProductsToCart([ { sku: 'ABC123', quantity: 1 } ]); console.log('Product added successfully'); } catch (error) { console.error('Failed to add product:', error); } }); } // Update cart totals in custom UI events.on('cart/data', (cartData) => { const totalElement = block.querySelector('.cart-total'); if (totalElement && cartData?.prices?.grandTotal) { totalElement.textContent = cartData.prices.grandTotal.value; } }, { eager: true }); } ``` ### Error handling Handle errors gracefully with try/catch blocks and user notifications: ```js title="blocks/commerce-mini-cart/commerce-mini-cart.js" export default async function decorate(block) { const placeholders = await fetchPlaceholders(); let currentModal = null; // Custom message display function const showMessage = (message) => { const messageEl = block.querySelector('.mini-cart__message'); if (messageEl) { messageEl.textContent = message; messageEl.classList.add('visible'); setTimeout(() => messageEl.classList.remove('visible'), 3000); } }; async function handleEditButtonClick(cartItem) { try { // Attempt to load and show mini PDP const miniPDPContent = await createMiniPDP(cartItem); currentModal = await createModal([miniPDPContent]); if (currentModal.block) { currentModal.block.setAttribute('id', 'mini-pdp-modal'); } currentModal.showModal(); } catch (error) { console.error('Error opening mini PDP modal:', error); // Show error message using mini-cart's message system showMessage(placeholders?.Global?.ProductLoadError || 'Failed to load product'); } } // ... rest of block implementation } ``` > **Always provide feedback** Always provide feedback to users when operations fail. The example shows how to define a custom message function, but you can also use the `InLineAlert` component from `@dropins/tools/components.js` for consistent error messaging. ## What the boilerplate provides The boilerplate includes everything you need: - Drop-in packages installed in `package.json`. - Optimized code in `scripts/__dropins__/`. - Import maps in `head.html`. - Initializers in `scripts/initializers/` for automatic setup. - Example blocks demonstrating usage. ## How it works The following diagram shows how drop-ins integrate into your boilerplate project: ![Drop-in Setup Flow](https://experienceleague.adobe.com/developer/commerce/storefront/images/pdp/pdp-installation.svg) ## Additional concepts Additional terms you'll encounter as you work with drop-ins. **Drop-in** A self-contained Commerce component (Cart, Checkout, Product Details) that includes containers, API functions, and events. **Import maps** (in `head.html`) Configuration that maps clean import paths (for example, `@dropins/storefront-cart`) to optimized code in `scripts/__dropins__/`. The boilerplate includes these pre-configured. **Event bus** (`@dropins/tools/event-bus.js`) Pub/sub system for drop-in communication. Drop-ins emit events when state changes (for example, `cart/data`, `checkout/updated`). Listen to events to update custom UI or trigger logic. **API functions** Programmatic interfaces to control drop-in behavior without rendering UI. Fetch data, trigger actions, and read cached state (for example, `Cart.addProductsToCart()`, `Cart.getCartData()`). **Slots** Extension points in containers where you can inject custom content or replace default behavior. Used for deep customization beyond configuration options. ## Summary The boilerplate makes using drop-ins straightforward: import an initializer for automatic setup, import the containers you need, render them with configuration options, and optionally listen to events for custom behavior. No manual package installation or complex configuration required. --- # Slots Using slots (An extension point inside a drop-in where custom UI or behavior can be added, replaced, or removed.) provides the deepest level of customization for drop-in components. A slot provides a place in a drop-in container (A pre-built UI module that renders drop-in functionality and manages logic, state, and data for a feature.) to add your own UI components and functions. This architecture makes it easy to change the default look, layout, and behavior. Let's learn how slots work. ## Big Picture ![What is a slot?](https://experienceleague.adobe.com/developer/commerce/storefront/images/slots/what-is-a-slot.svg) *What is a slot?* The following functions are available to all slots: 1. `prependSibling`: Prepends a new HTML element before the content of the slot. 1. `prependChild`: Prepends a new HTML element to the content of the slot. 1. `replaceWith`: Replaces the content of the slot with a new HTML element. 1. `appendChild`: Appends a new HTML element to the content of the slot. 1. `appendSibling`: Appends a new HTML element after the content of the slot. 1. `remove`: Removes the slot from the DOM. 1. `getSlotElement`: Gets a slot element. 1. `onChange`: Listens to changes in the context of the slot. 1. `dictionary`: Provides a JSON Object for the current locale. If the locale changes, the `dictionary` values change to reflect the values for the selected language. ## Best practice for dynamic slot content **Do not use context methods inside other context methods.** Context methods include `appendChild()`, `prependChild()`, `replaceWith()`, `appendSibling()`, `prependSibling()`, `remove()`, `getSlotElement()`, and `onChange()`. Instead, create and append wrapper elements on mount, then update their content inside callbacks using standard DOM methods like `innerHTML`: ```js slots: { MySlot: (ctx) => { const wrapper = document.createElement('div'); // Use context method on mount (outside other context methods) ctx.appendChild(wrapper); // Update content inside onChange using standard DOM methods ctx.onChange((next) => { if (next.data.condition) { wrapper.innerHTML = 'Content A'; } else { wrapper.innerHTML = 'Content B'; } }); } } ``` ```js // ❌ Incorrect: Calling context method inside context method ctx.onChange((next) => { const element = document.createElement('div'); ctx.appendChild(element); // Incorrect - context method inside context method }); ``` ## Related resources - [Extending drop-in components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/extending/) - Advanced customization techniques - [Cart drop-in](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/) - Cart drop-in documentation - [Recommendations drop-in](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/recommendations/) - Recommendations drop-in documentation --- # Styling Drop-In Components export const brandColors = [ { "name": "--color-brand-300", "value": "#6d6d6d", "resolvedColor": "#6d6d6d" }, { "name": "--color-brand-500", "value": "#454545", "resolvedColor": "#454545" }, { "name": "--color-brand-600", "value": "#383838", "resolvedColor": "#383838" }, { "name": "--color-brand-700", "value": "#2b2b2b", "resolvedColor": "#2b2b2b" } ]; export const neutralColors = [ { "name": "--color-neutral-50", "value": "#fff", "resolvedColor": "#fff" }, { "name": "--color-neutral-100", "value": "#fafafa", "resolvedColor": "#fafafa" }, { "name": "--color-neutral-200", "value": "#f5f5f5", "resolvedColor": "#f5f5f5" }, { "name": "--color-neutral-300", "value": "#e8e8e8", "resolvedColor": "#e8e8e8" }, { "name": "--color-neutral-400", "value": "#d6d6d6", "resolvedColor": "#d6d6d6" }, { "name": "--color-neutral-500", "value": "#b8b8b8", "resolvedColor": "#b8b8b8" }, { "name": "--color-neutral-600", "value": "#8f8f8f", "resolvedColor": "#8f8f8f" }, { "name": "--color-neutral-700", "value": "#666", "resolvedColor": "#666" }, { "name": "--color-neutral-800", "value": "#3d3d3d", "resolvedColor": "#3d3d3d" }, { "name": "--color-neutral-900", "value": "#292929", "resolvedColor": "#292929" } ]; export const semanticColors = [ { "name": "--color-positive-200", "value": "#eff5ef", "resolvedColor": "#eff5ef" }, { "name": "--color-positive-500", "value": "#7fb078", "resolvedColor": "#7fb078" }, { "name": "--color-positive-800", "value": "#53824c", "resolvedColor": "#53824c" }, { "name": "--color-informational-200", "value": "#eeeffb", "resolvedColor": "#eeeffb" }, { "name": "--color-informational-500", "value": "#6978d9", "resolvedColor": "#6978d9" }, { "name": "--color-informational-800", "value": "#5d6dd6", "resolvedColor": "#5d6dd6" }, { "name": "--color-warning-200", "value": "#fdf3e9", "resolvedColor": "#fdf3e9" }, { "name": "--color-warning-500", "value": "#e79f5c", "resolvedColor": "#e79f5c" }, { "name": "--color-warning-800", "value": "#cc7a2e", "resolvedColor": "#cc7a2e" }, { "name": "--color-alert-200", "value": "#ffebeb", "resolvedColor": "#ffebeb" }, { "name": "--color-alert-500", "value": "#db7070", "resolvedColor": "#db7070" }, { "name": "--color-alert-800", "value": "#c35050", "resolvedColor": "#c35050" } ]; export const buttonColors = [ { "name": "--color-button-active", "value": "var(--color-brand-700)", "resolvedColor": "#2b2b2b" }, { "name": "--color-button-focus", "value": "var(--color-neutral-400)", "resolvedColor": "#d6d6d6" }, { "name": "--color-button-hover", "value": "var(--color-brand-600)", "resolvedColor": "#383838" }, { "name": "--color-action-button-active", "value": "var(--color-neutral-50)", "resolvedColor": "#fff" }, { "name": "--color-action-button-hover", "value": "var(--color-neutral-300)", "resolvedColor": "#e8e8e8" } ]; export const opacityColors = [ { "name": "--color-opacity-16", "value": "rgb(255 255 255 / 16%)", "resolvedColor": "rgb(255 255 255 / 16%)" }, { "name": "--color-opacity-24", "value": "rgb(255 255 255 / 24%)", "resolvedColor": "rgb(255 255 255 / 24%)" } ]; Customize drop-in components using the design token system and CSS classes from the boilerplate. This guide covers the universal styling approach used across all drop-ins. ## Drop-in-specific styles Each drop-in has a dedicated styles page with practical customization examples. See the individual drop-in documentation: - [Cart styles](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/styles/) - [Checkout styles](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/styles/) - [Order styles](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/styles/) - [Payment Services styles](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/payment-services/styles/) - [Personalization styles](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/personalization/styles/) - [Product Details styles](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/styles/) - [Product Discovery styles](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-discovery/styles/) - [Recommendations styles](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/recommendations/styles/) - [User Account styles](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/user-account/styles/) - [User Auth styles](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/user-auth/styles/) - [Wishlist styles](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/wishlist/styles/) ## Where to add custom styles Add your custom CSS in the appropriate location based on the scope of your changes: ### Global styles and design token overrides - Edit https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/styles/styles.css to override design tokens or add site-wide styles - These styles load immediately and affect all drop-ins ### Block-specific styles - Add styles to `blocks/{block-name}/{block-name}.css` - For example: https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/product-details/product-details.css - These styles load only when the block is used - See all https://github.com/hlxsites/aem-boilerplate-commerce/tree/main/blocks in the boilerplate ### Deferred global styles - Add to https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/styles/lazy-styles.css for non-critical styles that can load after page render - Improves initial page load performance ## Design tokens The drop-in components use CSS custom properties (design tokens) defined in the `styles/styles.css` file from the boilerplate. These tokens ensure consistent styling across all drop-ins and make global theme changes easy to apply. ### Override design tokens Change the appearance of all drop-ins by overriding design tokens in your custom CSS: ```css :root { /* Brand colors */ --color-brand-500: #0066cc; --color-brand-600: #0052a3; --color-brand-700: #003d7a; /* Typography */ --type-base-font-family: 'Inter', system-ui, sans-serif; /* Spacing */ --spacing-small: 12px; --spacing-medium: 20px; } ``` ## Finding CSS classes Use the browser DevTools to find specific class names for your customizations: Inspect the UI of any drop-in component using your browser developer tools: ![Browser developer tools inspecting a PDP drop-in element with the Styles panel listing BEM-style CSS classes to override](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/findstyles.webp) *Find CSS classes to override.* 1. **Inspect the element** you want to customize (right-click the element and select "Inspect" from the menu). 1. **Identify the CSS class(es)** for the element. We use https://getbem.com/naming/, which makes components and their elements easy to identify. For example, `.pdp-product__title` indicates the title element of the product component. 1. **Copy the CSS class** to your CSS file to override existing rules or add new rules. If the class uses design tokens (like `var(--spacing-small)`), override the token value instead of removing it. This keeps your customizations consistent with the design system. ## Examples These examples show common component customization patterns: ```css /* Adjust layout for a specific component */ .pdp-product__options { grid-column: 1 / span 3; } .pdp-product__quantity { grid-column: 1 / span 3; } /* Modify spacing using design tokens */ .pdp-product__buttons { gap: var(--spacing-small); } ``` ## Responsive breakpoints Drop-in components use these breakpoints: - **Mobile**: up to 767px - **Tablet**: 768px - 1023px - **Desktop**: 1024px and up Use a mobile-first approach when you add responsive styles: ```css /* Mobile styles (default) */ .my-component { padding: var(--spacing-small); } /* Desktop styles */ @media (min-width: 1024px) { .my-component { padding: var(--spacing-big); } } ``` ## Design tokens reference The following sections show all available design tokens with their default values for reference when customizing your storefront. ### Colors Color tokens define the palette for branding, UI elements, semantic states, and interactive components. #### Brand Colors #### Neutral Colors #### Semantic Colors #### Button Colors #### Opacity ### Spacing Spacing tokens provide consistent padding, margins, and gaps across all components. ```css --spacing-xxsmall: 4px --spacing-xsmall: 8px --spacing-small: 16px --spacing-medium: 24px --spacing-big: 32px --spacing-xbig: 40px --spacing-xxbig: 48px --spacing-large: 64px --spacing-xlarge: 72px --spacing-xxlarge: 96px --spacing-huge: 120px --spacing-xhuge: 144px --spacing-xxhuge: 192px ``` ### Typography Typography tokens define font families, sizes, weights, line heights, and letter spacing for text elements. #### Font Families ```css --type-base-font-family: adobe-clean, roboto, roboto-fallback, system-ui, sans-serif --type-fixed-font-family: adobe-clean, "Roboto Mono", menlo, consolas, "Liberation Mono", monospace, system-ui, sans-serif ``` #### Type Scales ```css --type-display-1-font: normal normal 300 6rem/7.2rem var(--type-base-font-family) --type-display-1-letter-spacing: 0.04em --type-display-2-font: normal normal 300 4.8rem/5.6rem var(--type-base-font-family) --type-display-2-letter-spacing: 0.04em --type-display-3-font: normal normal 300 3.4rem/4rem var(--type-base-font-family) --type-display-3-letter-spacing: 0.04em --type-headline-1-font: normal normal 400 2.4rem/3.2rem var(--type-base-font-family) --type-headline-1-letter-spacing: 0.04em --type-headline-2-default-font: normal normal 300 2rem/2.4rem var(--type-base-font-family) --type-headline-2-default-letter-spacing: 0.04em --type-headline-2-strong-font: normal normal 700 2rem/2.4rem var(--type-base-font-family) --type-headline-2-strong-letter-spacing: 0.04em --type-body-1-default-font: normal normal 300 1.6rem/2.4rem var(--type-base-font-family) --type-body-1-default-letter-spacing: 0.04em --type-body-1-strong-font: normal normal 700 1.6rem/2.4rem var(--type-base-font-family) --type-body-1-strong-letter-spacing: 0.04em --type-body-1-emphasized-font: normal normal 700 1.6rem/2.4rem var(--type-base-font-family) --type-body-1-emphasized-letter-spacing: 0.04em --type-body-2-default-font: normal normal 300 1.4rem/2rem var(--type-base-font-family) --type-body-2-default-letter-spacing: 0.04em --type-body-2-strong-font: normal normal 700 1.4rem/2rem var(--type-base-font-family) --type-body-2-strong-letter-spacing: 0.04em --type-body-2-emphasized-font: normal normal 700 1.4rem/2rem var(--type-base-font-family) --type-body-2-emphasized-letter-spacing: 0.04em --type-button-1-font: normal normal 400 2rem/2.6rem var(--type-base-font-family) --type-button-1-letter-spacing: 0.08em --type-button-2-font: normal normal 400 1.6rem/2.4rem var(--type-base-font-family) --type-button-2-letter-spacing: 0.08em --type-details-caption-1-font: normal normal 400 1.2rem/1.6rem var(--type-base-font-family) --type-details-caption-1-letter-spacing: 0.08em --type-details-caption-2-font: normal normal 300 1.2rem/1.6rem var(--type-base-font-family) --type-details-caption-2-letter-spacing: 0.08em --type-details-overline-font: normal normal 400 1.2rem/2rem var(--type-base-font-family) --type-details-overline-letter-spacing: 0.16em ``` ### Shapes & Borders Shape tokens control the visual appearance of borders, shadows, and icon strokes. #### Border Radius ```css --shape-border-radius-1: 3px --shape-border-radius-2: 8px --shape-border-radius-3: 24px ``` #### Border Width ```css --shape-border-width-1: 1px --shape-border-width-2: 1.5px --shape-border-width-3: 2px --shape-border-width-4: 4px ``` #### Shadows ```css --shape-shadow-1: 0 0 16px 0 rgb(0 0 0 / 16%) --shape-shadow-2: 0 2px 16px 0 rgb(0 0 0 / 16%) --shape-shadow-3: 0 2px 3px 0 rgb(0 0 0 / 16%) ``` #### Icon Stroke ```css --shape-icon-stroke-1: 1px --shape-icon-stroke-2: 1.5px --shape-icon-stroke-3: 2px --shape-icon-stroke-4: 4px ``` ### Grid System ```css --grid-1-columns: 4 --grid-1-margins: 0 --grid-1-gutters: 16px --grid-2-columns: 12 --grid-2-margins: 0 --grid-2-gutters: 16px --grid-3-columns: 12 --grid-3-margins: 0 --grid-3-gutters: 24px --grid-4-columns: 12 --grid-4-margins: 0 --grid-4-gutters: 24px --grid-5-columns: 12 --grid-5-margins: 0 --grid-5-gutters: 24px ``` ## Advanced customization ### Inspect CSS variables in use Use the browser DevTools to discover which CSS variables (design tokens) a component is using: 1. **Right-click on any element** and select "Inspect" 2. **In the Styles panel**, look for properties using `var()` syntax 3. **Click the variable name** (e.g., `var(--spacing-medium)`) to see its computed value 4. **In the Computed tab**, filter by "spacing", "color", etc. to see all applied tokens #### Example: Inspecting a button might show ```css padding: var(--spacing-small); /* Resolves to: 16px */ background: var(--color-brand-500); /* Resolves to: #454545 */ ``` ### How tokens flow through components Design tokens control the visual appearance of components by mapping to specific CSS properties. Understanding this flow helps you predict what changes when you override a token. #### The flow: 1. A design token is defined in the boilerplate: `--spacing-small: 16px` 2. A component uses it in a CSS property: `gap: var(--spacing-small);` 3. The browser resolves it to the actual value: `gap: 16px` 4. The visual effect appears: 16px of spacing between grid items #### Real-world example: Cart grid spacing The grid component for the Cart drop-in uses `--spacing-small` to control the gap between product images: ```css .cart-cart-summary-grid__content { display: grid; gap: var(--spacing-small); /* Controls spacing between rows and columns */ grid-template-columns: repeat(6, 1fr); } ``` ``` ┌─────────┐ ←──16px──→ ┌─────────┐ ←──16px──→ ┌─────────┐ │ Product │ │ Product │ │ Product │ │ Image │ │ Image │ │ Image │ └─────────┘ └─────────┘ └─────────┘ ↓ ↓ ↓ 16px 16px 16px ↓ ↓ ↓ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Product │ │ Product │ │ Product │ │ Image │ │ Image │ │ Image │ └─────────┘ └─────────┘ └─────────┘ ↓ ↓ ↓ 16px 16px 16px ↓ ↓ ↓ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Product │ │ Product │ │ Product │ │ Image │ │ Image │ │ Image │ └─────────┘ └─────────┘ └─────────┘ gap: var(--spacing-small) = 16px ``` *The gap property controls spacing between grid items in both directions.* When you override `--spacing-small`, you directly change how tightly or loosely the product images are packed together. A smaller value (8px) creates a denser grid, while a larger value (24px) creates more breathing room. #### Common token-to-property mappings - `--spacing-*` tokens → `gap`, `padding`, `margin` properties → Controls whitespace - `--color-*` tokens → `background`, `color`, `border-color` properties → Controls visual identity - `--shape-border-radius-*` tokens → `border-radius` property → Controls corner roundness - `--type-*` tokens → `font`, `font-size`, `line-height` properties → Controls typography ### Scoped token overrides Override design tokens for specific components only, rather than globally: ```css /* Global override - affects all drop-ins */ :root { --spacing-small: 12px; } /* Scoped override - only affects Cart drop-in */ .cart-cart-summary-grid { --spacing-small: 16px; /* This component now uses 16px, others still use the global value */ } /* Scoped to a specific element state */ .dropin-button:hover { --color-brand-500: #0066cc; background: var(--color-brand-500); /* Uses the hover value */ } ``` --- # CartSummaryGrid container The `CartSummaryGrid` container manages and displays the contents of the shopping cart in a grid layout. Its state is managed by the `CartModel` interface, which contains the cart's initial data and is passed down to any child components. ![CartSummaryGrid container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/cart-summary-grid-small.png) *CartSummaryGrid container* ## Configurations The `CartSummaryGrid` container provides the following configuration options: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `children` | `CartModel` | Yes | Child elements to be rendered inside the container. | | `initialData` | `string` | Yes | Initial cart data to preload the component. Defaults to null. | | `routeProduct` | `function` | No | Callback function that returns a product. | | `routeEmptyCartCTA` | `function` | No | Callback function that returns an empty cart. | The `CartModel` object has the following shape: ```ts export interface CartModel { id: string; totalQuantity: number; errors?: ItemError[]; items: Item[]; miniCartMaxItems: Item[]; total: { includingTax: Price; excludingTax: Price; }; discount?: Price; subtotal: { excludingTax: Price; includingTax: Price; includingDiscountOnly: Price; }; appliedTaxes: TotalPriceModifier[]; totalTax?: Price; appliedDiscounts: TotalPriceModifier[]; shipping?: Price; isVirtual?: boolean; addresses: { shipping?: { countryCode: string; zipCode?: string; regionCode?: string; }[]; }; isGuestCart?: boolean; hasOutOfStockItems?: boolean; hasFullyOutOfStockItems?: boolean; appliedCoupons?: Coupon[]; } interface TotalPriceModifier { amount: Price; label: string; coupon?: Coupon; } interface FixedProductTax { amount: Price; label: string; } export interface Item { taxedPrice: Price; rowTotal: Price; rowTotalIncludingTax: Price; itemType: string; uid: string; url: ItemURL; quantity: number; sku: string; name: string; image: ItemImage; links?: ItemLinks; price: Price; total: Price; discountedTotal?: Price; discount?: Price; regularPrice: Price; discounted: boolean; bundleOptions?: { [key: string]: any }; selectedOptions?: { [key: string]: any }; customizableOptions?: { [key: string]: any }; message?: string; recipient?: string; recipientEmail?: string; sender?: string; senderEmail?: string; lowInventory?: boolean; insufficientQuantity?: boolean; onlyXLeftInStock?: number | null; outOfStock?: boolean; notAvailableMessage?: string; stockLevel?: String; discountPercentage?: number; savingsAmount?: Price; productAttributes?: Attribute[]; fixedProductTaxes?: FixedProductTax[]; } interface ItemError { id: string; text: string; } interface ItemImage { src: string; alt: string; } export interface Price { value: number; currency: string; } interface ItemURL { urlKey: string; categories: string[]; } interface ItemLinks { count: number; result: string; } interface AttributeOption { value: string; label: string; } interface Attribute { code: string; value?: string; selected_options?: AttributeOption[]; } interface Coupon { code: string; } ``` ## Example configuration The following example demonstrates how to render the `CartSummaryGrid` container with the `routeProduct` and `routeEmptyCartCTA` callbacks: ```js provider.render(CartSummaryGrid, { routeProduct: (item) => { return `${item.url.categories.join('/')}/${item.url.urlKey}`; }, routeEmptyCartCTA: () => '#empty-cart', })(document.getElementById('@dropins/CartSummaryGrid')); ``` --- # CartSummaryList container The `CartSummaryList` container displays a summary of the items in the shopping cart by rendering a list of `CartItem` components. Each `CartItem` represents an individual item in the cart. ![CartSummaryList container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/cart-summary-list.png) *CartSummaryList container* ## Configurations The `CartSummaryList` container provides the following configuration options: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `children` | `CartModel` | Yes | Child elements to be rendered inside the container. | | `routeProduct` | `function` | No | Callback function that returns a product. | | `routeEmptyCartCTA` | `function` | No | Callback function that returns an empty cart. | | `initialData` | `string` | Yes | Initial cart data to preload the component. Defaults to null. | | `hideHeading` | `boolean` | No | Whether to hide the heading of the cart. | | `hideFooter` | `boolean` | No | Whether to hide the footer of the cart. | | `routeCart` | `function` | No | Callback function that navigates to the cart. | | `onItemUpdate` | `function` | No | Callback function that updates the item. | | `onItemRemove` | `function` | No | Callback function that removes the item. | | `maxItems` | `number` | No | Maximum number of items to display. | | `slots` | `function` | No | Allows passing a container or custom component. | | `attributesToHide` | `string[]` | No | Attributes to hide. | | `enableRemoveItem` | `boolean` | No | Enable remove item. | | `enableUpdateItemQuantity` | `boolean \| { removeOnZero?: boolean }` | No | Enables the quantity stepper. Pass `{ removeOnZero: true }` to also remove the item when the quantity reaches `0`; defaults to `false`. | | `onItemsErrorsChange` | `function` | No | Callback function that changes the items errors. | | `accordion` | `boolean` | No | Toggle accordion view. | | `variant` | `primary \| secondary` | No | Cart variant. | | `isLoading` | `boolean` | No | Toggle loading state. | | `showMaxItems` | `boolean` | No | Toggle show max items. | | `showDiscount` | `boolean` | No | Toggle show discount. | | `showSavings` | `boolean` | No | Toggle show savings. | | `quantityType` | `stepper \| dropdown` | No | Display quantity changes as a stepper or in a dropdown menu. | | `dropdownOptions` | `string[]` | No | An array of items to display in a dropdown menu. | | `undo` | `boolean` | No | Enables the undo banner to restore recently removed items to the cart. | | `includeOutOfStockItems` | `boolean` | No | Display out-of-stock and insufficient-quantity items in the main cart item list alongside in-stock items. Default: `false`. | | `confirmBeforeDelete` | `boolean` | No | Enables the confirmation banner when an item is removed from the cart. When set to true, clicking the remove button shows an inline confirmation banner instead of immediately deleting the item. | | `headingLevel` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | No | Sets the heading level for the cart heading. | | `itemTitleHeadingLevel` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | No | Sets the heading level for each cart item title. | The `CartModel` object has the following shape: ```ts export interface CartModel { id: string; totalQuantity: number; errors?: ItemError[]; items: Item[]; miniCartMaxItems: Item[]; total: { includingTax: Price; excludingTax: Price; }; discount?: Price; subtotal: { excludingTax: Price; includingTax: Price; includingDiscountOnly: Price; }; appliedTaxes: TotalPriceModifier[]; totalTax?: Price; appliedDiscounts: TotalPriceModifier[]; shipping?: Price; isVirtual?: boolean; addresses: { shipping?: { countryCode: string; zipCode?: string; regionCode?: string; }[]; }; isGuestCart?: boolean; hasOutOfStockItems?: boolean; hasFullyOutOfStockItems?: boolean; appliedCoupons?: Coupon[]; } interface TotalPriceModifier { amount: Price; label: string; coupon?: Coupon; } interface FixedProductTax { amount: Price; label: string; } export interface Item { taxedPrice: Price; rowTotal: Price; rowTotalIncludingTax: Price; itemType: string; uid: string; url: ItemURL; quantity: number; sku: string; name: string; image: ItemImage; links?: ItemLinks; price: Price; total: Price; discountedTotal?: Price; discount?: Price; regularPrice: Price; discounted: boolean; bundleOptions?: { [key: string]: any }; selectedOptions?: { [key: string]: any }; customizableOptions?: { [key: string]: any }; message?: string; recipient?: string; recipientEmail?: string; sender?: string; senderEmail?: string; lowInventory?: boolean; insufficientQuantity?: boolean; onlyXLeftInStock?: number | null; outOfStock?: boolean; notAvailableMessage?: string; stockLevel?: String; discountPercentage?: number; savingsAmount?: Price; productAttributes?: Attribute[]; fixedProductTaxes?: FixedProductTax[]; } interface ItemError { id: string; text: string; } interface ItemImage { src: string; alt: string; } export interface Price { value: number; currency: string; } interface ItemURL { urlKey: string; categories: string[]; } interface ItemLinks { count: number; result: string; } interface AttributeOption { value: string; label: string; } interface Attribute { code: string; value?: string; selected_options?: AttributeOption[]; } interface Coupon { code: string; } ``` ## Supported slots The `CartSummaryList` container supports the following slots: * Heading * EmptyCart * Footer * Thumbnail * ProductAttributes * CartSummaryFooter * CartItem * UndoBanner * ConfirmDeleteBanner * ItemTitle * ItemPrice * ItemQuantity * ItemTotal * ItemSku * ItemRemoveAction ## Example configuration The following example demonstrates how to render the `CartSummaryList` container with the `routeProduct` and `routeEmptyCartCTA` callbacks: ```js provider.render(CartSummaryList, { enableRemoveItem: true, enableUpdateItemQuantity: true, showDiscount: true, // confirmBeforeDelete: true, // accordion: true, // includeOutOfStockItems: true, // showMaxItems: false, // maxItems: 6, // routeCart: () => '#cart', // showSavings: true, // quantityType: 'dropdown', // dropdownOptions: [ // { value: '1', text: '1' }, // { value: '2', text: '2' }, // { value: '3', text: '3' }, // ], routeProduct: (item) => { return `${item.url.categories.join('/')}/${item.url.urlKey}`; }, routeEmptyCartCTA: () => '#empty-cart', slots: { Footer: (ctx) => { // Runs on mount const wrapper = document.createElement('div'); ctx.appendChild(wrapper); // Append Product Promotions on every update ctx.onChange((next) => { wrapper.innerHTML = ''; next.item?.discount?.label?.forEach((label) => { const discount = document.createElement('div'); discount.style.color = '#3d3d3d'; discount.innerText = label; wrapper.appendChild(discount); }); }); }, }, })($cartSummaryList); --- # CartSummaryTable container The `CartSummaryTable` container displays a summary of the items in the shopping cart by rendering a table of cart items. Each row represents an individual item in the cart, with columns for the item details, price, quantity, subtotal, and actions. ![CartSummaryGrid container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/cart-summary-table.png) *CartSummaryTable container* ## Features The `CartSummaryTable` container includes the following features: - Automatic loading state with skeleton UI - Support for out-of-stock items with visual indicators - Quantity update functionality with error handling - Item removal capability - Tax price display (including/excluding) - Product image display with lazy loading - Configurable product routing - Customizable slots for all major components - Support for product configurations - Warning and alert message display - Discount and savings display ## Configurations The `CartSummaryTable` container provides the following configuration options: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `initialData` | `CartModel \| null` | No | Initial data for the cart. Defaults to null. | | `className` | `string` | No | Optional CSS class name for custom styling. | | `routeProduct` | `function` | No | Function for getting the product page route. | | `allowQuantityUpdates` | `boolean` | No | Whether to allow quantity updates. Defaults to true. | | `allowRemoveItems` | `boolean` | No | Whether to allow removing items. Defaults to true. | | `onQuantityUpdate` | `function` | No | Callback function when quantity is updated. | | `onItemRemove` | `function` | No | Callback function when an item is removed. | | `routeEmptyCartCTA` | `function` | No | Function to generate the URL for the empty cart call-to-action button. | | `undo` | `boolean` | No | Enables the undo banner to restore recently removed items to the cart. | The `CartModel` object has the following shape: ```ts export interface CartModel { id: string; totalQuantity: number; errors?: ItemError[]; items: Item[]; miniCartMaxItems: Item[]; total: { includingTax: Price; excludingTax: Price; }; discount?: Price; subtotal: { excludingTax: Price; includingTax: Price; includingDiscountOnly: Price; }; appliedTaxes: TotalPriceModifier[]; totalTax?: Price; appliedDiscounts: TotalPriceModifier[]; shipping?: Price; isVirtual?: boolean; addresses: { shipping?: { countryCode: string; zipCode?: string; regionCode?: string; }[]; }; isGuestCart?: boolean; hasOutOfStockItems?: boolean; hasFullyOutOfStockItems?: boolean; appliedCoupons?: Coupon[]; } interface TotalPriceModifier { amount: Price; label: string; coupon?: Coupon; } interface FixedProductTax { amount: Price; label: string; } export interface Item { taxedPrice: Price; rowTotal: Price; rowTotalIncludingTax: Price; itemType: string; uid: string; url: ItemURL; quantity: number; sku: string; name: string; image: ItemImage; links?: ItemLinks; price: Price; total: Price; discountedTotal?: Price; discount?: Price; regularPrice: Price; discounted: boolean; bundleOptions?: { [key: string]: any }; selectedOptions?: { [key: string]: any }; customizableOptions?: { [key: string]: any }; message?: string; recipient?: string; recipientEmail?: string; sender?: string; senderEmail?: string; lowInventory?: boolean; insufficientQuantity?: boolean; onlyXLeftInStock?: number | null; outOfStock?: boolean; notAvailableMessage?: string; stockLevel?: String; discountPercentage?: number; savingsAmount?: Price; productAttributes?: Attribute[]; fixedProductTaxes?: FixedProductTax[]; } interface ItemError { id: string; text: string; } interface ItemImage { src: string; alt: string; } export interface Price { value: number; currency: string; } interface ItemURL { urlKey: string; categories: string[]; } interface ItemLinks { count: number; result: string; } interface AttributeOption { value: string; label: string; } interface Attribute { code: string; value?: string; selected_options?: AttributeOption[]; } interface Coupon { code: string; } ``` ## Supported slots The `CartSummaryTable` container supports the following slots for customization: * **Item**: Customize the item cell content * Context: `{ item: CartModel['items'][number] }` * **Price**: Customize the price cell content * Context: `{ item: CartModel['items'][number] }` * **Quantity**: Customize the quantity cell content * Context: `{ item: CartModel['items'][number], isUpdating: boolean, quantityInputValue: number, handleInputChange: (e: Event) => void, itemUpdateErrors: Map }` * **Subtotal**: Customize the subtotal cell content * Context: `{ item: CartModel['items'][number] }` * **Thumbnail**: Customize the thumbnail image on an item * Context: `{ item: CartModel['items'][number], defaultImageProps: ImageProps, index: number }` * **ProductTitle**: Customize the product title on an item * Context: `{ item: CartModel['items'][number] }` * **Sku**: Customize the product SKU on an item * Context: `{ item: CartModel['items'][number] }` * **Configurations**: Customize the product configurations on an item * Context: `{ item: CartModel['items'][number] }` * **ItemAlert**: Customize the product alert on an item * Context: `{ item: CartModel['items'][number] }` * **ItemWarning**: Customize the product warning on an item * Context: `{ item: CartModel['items'][number] }` * **Actions**: Customize the actions on an item * Context: `{ item: CartModel['items'][number], itemsUpdating: Map, setItemUpdating: (uid: string, state: boolean) => void, setItemUpdateError: (uid: string, error: string) => void }` ## Example configuration The following example demonstrates how to render the `CartSummaryTable` container with the some of the configuration options and slots: ```js provider.render(CartSummaryTable, { initialData: cartData, allowQuantityUpdates: true, allowRemoveItems: true, routeProduct: (item) => `/products/${item.urlKey}`, onQuantityUpdate: (item, quantity) => { // Handler after quantity update }, onItemRemove: (item) => { // Handler after item removal }, slots: { Item: (ctx) => { // Custom item cell content }, Price: (ctx) => { // Custom price cell content }, // ... other slot customizations } }); ``` --- # Coupons container The `Coupons` container manages the application of coupons to the shopping cart. It provides a text box for users to enter coupon codes. The container uses the `applyCouponsToCart` function to apply the coupon to the cart. ![Coupons container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/apply-coupon.png) *Coupons container* ## Configurations The `Coupons` container has no public configuration options. Render it without any props inside an `OrderSummary` slot. ## Example configuration The following example demonstrates how to render the `Coupons` container as part of the OrderSummary slot: ```js { provider.render(OrderSummary, { routeCheckout: () => '#checkout', slots: { Coupons: (ctx) => { const coupons = document.createElement('div'); provider.render(Coupons)(coupons); ctx.appendChild(coupons); }, }, showTotalSaved: true, })('.cart__order-summary'), } --- # EmptyCart container The `EmptyCart` container renders a message or component indicating that the cart is empty. It can provide navigation options to continue shopping or explore products. ![EmptyCart container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/empty-cart.png) *EmptyCart container* ## Configurations The `EmptyCart` container provides the following configuration options: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `routeCTA` | `function` | No | Callback function that returns an empty cart. | ## Example configuration The following example demonstrates how to render the `EmptyCart` container: ```js provider.render(EmptyCart, { routeCTA: startShoppingURL ? () => startShoppingURL : undefined, })($emptyCart), ``` `; --- # EstimateShipping container The `EstimateShipping` container renders a form that allows shoppers to estimate shipping costs based on their specified location. The form includes fields for the shopper to enter their country, state, and postal code. ![EstimateShipping container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/estimate-shipping.png) *EstimateShipping container* ## Configurations The `EstimateShipping` container provides the following configuration options: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `showDefaultEstimatedShippingCost` | `boolean` | Yes | Displays the default estimated shipping cost before the shopper enters location details. | ## Example configuration The following example demonstrates how to render the `EstimateShipping` container: ```js EstimateShipping: (ctx) => { const estimateShippingForm = document.createElement('div'); provider.render(EstimateShipping, { showDefaultEstimatedShippingCost: true, })('#estimate-shipping'); --- # GiftCards container The `GiftCards` container manages the application and removal of gift cards to the shopping cart. It provides a text box for users to enter gift card codes. The container uses the `applyGiftCardToCart` and `removeGiftCardFromCart` functions to apply the coupon to the cart. When a gift card code is applied, the corresponding amount is subtracted from the total order value, and the discount is displayed in the cart and order summary. The Adobe Commerce merchant can manage gift card configuration from **Marketing** > **Gift Card Accounts**. ![GiftCards container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/gift-cards.png) *GiftCards container* ## Configurations The `GiftCards` container provides the following configuration options: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `className` | `string` | No | CSS class applied to the container. | ## Example configuration The following example demonstrates how to render the `GiftCards` container as part of the OrderSummary slot: ```js provider.render(OrderSummary, { routeProduct: (product) => rootLink(`/products/${product.url.urlKey}/${product.topLevelSku}`), routeCheckout: checkoutURL ? () => rootLink(checkoutURL) : undefined, slots: { GiftCards: (ctx) => { const giftCards = document.createElement('div'); provider.render(GiftCards)(giftCards); ctx.appendChild(giftCards); }, }, })($summary); ``` --- # GiftOptions container The `GiftOptions` container allows shoppers to personalize their orders by adding gift wrapping and a gift message for each cart item or by applying gift-related options to the entire order. It can be displayed in a product or order view, with each view supporting both editable and non-editable modes, controlled via props. Products can have the following gift options: * Gift wrapping * Gift message Orders can have the following gift options: * Gift receipt * Printed card * Gift wrapping * Gift message The following diagrams illustrate how gift options can be rendered. ![GiftOptions container with all available options](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/gift-options-unset.png) *GiftOptions container with all available options* ![GiftOptions container with options that have been set](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/gift-options-set.png) *GiftOptions container with options that have been set* ## Admin configuration Gift options highly configurable, allowing merchants to manage gift options at both the global and product levels. This flexibility ensures that gift options can be tailored to meet the specific needs of the store and its products. These configurations determine how your frontend code will behave and what options will be available to customers during the shopping experience. ### Global configuration The merchant can manage global gift option configurations from the Admin at **Stores** > Configuration > **Sales** > **Sales** > **Gift Options**. The following options are available: * **Allow Gift Messages on Order Level** * **Allow Gift Messages for Order Items** * **Allow Gift Wrapping on Order Level** * **Allow Gift Wrapping for Order Items** * **Allow Gift Receipt** * **Allow Printed Card** * **Default Price for Printed Card** Global configurations apply to all products unless overridden by product level configurations. ### Product-level configuration Each product can have its own gift option configuration, which takes precedence over the global configurations. To manage product-level configurations, the administrator must ensure that the product is enabled for gift options. This can be done by setting the following options in the product configuration (**Catalog** > **Products** > _Product_ > **Gift Options**): **Allow Gift Message** - If enabled, customers can add a personalized gift message for this product even if gift messages are globally disabled. If disabled, gift messages will not be available for this product, even if globally enabled. **Allow Gift Wrapping** - If enabled, customers can select a gift wrapping for this product even if gift wrapping is globally disabled. If disabled, gift wrapping will not be available for this product, even if globally enabled. If gift wrapping is disabled at least for one product in cart, you cannot apply gift wrapping to the whole order, even if order-level gift wrapping is enabled globally. **Price for Gift Wrapping** - If set, overrides the pricing for all gift-wrapping options for this product. ### Gift wrapping configuration Gift wrapping options can be configured and managed from **Stores** > Configuration > **Gift Wrapping**. Settings include a title, price, and image for each gift wrapping option. ### Tax Display configuration Tax display settings define how taxes for gift options are shown on different pages, such as the cart and order summary. These settings can be configured under **Stores** > Configuration > **Sales** > **Tax**. ## Container configurations The `GiftOptions` container provides the following configuration options: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `dataSource` | `cart \| order` | No | Coupon code input field. | | `view` | `product \| order` | No | Defines which view of the GiftOptions container should be rendered based on the insertion location. | | `isEditable` | `Boolean` | No | Determines whether the GiftOptions container should be rendered in an editable mode. | | `item` | `Object` | No | The item prop is required for initializing the product view. It is used to retrieve available gift options and product-level configurations. | | `initialLoading` | `Boolean` | No | Indicates the initial state of the component, which can be used for UX purposes. | | `readOnlyFormOrderView` | `primary \| secondary` | No | Determines the styling of the GiftOptions container in order view - non-editable mode. | | `handleItemsLoading` | `Function` | No | Used to integrate GiftOptions with the CartSummaryList container provided by cart drop-in. | | `handleItemsError` | `Function` | No | Used to integrate GiftOptions with cart CartSummaryList container provided by cart drop-in. | | `onItemUpdate` | `Function` | No | Used to integrate GiftOptions with cart CartSummaryList container provided by cart drop-in. | | `onGiftOptionsChange` | `Function` | No | Used to build custom GiftOptions container integrations. | For the `dataSource` prop, specify `cart` when the source of truth is the cart page, meaning gift options are applied at the cart level and the container should initialize with the currently selected gift options. Also, specify card on any other page where the cart drop-in fires a `cart/data` event. Specify `order` when the source of truth is a previously-placed order or if the page is controlled by the order drop-in and initialized by an `order/data` event. For the `view` prop, specify `product` when the `GiftOptions` container is rendered at the product level, allowing users to configure gift options for a specific product. Use `order` when the container is rendered at the order level, enabling users to configure gift options for the entire order. Set the `isEditable` prop to `true` when gift options should be editable, such as on the cart page, where users can modify options before placing an order. Use `false` when gift options should be non-editable, such as on the Order Details page, where users view gift options for an already placed order. The `items` prop accepts: * A cart item object (for seamless integration with the cart and checkout pages). * A custom-shaped object with the required fields: ```js export type ProductGiftOptionsConfig = { giftWrappingAvailable: boolean; giftMessageAvailable: boolean; giftWrappingPrice?: Price; giftMessage?: { recipientName?: string; senderName?: string; message?: string; }; productGiftWrapping: GiftWrappingConfigProps[]; }; ``` ## Example configurations The following examples demonstrate how to configure the `GiftOptions` container in different scenarios. ### Product view: editable The following example demonstrates how to render the `GiftOptions` container in an editable mode at the product level: ```js provider.render(CartSummaryList, { hideHeading: hideHeading === 'true', routeProduct: (product) => rootLink(`/products/${product.url.urlKey}/${product.topLevelSku}`), routeEmptyCartCTA: startShoppingURL ? () => rootLink(startShoppingURL) : undefined, maxItems: parseInt(maxItems, 10) || undefined, attributesToHide: hideAttributes .split(',') .map((attr) => attr.trim().toLowerCase()), enableUpdateItemQuantity: enableUpdateItemQuantity === 'true', enableRemoveItem: enableRemoveItem === 'true', slots: { Footer: (ctx) => { const giftOptions = document.createElement('div'); provider.render(GiftOptions, { item: ctx.item, view: 'product', dataSource: 'cart', handleItemsLoading: ctx.handleItemsLoading, handleItemsError: ctx.handleItemsError, onItemUpdate: ctx.onItemUpdate, })(giftOptions); ctx.appendChild(giftOptions); }, }, })($list); ``` ### Product View: non-editable The following example demonstrates how to render the `GiftOptions` container in a non-editable mode at the product level: ```js CartProvider.render(CartSummaryList, { variant: 'secondary', slots: { Heading: (headingCtx) => { const title = 'Your Cart ({count})'; const cartSummaryListHeading = document.createElement('div'); cartSummaryListHeading.classList.add('cart-summary-list__heading'); const cartSummaryListHeadingText = document.createElement('div'); cartSummaryListHeadingText.classList.add( 'cart-summary-list__heading-text', ); cartSummaryListHeadingText.innerText = title.replace( '({count})', headingCtx.count ? `(${headingCtx.count})` : '', ); const editCartLink = document.createElement('a'); editCartLink.classList.add('cart-summary-list__edit'); editCartLink.href = rootLink('/cart'); editCartLink.rel = 'noreferrer'; editCartLink.innerText = 'Edit'; cartSummaryListHeading.appendChild(cartSummaryListHeadingText); cartSummaryListHeading.appendChild(editCartLink); headingCtx.appendChild(cartSummaryListHeading); headingCtx.onChange((nextHeadingCtx) => { cartSummaryListHeadingText.innerText = title.replace( '({count})', nextHeadingCtx.count ? `(${nextHeadingCtx.count})` : '', ); }); }, Footer: (ctx) => { const giftOptions = document.createElement('div'); CartProvider.render(GiftOptions, { item: ctx.item, view: 'product', dataSource: 'cart', isEditable: false, handleItemsLoading: ctx.handleItemsLoading, handleItemsError: ctx.handleItemsError, onItemUpdate: ctx.onItemUpdate, })(giftOptions); ctx.appendChild(giftOptions); }, }, })($cartSummary); ``` ### Order view: editable The following example demonstrates how to render the `GiftOptions` container in an editable mode at the order level: ```js provider.render(GiftOptions, { view: 'order', dataSource: 'cart', })($giftOptions); ``` ### Order view: non-editable The following example demonstrates how to render the `GiftOptions` container in a non-editable mode at the order level: ```js CartProvider.render(GiftOptions, { view: 'order', dataSource: 'cart', isEditable: false, })($giftOptions); ``` ## Custom integrations You can build additional custom integrations for gift option functionality using the GiftOptions container. As an example, we can integrate the GiftOptions container on the Product Detail Page (PDP), allowing customers to select gift options for products before adding them to the cart. The code examples provided below demonstrate the general approach to building custom integrations with the GiftOptions container. --- # Cart Containers The **Cart** drop-in provides pre-built container components for integrating into your storefront. Version: 3.3.0 ## What are Containers? Containers are pre-built UI components that combine functionality, state management, and presentation. They provide a complete solution for specific features and can be customized through props, slots, and CSS. ## Available Containers | Container | Description | | --------- | ----------- | | [CartSummaryGrid](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/containers/cart-summary-grid/) | Learn about the `CartSummaryGrid` container. | | [CartSummaryList](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/containers/cart-summary-list/) | Learn about the `CartSummaryList` container. | | [CartSummaryTable](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/containers/cart-summary-table/) | Learn about the `CartSummaryTable` container. | | [Coupons](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/containers/coupons/) | Learn about the Coupons container. | | [EmptyCart](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/containers/empty-cart/) | Learn about the `EmptyCart` container. | | [EstimateShipping](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/containers/estimate-shipping/) | Learn about the `EstimateShipping` container. | | [GiftCards](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/containers/gift-cards/) | Learn about the `GiftCards` container. | | [GiftOptions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/containers/gift-options/) | Learn about the `GiftOptions` container. | | [MiniCart](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/containers/mini-cart/) | Displays a summary of the shopper's shopping cart. | | [OrderSummary](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/containers/order-summary/) | Learn about the `OrderSummary` container. | | [OrderSummaryLine](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/containers/order-summary-line/) | Learn about the `OrderSummaryLine` container. | > Each container is designed to work independently but can be composed together to create comprehensive user experiences. --- # MiniCart container The `MiniCart` container displays a summary of the shopper's shopping cart. It shows a list of products currently in the cart and subtotal amounts. It also provides call-to-action buttons for proceeding to checkout or updating the cart. ![MiniCart drop-in UI showing cart line items, quantities, subtotal, and buttons to view the full cart or continue to checkout](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/mini-cart.png) *MiniCart container* ## Configurations The `MiniCart` container provides the following configuration options: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `children` | `VNode[]` | No | The child elements to be rendered inside the mini cart. | | `initialData` | `CartModel \| null` | No | The initial data for the mini cart. Defaults to null. | | `hideFooter` | `boolean` | No | Flag to hide the footer in the mini cart. Defaults to true. | | `slots` | `{ ProductList?: SlotProps }` | No | Slot props for customizing the product list display. | | `routeProduct` | `function` | No | Function to generate the URL for a product. | | `routeCart` | `function` | No | Function to generate the URL for the cart page. | | `routeCheckout` | `function` | No | Function to generate the URL for the checkout page. | | `routeEmptyCartCTA` | `function` | No | Function to generate the URL for the empty cart call-to-action. | | `displayAllItems` | `boolean` | No | Flag to show all items. | | `showDiscount` | `boolean` | No | Flag to show discounts in the mini cart. | | `showSavings` | `boolean` | No | Flag to show savings in the mini cart. | | `enableItemRemoval` | `boolean` | Yes | Flag to enable removing items from the mini cart. When set to true, users can remove products from the cart directly in the mini cart interface. | | `enableQuantityUpdate` | `boolean \| { removeOnZero?: boolean }` | No | Enables quantity updates in the mini cart. Pass `{ removeOnZero: true }` to remove items when quantity reaches `0`. Defaults to `false`. | | `hideHeading` | `boolean` | No | Flag to hide the heading in the mini cart. When set to true, the mini cart header will not be displayed. | | `undo` | `boolean` | No | Enables the undo banner to restore recently removed items to the cart. | | `confirmBeforeDelete` | `boolean` | No | Enables the confirmation banner when an item is removed from the cart. When set to true, clicking the remove button shows an inline confirmation banner instead of immediately deleting the item. | The `CartModel` object has the following shape: ```ts export interface CartModel { id: string; totalQuantity: number; errors?: ItemError[]; items: Item[]; miniCartMaxItems: Item[]; total: { includingTax: Price; excludingTax: Price; }; discount?: Price; subtotal: { excludingTax: Price; includingTax: Price; includingDiscountOnly: Price; }; appliedTaxes: TotalPriceModifier[]; totalTax?: Price; appliedDiscounts: TotalPriceModifier[]; shipping?: Price; isVirtual?: boolean; addresses: { shipping?: { countryCode: string; zipCode?: string; regionCode?: string; }[]; }; isGuestCart?: boolean; hasOutOfStockItems?: boolean; hasFullyOutOfStockItems?: boolean; appliedCoupons?: Coupon[]; } interface TotalPriceModifier { amount: Price; label: string; coupon?: Coupon; } interface FixedProductTax { amount: Price; label: string; } export interface Item { taxedPrice: Price; rowTotal: Price; rowTotalIncludingTax: Price; itemType: string; uid: string; url: ItemURL; quantity: number; sku: string; name: string; image: ItemImage; links?: ItemLinks; price: Price; total: Price; discountedTotal?: Price; discount?: Price; regularPrice: Price; discounted: boolean; bundleOptions?: { [key: string]: any }; selectedOptions?: { [key: string]: any }; customizableOptions?: { [key: string]: any }; message?: string; recipient?: string; recipientEmail?: string; sender?: string; senderEmail?: string; lowInventory?: boolean; insufficientQuantity?: boolean; onlyXLeftInStock?: number | null; outOfStock?: boolean; notAvailableMessage?: string; stockLevel?: String; discountPercentage?: number; savingsAmount?: Price; productAttributes?: Attribute[]; fixedProductTaxes?: FixedProductTax[]; } interface ItemError { id: string; text: string; } interface ItemImage { src: string; alt: string; } export interface Price { value: number; currency: string; } interface ItemURL { urlKey: string; categories: string[]; } interface ItemLinks { count: number; result: string; } interface AttributeOption { value: string; label: string; } interface Attribute { code: string; value?: string; selected_options?: AttributeOption[]; } interface Coupon { code: string; } ``` ## Supported slots The `MiniCart` container supports the following slots: * ProductList * ProductListFooter * PreCheckoutSection * Thumbnail * Heading * EmptyCart * Footer * ProductAttributes * CartSummaryFooter * CartItem * UndoBanner * ConfirmDeleteBanner * ItemTitle * ItemPrice * ItemQuantity * ItemTotal * ItemSku * ItemRemoveAction ## Example configuration The following example demonstrates how to render the `MiniCart` container: ```javascript provider.render(MiniCart, { routeProduct: (item) => { return `${item.url.categories.join('/')}/${item.url.urlKey}`; }, routeEmptyCartCTA: () => '#empty-cart', routeCart: () => '#cart', routeCheckout: () => '#checkout', showDiscount: true, // showSavings: true, // enableItemRemoval: true, // enableQuantityUpdate: true, // hideHeading: true, // confirmBeforeDelete: true, })($miniCart); ``` --- # OrderSummary container The `OrderSummary` container displays a detailed summary of the shopper's order. It includes the subtotal, taxes, shipping costs, and total amount due. It optionally applies discounts or coupons. ![OrderSummary container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/order-summary.png) *OrderSummary container* This container supports the Coupon and EstimatedShipping slots. ## Configurations The `OrderSummary` container provides the following configuration options: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `children` | `VNode[]` | No | The child elements to be rendered inside the order summary. | | `initialData` | `CartModel \| null` | No | The initial data for the order summary. Defaults to null. | | `routeCheckout` | `function` | No | Function to generate the URL for the checkout page. | | `slots` | `Slot` | No | Slot props for customizing the estimate shipping and coupons display. | | `errors` | `boolean` | Yes | Flag to indicate if there are errors in the order summary. | | `showTotalSaved` | `boolean` | No | Flag to show the total amount saved in the order summary. | | `enableCoupons` | `boolean` | No | Flag to enable or disable the coupons section. | | `enableGiftCards` | `boolean` | No | Flag to enable or disable the gift cards section. | | `updateLineItems` | `function` | No | Function to update the line items in the order summary. Defaults to returning the same items. | The `CartModel` object has the following shape: ```ts export interface CartModel { id: string; totalQuantity: number; errors?: ItemError[]; items: Item[]; miniCartMaxItems: Item[]; total: { includingTax: Price; excludingTax: Price; }; discount?: Price; subtotal: { excludingTax: Price; includingTax: Price; includingDiscountOnly: Price; }; appliedTaxes: TotalPriceModifier[]; totalTax?: Price; appliedDiscounts: TotalPriceModifier[]; shipping?: Price; isVirtual?: boolean; addresses: { shipping?: { countryCode: string; zipCode?: string; regionCode?: string; }[]; }; isGuestCart?: boolean; hasOutOfStockItems?: boolean; hasFullyOutOfStockItems?: boolean; appliedCoupons?: Coupon[]; } interface TotalPriceModifier { amount: Price; label: string; coupon?: Coupon; } interface FixedProductTax { amount: Price; label: string; } export interface Item { taxedPrice: Price; rowTotal: Price; rowTotalIncludingTax: Price; itemType: string; uid: string; url: ItemURL; quantity: number; sku: string; name: string; image: ItemImage; links?: ItemLinks; price: Price; total: Price; discountedTotal?: Price; discount?: Price; regularPrice: Price; discounted: boolean; bundleOptions?: { [key: string]: any }; selectedOptions?: { [key: string]: any }; customizableOptions?: { [key: string]: any }; message?: string; recipient?: string; recipientEmail?: string; sender?: string; senderEmail?: string; lowInventory?: boolean; insufficientQuantity?: boolean; onlyXLeftInStock?: number | null; outOfStock?: boolean; notAvailableMessage?: string; stockLevel?: String; discountPercentage?: number; savingsAmount?: Price; productAttributes?: Attribute[]; fixedProductTaxes?: FixedProductTax[]; } interface ItemError { id: string; text: string; } interface ItemImage { src: string; alt: string; } export interface Price { value: number; currency: string; } interface ItemURL { urlKey: string; categories: string[]; } interface ItemLinks { count: number; result: string; } interface AttributeOption { value: string; label: string; } interface Attribute { code: string; value?: string; selected_options?: AttributeOption[]; } interface Coupon { code: string; } ``` ## Supported slots The `OrderSummary` container supports the Coupons and EstimateShipping slots. ## Example configuration The following example demonstrates how to render the `OrderSummary` container with the `EstimateShipping` and `Coupons` slots: ```js provider.render(OrderSummary, { routeCheckout: () => '#checkout', errors: ctx.hasErrors, slots: { EstimateShipping: (ctx) => { const estimateShippingForm = document.createElement('div'); provider.render(EstimateShipping, { showDefaultEstimatedShippingCost: true })(estimateShippingForm); ctx.appendChild(estimateShippingForm); }, Coupons: (ctx) => { const coupons = document.createElement('div'); provider.render(Coupons)(coupons); ctx.appendChild(coupons); }, }, showTotalSaved: true })(orderSummary); ``` --- # OrderSummaryLine container The `OrderSummaryLine` container displays a line item in the order summary. The `OrderSummaryLine` container behaves like a wrapper for the `OrderSummaryLine` component. The component ultimately decides how to render the line item, based on the `children` attribute. ![OrderSummaryLine container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/order-summary-line.png) *OrderSummaryLine container* ## Configurations The `OrderSummaryLine` container provides the following configuration options: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `label` | `VNode \| string` | Yes | The label for the order summary line. Accepts a plain string or a VNode for custom rendering. | | `price` | `VNode` | Yes | The price for the order summary line. | | `classSuffixes` | `string[]` | No | An array of class suffixes to apply to the order summary line. | | `labelClassSuffix` | `string` | No | The class suffix to apply to the label. | | `testId` | `string` | No | The test ID for the order summary line. | | `children` | `VNode[]` | No | The child elements to be rendered inside the order summary. | ## Example configuration The following example adds the Fixed Product Tax (PDT) line to the order summary: ```js updateLineItems: (lineItems) => { const totalFpt = ctx.data.items.reduce((allItemsFpt, item) => { const itemFpt = item.fixedProductTaxes.reduce( (accumulator, fpt) => { accumulator.labels.push(fpt.label); accumulator.total += fpt.amount.value; return accumulator; }, { labels: [], total: 0 } ); allItemsFpt.labels = [...allItemsFpt.labels, ...itemFpt.labels]; allItemsFpt.total += itemFpt.total; return allItemsFpt; }, { labels: [], total: 0 }); lineItems.push({ key: 'fpt', sortOrder: 350, title: 'Fixed Product Tax', content: OrderSummaryLine({ label: "FPT(" + totalFpt.labels.join(',') + ')', price: Price({ amount: totalFpt.total }), classSuffix: 'fpt' }) }); return lineItems; }; ``` --- # Cart Dictionary The **Cart dictionary** contains all user-facing text, labels, and messages displayed by this drop-in. Customize the dictionary to: - **Localize** the drop-in for different languages and regions - **Customize** labels and messages to match your brand voice - **Override** default text without modifying source code for the drop-in Dictionaries use the **i18n (internationalization)** pattern, where each text string is identified by a unique key path. Version: 3.3.0 ## How to customize Override dictionary values during drop-in initialization. The drop-in deep-merges your custom values with the defaults. ```javascript await initialize({ langDefinitions: { en_US: { "Cart": { "Cart": { "heading": "My Custom Title", "editCart": "Custom value" } } } } }); ``` You only need to include the keys you want to change. For multi-language support and advanced patterns, see the [Dictionary customization guide](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/dictionaries/). ## Default keys and values Below are the default English (`en_US`) strings provided by the **Cart** drop-in: ```json title="en_US.json" { "Cart": { "Cart": { "heading": "Shopping Cart ({count})", "editCart": "Edit", "viewAll": "View all in cart", "viewMore": "View more" }, "CartSummaryTable": { "item": "Item", "price": "Price", "qty": "Qty", "subtotal": "Subtotal", "mobilePrice": "Price", "mobileQty": "Qty", "mobileSubtotal": "Subtotal" }, "MiniCart": { "heading": "Shopping Cart ({count})", "subtotal": "Subtotal", "subtotalExcludingTaxes": "Subtotal excluding taxes", "cartLink": "View Cart", "checkoutLink": "Checkout" }, "EmptyCart": { "heading": "Your cart is empty", "cta": "Start shopping" }, "PriceSummary": { "taxToBeDetermined": "TBD", "checkout": "Checkout", "orderSummary": "Order Summary", "giftCard": { "label": "Gift Card", "applyAction": "Apply", "applyActionAriaLabel": "Apply gift card", "ariaLabel": "Enter gift card code", "ariaLabelRemove": "Remove gift card", "placeholder": "Enter code", "title": "Gift Card", "errors": { "empty": "Please enter a gift card code." }, "appliedGiftCards": { "label": { "singular": "Gift card", "plural": "Gift cards" }, "remainingBalance": "Remaining balance" } }, "giftOptionsTax": { "printedCard": { "title": "Printed card", "inclTax": "Including taxes", "exclTax": "excluding taxes" }, "itemGiftWrapping": { "title": "Item gift wrapping", "inclTax": "Including taxes", "exclTax": "excluding taxes" }, "orderGiftWrapping": { "title": "Order gift wrapping", "inclTax": "Including taxes", "exclTax": "excluding taxes" } }, "subTotal": { "label": "Subtotal", "withTaxes": "Including taxes", "withoutTaxes": "excluding taxes" }, "shipping": { "label": "Shipping", "editZipAction": "Apply", "estimated": "Estimated Shipping", "estimatedDestination": "Estimated Shipping to", "destinationLinkAriaLabel": "Change destination", "zipPlaceholder": "Zip Code", "withTaxes": "Including taxes", "withoutTaxes": "excluding taxes", "alternateField": { "zip": "Estimate using country/zip", "state": "Estimate using country/state" } }, "taxes": { "total": "Tax Total", "totalOnly": "Tax", "breakdown": "Taxes", "showBreakdown": "Show Tax Breakdown", "hideBreakdown": "Hide Tax Breakdown", "estimated": "Estimated Tax" }, "total": { "estimated": "Estimated Total", "free": "Free", "label": "Total", "withoutTax": "Total excluding taxes", "saved": "Total saved" }, "estimatedShippingForm": { "country": { "placeholder": "Country" }, "state": { "placeholder": "State" }, "zip": { "placeholder": "Zip Code" }, "apply": { "label": "Apply", "ariaLabel": "Apply destination" } }, "freeShipping": "Free", "coupon": { "applyAction": "Apply", "applyActionAriaLabel": "Apply discount code", "placeholder": "Enter code", "title": "Discount code", "ariaLabelRemove": "Remove coupon" } }, "CartItem": { "discountedPrice": "Discounted Price", "download": "file", "message": "Note", "recipient": "To", "regularPrice": "Regular Price", "sender": "From", "file": "{count} file", "files": "{count} files", "lowInventory": "Only {count} left!", "insufficientQuantity": "Only {inventory} of {count} in stock", "insufficientQuantityGeneral": "Not enough items for sale", "notAvailableMessage": "Requested qty. not available", "discountPercentage": "{discount}% off", "savingsAmount": "Savings", "includingTax": "Incl. tax", "excludingTax": "Excl. tax", "itemBeingRemoved": "\"{product}\" is being removed", "itemRemoved": "\"{product}\" was removed", "itemRemovedDescription": "Changed your mind? You can undo this action.", "undoAction": "Undo", "dismissAction": "Dismiss", "confirmDeleteHeading": "Remove \"{product}\" from your cart?", "confirmAction": "Remove", "cancelAction": "Cancel" }, "EstimateShipping": { "label": "Shipping", "editZipAction": "Apply", "estimated": "Estimated Shipping", "estimatedDestination": "Estimated Shipping to", "destinationLinkAriaLabel": "{destination}, Change destination", "zipPlaceholder": "Zip Code", "withTaxes": "Including taxes", "withoutTaxes": "excluding taxes", "alternateField": { "zip": "Estimate using country/zip", "state": "Estimate using country/state" } }, "OutOfStockMessage": { "heading": "Your cart contains items with limited stock", "message": "Please adjust quantities to continue", "alert": "Out of stock", "action": "Remove all out of stock items from cart" }, "GiftOptions": { "accordionAriaLabel": "{title} for {product}", "giftOptionsApplied": "Applied", "formText": { "requiredFieldError": "This field is required" }, "modal": { "defaultTitle": "Gift wrapping for Cart", "title": "Gift wrapping for", "wrappingText": "Wrapping choice", "wrappingSubText": "", "modalConfirmButton": "Apply", "modalCancelButton": "Cancel", "ariaLabelWrapping": "Wrapping options" }, "order": { "customize": "Customize", "accordionHeading": "Gift options", "giftReceiptIncluded": { "title": "Use gift receipt", "subtitle": "The receipt and order invoice will not show the price." }, "printedCardIncluded": { "title": "Include printed card", "subtitle": "" }, "giftOptionsWrap": { "title": "Gift wrap this order", "subtitle": "Wrapping option:" }, "formContent": { "formTitle": "Add a message to the order (optional)", "formTo": "To", "formFrom": "From", "giftMessageTitle": "Gift message", "formToPlaceholder": "Recipient's name", "formFromPlaceholder": "Sender's name", "formMessagePlaceholder": "Gift message" }, "readOnlyFormView": { "title": "Selected gift order options", "giftWrap": "Gift wrap this order", "giftWrapOptions": "Wrapping option:", "giftReceipt": "Use gift receipt", "giftReceiptText": "The receipt and order invoice will not show the price.", "printCard": "Use printed card", "printCardText": "", "formTitle": "Your gift message", "formTo": "To", "formFrom": "From", "formMessageTitle": "Gift message" } }, "product": { "customize": "Customize", "accordionHeading": "Gift options", "giftReceiptIncluded": { "title": "Use gift receipt", "subtitle": "The receipt and order invoice will not show the price." }, "printedCardIncluded": { "title": "Include printed card", "subtitle": "" }, "giftOptionsWrap": { "title": "Gift wrap this item", "subtitle": "Wrapping option:" }, "formContent": { "formTitle": "Add a message to the item (optional)", "formTo": "To", "formFrom": "From", "giftMessageTitle": "Gift message", "formToPlaceholder": "Recipient's name", "formFromPlaceholder": "Sender's name", "formMessagePlaceholder": "Gift message" }, "readOnlyFormView": { "title": "This item is a gift", "wrapping": "Wrapping:", "recipient": "To:", "sender": "From:", "message": "Message:" } } } } } ``` --- # Cart Data & Events The **Cart** drop-in uses the [event bus](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/events/) to emit and listen to events for communication between drop-ins and external integrations. Version: 3.3.0 ## Events reference {/* EVENTS_TABLE_START */} | Event | Direction | Description | |-------|-----------|-------------| | [cart/initialized](#cartinitialized-emits) | Emits | Emitted when the component completes initialization. | | [cart/product/added](#cartproductadded-emits) | Emits | Emitted when an item is added. | | [cart/product/removed](#cartproductremoved-emits) | Emits | Emitted when an item is removed. | | [cart/product/updated](#cartproductupdated-emits) | Emits | Emitted when the component state is updated. | | [checkout/initialized](#checkoutinitialized-listens) | Listens | Fired by Checkout (`checkout`) when the component completes initialization. | | [checkout/updated](#checkoutupdated-listens) | Listens | Fired by Checkout (`checkout`) when the component state is updated. | | [requisitionList/alert](#requisitionlistalert-listens) | Listens | Fired by Requisition List (`requisitionList`) when an alert or notification is triggered. | | [cart/data](#cartdata-emits-and-listens) | Emits and listens | Triggered when data is available or changes. | | [cart/merged](#cartmerged-emits-and-listens) | Emits and listens | Triggered when data is merged. | | [cart/reset](#cartreset-emits-and-listens) | Emits and listens | Triggered when the component state is reset. | | [cart/updated](#cartupdated-emits-and-listens) | Emits and listens | Triggered when the component state is updated. | | [shipping/estimate](#shippingestimate-emits-and-listens) | Emits and listens | Triggered when an estimate is calculated. | {/* EVENTS_TABLE_END */} ## Event details The following sections provide detailed information about each event, including its direction, event payload, and usage examples. ### `cart/data` (emits and listens) Emitted when cart data is available or changes. This event is triggered during cart initialization and updates to provide the current cart state. #### Event payload ```typescript CartModel | null ``` See [`CartModel`](#cartmodel) for full type definition. #### Example ```js events.on('cart/data', (payload) => { console.log('cart/data event received:', payload); // Add your custom logic here }); ``` ### `cart/initialized` (emits) Emitted when the component completes initialization. #### Event payload ```typescript CartModel | null ``` See [`CartModel`](#cartmodel) for full type definition. #### Example ```js events.on('cart/initialized', (payload) => { console.log('cart/initialized event received:', payload); // Add your custom logic here }); ``` ### `cart/merged` (emits and listens) Emitted when a guest cart is merged with a customer cart after login. This typically happens when an unauthenticated user adds items to their cart, then signs in, and their guest cart items are combined with any existing items in their customer cart. #### Event payload ```typescript { oldCartItems: Item[] | null; newCart: CartModel | null; } ``` See [`Item`](#item), [`CartModel`](#cartmodel) for full type definitions. #### Example ```js events.on('cart/merged', (payload) => { console.log('cart/merged event received:', payload); // Add your custom logic here }); ``` ### `cart/product/added` (emits) Emitted when new products are added to the cart. This event fires for genuinely new items, not quantity updates of existing items. #### Event payload ```typescript Item[] | null ``` See [`Item`](#item) for full type definition. #### Example ```js events.on('cart/product/added', (payload) => { console.log('cart/product/added event received:', payload); // Add your custom logic here }); ``` ### `cart/product/removed` (emits) Emitted when an item is removed. #### Event payload #### Example ```js events.on('cart/product/removed', (payload) => { console.log('cart/product/removed event received:', payload); // Add your custom logic here }); ``` ### `cart/product/updated` (emits) Emitted when the quantity of existing cart items is increased. This event fires when adding more of a product that's already in the cart, as opposed to adding a brand new product. #### Event payload ```typescript Item[] | null ``` See [`Item`](#item) for full type definition. #### Example ```js events.on('cart/product/updated', (payload) => { console.log('cart/product/updated event received:', payload); // Add your custom logic here }); ``` ### `cart/reset` (emits and listens) Triggered when the component state is reset. #### Event payload #### Example ```js events.on('cart/reset', (payload) => { console.log('cart/reset event received:', payload); // Add your custom logic here }); ``` ### `cart/updated` (emits and listens) Triggered when the component state is updated. #### Event payload ```typescript CartModel | null ``` See [`CartModel`](#cartmodel) for full type definition. #### Example ```js events.on('cart/updated', (payload) => { console.log('cart/updated event received:', payload); // Add your custom logic here }); ``` ### `checkout/initialized` (listens) Fired by Checkout (`checkout`) when the component completes initialization. #### Event payload ```typescript Cart | NegotiableQuote | null ``` See [`Cart`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/events/#cart), [`NegotiableQuote`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/events/#negotiablequote) for full type definitions. #### Example ```js events.on('checkout/initialized', (payload) => { console.log('checkout/initialized event received:', payload); // Add your custom logic here }); ``` ### `checkout/updated` (listens) Fired by Checkout (`checkout`) when the component state is updated. #### Event payload ```typescript Cart | NegotiableQuote | null ``` See [`Cart`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/events/#cart), [`NegotiableQuote`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/events/#negotiablequote) for full type definitions. #### Example ```js events.on('checkout/updated', (payload) => { console.log('checkout/updated event received:', payload); // Add your custom logic here }); ``` ### `requisitionList/alert` (listens) Fired by Requisition List (`requisitionList`) when an alert or notification is triggered. #### Event payload #### Example ```js events.on('requisitionList/alert', (payload) => { console.log('requisitionList/alert event received:', payload); // Add your custom logic here }); ``` ### `shipping/estimate` (emits and listens) Emitted when shipping cost estimates are calculated for a given address. This event provides both the address used for estimation and the resulting shipping method with its cost. #### Event payload ```typescript { address: PartialAddress; shippingMethod: ShippingMethod | null; } ``` See [`PartialAddress`](#partialaddress), [`ShippingMethod`](#shippingmethod) for full type definitions. #### Example ```js events.on('shipping/estimate', (payload) => { console.log('shipping/estimate event received:', payload); // Add your custom logic here }); ``` ## Data Models The following data models are used in event payloads for this drop-in. ### CartModel The `CartModel` represents the complete state of a shopping cart, including items, pricing, discounts, shipping estimates, and gift options. Used in: [`cart/data`](#cartdata-emits-and-listens), [`cart/initialized`](#cartinitialized-emits), [`cart/merged`](#cartmerged-emits-and-listens), [`cart/updated`](#cartupdated-emits-and-listens). ```ts interface CartModel { totalGiftOptions: { giftWrappingForItems: Price; giftWrappingForItemsInclTax: Price; giftWrappingForOrder: Price; giftWrappingForOrderInclTax: Price; printedCard: Price; printedCardInclTax: Price; }; cartGiftWrapping: { uid: string; design: string; selected: boolean; image: WrappingImage; price: Price; }[]; giftReceiptIncluded: boolean; printedCardIncluded: boolean; giftMessage: { recipientName: string; senderName: string; message: string; }; appliedGiftCards: AppliedGiftCardProps[]; id: string; totalQuantity: number; totalUniqueItems: number; errors?: ItemError[]; items: Item[]; miniCartMaxItems: Item[]; total: { includingTax: Price; excludingTax: Price; }; discount?: Price; subtotal: { excludingTax: Price; includingTax: Price; includingDiscountOnly: Price; }; appliedTaxes: TotalPriceModifier[]; totalTax?: Price; appliedDiscounts: TotalPriceModifier[]; shipping?: Price; isVirtual?: boolean; addresses: { shipping?: { countryCode: string; zipCode?: string; regionCode?: string; }[]; }; isGuestCart?: boolean; hasOutOfStockItems?: boolean; hasFullyOutOfStockItems?: boolean; appliedCoupons?: Coupon[]; } ``` ### Item The `Item` interface represents a single product in the cart, including product details, pricing, quantity, customization options, and inventory status. Used in: [`cart/merged`](#cartmerged-emits-and-listens), [`cart/product/added`](#cartproductadded-emits), [`cart/product/updated`](#cartproductupdated-emits). ```ts interface Item { giftWrappingAvailable: boolean; giftWrappingPrice: { currency: string; value: number; }; productGiftWrapping: { uid: string; design: string; selected: boolean; image: WrappingImage; price: Price; }[]; giftMessage: { recipientName: string; senderName: string; message: string; }; priceTiers: PriceTier[]; giftMessageAvailable: boolean | null; taxedPrice: Price; rowTotal: Price; rowTotalIncludingTax: Price; itemType: string; uid: string; url: ItemURL; canonicalUrl: string; categories: string[]; quantity: number; sku: string; topLevelSku: string; name: string; image: ItemImage; links?: ItemLinks; price: Price; total: Price; discountedTotal?: Price; discount?: Price; regularPrice: Price; discounted: boolean; bundleOptions?: { [key: string]: any }; bundleOptionsUIDs?: string[]; selectedOptions?: { [key: string]: any }; selectedOptionsUIDs?: { [key: string]: any }; customizableOptions?: { [key: string]: any }; message?: string; recipient?: string; recipientEmail?: string; sender?: string; senderEmail?: string; lowInventory?: boolean; insufficientQuantity?: boolean; onlyXLeftInStock?: number | null; outOfStock?: boolean; notAvailableMessage?: string; stockLevel?: String; discountPercentage?: number; savingsAmount?: Price; productAttributes?: Attribute[]; fixedProductTaxes?: FixedProductTax[]; } ``` ### PartialAddress The `PartialAddress` interface represents a minimal address used for shipping estimates, containing country, postal code, and region information. Used in: [`shipping/estimate`](#shippingestimate-emits-and-listens). ```ts interface PartialAddress { countryCode: string; postCode?: string; region?: string; regionCode?: string; regionId?: number; } ``` ### ShippingMethod The `ShippingMethod` interface represents a shipping option with carrier and method codes, along with pricing information. Used in: [`shipping/estimate`](#shippingestimate-emits-and-listens). ```ts interface ShippingMethod { carrierCode: string; methodCode: string; amountExclTax?: Price; amountInclTax?: Price; } ``` --- # Cart Functions The Cart drop-in provides API functions that enable you to programmatically control behavior, fetch data, and integrate with Adobe Commerce backend services. Version: 3.3.0 | Function | Description | | --- | --- | | [`addProductsToCart`](#addproductstocart) | Adds products to a cart. | | [`applyCouponsToCart`](#applycouponstocart) | Applies or replaces one or more coupons to the cart. | | [`applyGiftCardToCart`](#applygiftcardtocart) | Apply a gift card to the current shopping cart. | | [`createGuestCart`](#createguestcart) | Creates a new empty cart for a guest user. | | [`getCartData`](#getcartdata) | Is mainly used internally by the `initializeCart`() and `refreshCart`() functions. | | [`getCartDataFromCache`](#getcartdatafromcache) | Returns the current cart data from local storage without making an API call. | | [`getCountries`](#getcountries) | API function for the drop-in. | | [`getCustomerCartPayload`](#getcustomercartpayload) | Fetches the authenticated customer's cart, merging with any existing guest cart if needed. | | [`getEstimatedTotals`](#getestimatedtotals) | Returns estimated totals for cart based on an address. | | [`getEstimateShipping`](#getestimateshipping) | Returns the first available shipping method and its estimated cost, based on the provided address. | | [`getGuestCartPayload`](#getguestcartpayload) | Fetches the current guest cart data using the cart ID stored in state. | | [`getRegions`](#getregions) | API function for the drop-in. | | [`getStoreConfig`](#getstoreconfig) | Returns information about a store's configuration. | | [`initializeCart`](#initializecart) | Initializes a guest or customer cart. | | [`publishShoppingCartViewEvent`](#publishshoppingcartviewevent) | Publishes a shopping cart view event to the ACDL. | | [`refreshCart`](#refreshcart) | Refreshes the cart data. | | [`removeGiftCardFromCart`](#removegiftcardfromcart) | This function removes a single gift card from the cart. | | [`resetCart`](#resetcart) | This function resets the cart drop-in. | | [`setGiftOptionsOnCart`](#setgiftoptionsoncart) | `setGiftOptionsOnCart` is a function that sets gift options on the cart. | | [`updateProductsFromCart`](#updateproductsfromcart) | Updates cart items by either changing the quantity or removing and adding an item in one step. | ## addProductsToCart The `addProductsToCart` function adds products to a cart. You must supply a `sku` and `quantity` for each product. The other parameters are specified for complex product types. The function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/add-products/ mutation. ```ts const addProductsToCart = async ( items: { sku: string; parentSku?: string; quantity: number; optionsUIDs?: string[]; enteredOptions?: { uid: string; value: string }[]; customFields?: Record; }[] ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `sku` | `string` | Yes | The product identifier (SKU) to add to the cart. For configurable products (like a shirt available in multiple colors and sizes), use the child product SKU that represents the specific variant selected by the customer (e.g., \`MS09-M-Blue\` for a medium blue shirt). | | `parentSku` | `string` | No | For configurable products, this is the SKU of the parent (base) product. For example, if adding a specific variant like \`MS09-M-Blue\` (child SKU), the \`parentSku\` would be \`MS09\` (the base configurable product). This helps Commerce track the relationship between the variant and its parent product. | | `quantity` | `number` | Yes | The number of items to add to the cart. For example, \`1\` to add a single item, or \`3\` to add three units of the product. This value must be a positive number. | | `optionsUIDs` | `string[]` | No | An array of option UIDs for configurable products. These are the UIDs of the selected product options (such as color or size) that define which product variant the customer wants. For example, if a customer selects \*\*Medium\*\* and \*\*Blue\*\* for a configurable shirt, you would include the UIDs for those specific options. Use the product query to retrieve available option UIDs for a product. | | `enteredOptions` | `{ uid: string; value: string }[]` | No | An array of custom options that allow text input for customizable products. Each object contains a \`uid\` (the unique identifier for the custom option field from the product data) and a \`value\` (the text the customer entered). For example, if a product offers monogram personalization, you would provide the field's UID and the customer's text like \`\{ uid: 'Y3VzdG9tLW9wdGlvbi8x', value: 'ABC' \}\`. | | `customFields` | `Record` | No | An optional object for passing additional custom data or attributes to associate with the cart item. This can include any key-value pairs needed for your implementation, such as gift messages, special handling instructions, or custom metadata. The structure and usage of this field depends on your Commerce backend configuration and any custom extensions you have installed. | ### Events Emits the [`cart/updated`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartupdated-emits-and-listens) and [`cart/data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartdata-emits-and-listens) events with the [`CartModel`](#cartmodel) as the data payload. Additionally, emits `cart/product/added` for new items and `cart/product/updated` for items with increased quantities. Also publishes add-to-cart or remove-from-cart events to the Adobe Client Data Layer (ACDL). ### Returns Returns [`CartModel`](#cartmodel) or `null`. ## applyCouponsToCart A function that applies or replaces one or more coupons to the cart. The function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/apply-coupon/ mutation. ```ts const applyCouponsToCart = async ( couponCodes: string[], type: ApplyCouponsStrategy ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `couponCodes` | `string[]` | Yes | An array of coupon codes to apply to the cart. | | `type` | `ApplyCouponsStrategy` | Yes | The strategy for applying coupons. See \[\`ApplyCouponsStrategy\`\](#applycouponsstrategy). | ### Events Emits the [`cart/updated`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartupdated-emits-and-listens) and [`cart/data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartdata-emits-and-listens) events with the updated cart information after applying the coupons. ### Returns Returns [`CartModel`](#cartmodel) or `null`. ## applyGiftCardToCart The `applyGiftCardToCart` function is used to apply a gift card to the current shopping cart. It takes the gift card code as an argument and updates the cart with the applied gift card. The function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/apply-giftcard/ mutation. ```ts const applyGiftCardToCart = async ( giftCardCode: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `giftCardCode` | `string` | Yes | The code assigned to a gift card. | ### Events Emits the [`cart/updated`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartupdated-emits-and-listens) and [`cart/data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartdata-emits-and-listens) events. Also publishes add-to-cart or remove-from-cart events to the Adobe Client Data Layer (ACDL). ### Returns Returns [`CartModel`](#cartmodel) or `null`. ## createGuestCart The `createGuestCart` function creates a new empty cart for a guest user. This is typically used internally by the cart initialization process. ```ts const createGuestCart = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns the cart ID for the newly created guest cart: `cartId: string | null` ## getCartData The `getCartData` function is mainly used internally by the `initializeCart()` and `refreshCart()` functions. If you need detailed information about the current user's shopping cart, a more optimal approach is to listen for `c`art/dat`a` or `c`art/update`d` events so that you do not need to make another network call. ```ts const getCartData = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns [`CartModel`](#cartmodel) or `null`. ## getCountries ```ts const getCountries = async (): Promise<[CountryData]> ``` ### Events Does not emit any drop-in events. ### Returns Returns `[CountryData]`. ## getEstimatedTotals A function that returns estimated totals for cart based on an address. It takes an `address` parameter. The function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/estimate-totals/ mutation. ```ts const getEstimatedTotals = async ( address: EstimateAddressShippingInput ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `address` | `EstimateAddressShippingInput` | Yes | The shipping address used to calculate estimated cart totals, taxes, and shipping costs. See \[\`EstimateAddressShippingInput\`\](#estimateaddressshippinginput). | ### Events Does not emit any drop-in events. ### Returns Returns [`CartModel`](#cartmodel) or `null`. ## getEstimateShipping The `getEstimateShipping` function returns the first available shipping method and its estimated cost, based on the provided address. The function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/estimate-shipping-methods/ mutation. Note: This function returns raw `GraphQL` data. For a transformed `ShippingMethod` object, listen to the [`shipping/estimate`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#shippingestimate-emits-and-listens) event instead. ```ts const getEstimateShipping = async ( address: EstimateAddressInput ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `address` | `EstimateAddressInput` | Yes | The address criteria used to determine available shipping methods. See \[\`EstimateAddressInput\`\](#estimateaddressinput). | ### Events Emits the [`shipping/estimate`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#shippingestimate-emits-and-listens) event, which contains the transformed `ShippingMethod` data along with address information. ### Returns Returns a [`RawShippingMethodGraphQL`](#rawshippingmethodgraphql) object with snake_case properties from the GraphQL response, or null if no valid shipping method is available. ## getRegions ```ts const getRegions = async ( countryId: string ): Promise> ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `countryId` | `string` | Yes | See function signature above | ### Events Does not emit any drop-in events. ### Returns Returns `Array<{ code: string; name: string }>`. ## getStoreConfig The `getStoreConfig` function returns information about a store's configuration. The function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/store/queries/store-config/ query. ```ts const getStoreConfig = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns [`StoreConfigModel`](#storeconfigmodel) or `null`. ## initializeCart The `initializeCart` function initializes a guest or customer cart. This function is automatically called during the initialize phase of a drop-in's lifecycle. You do not need to call this manually. The function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/merge/ mutation. ```ts const initializeCart = async (): Promise ``` ### Events Emits the [`cart/initialized`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartinitialized-emits), [`cart/data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartdata-emits-and-listens), and [`cart/merged`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartmerged-emits-and-listens) events. The event payload contains data about the address and shipping method. ### Returns Returns [`CartModel`](#cartmodel) or `null`. ## publishShoppingCartViewEvent Publishes a shopping cart view event to the ACDL. This function sets the shopping cart context and triggers a `SHOPPING_CART_VIEW` event on the Adobe Client Data Layer, typically used when a cart page loads. ```ts const publishShoppingCartViewEvent = async (): any ``` ### Events Does not emit any drop-in events. Publishes the `SHOPPING_CART_VIEW` event to the Adobe Client Data Layer (ACDL) with the current cart context. ### Returns Returns `void`. ## refreshCart The `refreshCart` function refreshes the cart data. ```ts const refreshCart = async (): Promise ``` ### Events Emits the [`cart/data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartdata-emits-and-listens) event with the updated cart information. ### Returns Returns [`CartModel`](#cartmodel) or `null`. ## removeGiftCardFromCart This function removes a single gift card from the cart. It function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/remove-giftcard/ mutation. ```ts const removeGiftCardFromCart = async ( giftCardCode: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `giftCardCode` | `string` | Yes | Defines the gift card code to remove. | ### Events Emits the [`cart/updated`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartupdated-emits-and-listens) and [`cart/data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartdata-emits-and-listens) events. ### Returns Returns [`CartModel`](#cartmodel) or `null`. ## resetCart This function resets the cart drop-in. As a result, the cart ID is set to null and the authenticated status is set to false. ```ts const resetCart = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns [`CartModel`](#cartmodel) or `null`. ## setGiftOptionsOnCart https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/set-gift-options/ is a function that sets gift options on the cart. It takes a `giftOptions` parameter. ```ts const setGiftOptionsOnCart = async ( giftForm: GiftFormDataType ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `giftForm` | `GiftFormDataType` | Yes | Defines the gift options to set. | ### Events Emits the [`cart/updated`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartupdated-emits-and-listens) and [`cart/data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartdata-emits-and-listens) events. ### Returns Returns [`CartModel`](#cartmodel) or `null`. ## updateProductsFromCart The `updateProductsFromCart` function updates cart items by either changing the quantity or removing and adding an item in one step. When passing a specified quantity, the function replaces the current quantity. Setting the quantity to 0 removes an item from the cart. The function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/update-items/ mutation. When an `optionsUIDs` array is sent along with the cart item’s UID and quantity, the function adds the item with the specified options. It removes any pre-existing item with the same UID that lacks the newly provided `optionsUIDs`. In this process, the function invokes first the https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/add-products/, and later the https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/update-items/ mutations. ```ts const updateProductsFromCart = async ( items: UpdateProductsFromCart ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `items` | `UpdateProductsFromCart` | Yes | An input object that defines products to be updated. | ### Events Emits the [`cart/updated`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartupdated-emits-and-listens) and [`cart/data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartdata-emits-and-listens) events. Additionally, emits `cart/product/updated` event with the affected items when their quantities are changed. Also publishes add-to-cart or remove-from-cart events to the Adobe Client Data Layer (ACDL). ### Returns Returns [`CartModel`](#cartmodel) or `null`. ## getCartDataFromCache The `getCartDataFromCache` function returns the current cart data from local storage without making a network request. This is useful when you need a synchronous read of the last-known cart state. ```ts const getCartDataFromCache = (): CartModel | null ``` ### Events Does not emit any drop-in events. ### Returns Returns [`CartModel`](#cartmodel) or `null` if no cart data is cached. ## getCustomerCartPayload The `getCustomerCartPayload` function fetches the authenticated customer's cart from the backend. If a guest cart exists in state, it is merged into the customer cart before the result is returned. Used internally by `initializeCart`. ```ts const getCustomerCartPayload = async (): Promise ``` ### Events Emits the [`cart/merged`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartmerged-emits-and-listens) event when a guest cart is merged into the customer cart. ### Returns Returns [`CartModel`](#cartmodel) or `null`. ## getGuestCartPayload The `getGuestCartPayload` function fetches the current guest cart data using the cart ID stored in the drop-in state. Returns `null` if guest carts are disabled or if no cart ID is present. Used internally by `initializeCart`. ```ts const getGuestCartPayload = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns [`CartModel`](#cartmodel) or `null`. ## Data Models The following data models are used by functions in this drop-in. ### CartModel The `CartModel` object is returned by the following functions: [`addProductsToCart`](#addproductstocart), [`applyCouponsToCart`](#applycouponstocart), [`applyGiftCardToCart`](#applygiftcardtocart), [`getCartData`](#getcartdata), [`getEstimatedTotals`](#getestimatedtotals), [`initializeCart`](#initializecart), [`refreshCart`](#refreshcart), [`removeGiftCardFromCart`](#removegiftcardfromcart), [`resetCart`](#resetcart), [`setGiftOptionsOnCart`](#setgiftoptionsoncart), [`updateProductsFromCart`](#updateproductsfromcart). ```ts interface CartModel { totalGiftOptions: { giftWrappingForItems: Price; giftWrappingForItemsInclTax: Price; giftWrappingForOrder: Price; giftWrappingForOrderInclTax: Price; printedCard: Price; printedCardInclTax: Price; }; cartGiftWrapping: { uid: string; design: string; selected: boolean; image: WrappingImage; price: Price; }[]; giftReceiptIncluded: boolean; printedCardIncluded: boolean; giftMessage: { recipientName: string; senderName: string; message: string; }; appliedGiftCards: AppliedGiftCardProps[]; id: string; totalQuantity: number; totalUniqueItems: number; errors?: ItemError[]; items: Item[]; miniCartMaxItems: Item[]; total: { includingTax: Price; excludingTax: Price; }; discount?: Price; subtotal: { excludingTax: Price; includingTax: Price; includingDiscountOnly: Price; }; appliedTaxes: TotalPriceModifier[]; totalTax?: Price; appliedDiscounts: TotalPriceModifier[]; shipping?: Price; isVirtual?: boolean; addresses: { shipping?: { countryCode: string; zipCode?: string; regionCode?: string; }[]; }; isGuestCart?: boolean; hasOutOfStockItems?: boolean; hasFullyOutOfStockItems?: boolean; appliedCoupons?: Coupon[]; } ``` ### StoreConfigModel The `StoreConfigModel` object is returned by the following functions: [`getStoreConfig`](#getstoreconfig). ```ts interface StoreConfigModel { displayMiniCart: boolean; miniCartMaxItemsDisplay: number; cartExpiresInDays: number; cartSummaryDisplayTotal: number; cartSummaryMaxItems: number; defaultCountry: string; categoryFixedProductTaxDisplaySetting: string; productFixedProductTaxDisplaySetting: string; salesFixedProductTaxDisplaySetting: string; shoppingCartDisplaySetting: { fullSummary: boolean; grandTotal: boolean; price: number | string; shipping: number | string; subtotal: number | string; taxGiftWrapping: number | string; zeroTax: boolean; }; useConfigurableParentThumbnail: boolean; allowGiftWrappingOnOrder: boolean | null; allowGiftWrappingOnOrderItems: boolean | null; allowGiftMessageOnOrder: boolean | null; allowGiftMessageOnOrderItems: boolean | null; allowGiftReceipt: boolean; allowPrintedCard: boolean; printedCardPrice: Price; cartGiftWrapping: string; cartPrintedCard: string; } ``` ### RawShippingMethodGraphQL The raw GraphQL response structure with snake_case properties returned by the estimateShippingMethods mutation. Returned by: [`getEstimateShipping`](#getestimateshipping). ```ts interface RawShippingMethodGraphQL { amount: { currency: string; value: number; }; carrier_code: string; method_code: string; error_message?: string; price_excl_tax: { currency: string; value: number; }; price_incl_tax: { currency: string; value: number; }; } ``` ### ApplyCouponsStrategy Strategy for how coupons should be applied to the cart: - `APPEND`: Adds the specified coupons to any existing coupons already applied to the cart - `REPLACE`: Removes all existing coupons and applies only the specified coupons Used by: [`applyCouponsToCart`](#applycouponstocart). ```ts enum ApplyCouponsStrategy { APPEND = "APPEND", REPLACE = "REPLACE" } ``` ### EstimateAddressInput Defines the address criteria for estimating shipping methods. Used by: [`getEstimateShipping`](#getestimateshipping). ```ts interface EstimateAddressInput { countryCode: string; postcode?: string; region?: { region?: string; code?: string; id?: number; }; } ``` ### EstimateAddressShippingInput Defines the shipping address for calculating cart totals. Used by: [`getEstimatedTotals`](#getestimatedtotals). ```ts interface EstimateAddressShippingInput { countryCode: string; postcode?: string; region?: { region?: string; id?: number; }; shipping_method?: { carrier_code?: string; method_code?: string; }; } ``` {/* This documentation is auto-generated from the drop-in source repository: REPO_URL */} --- # Cart overview The cart drop-in component provides a variety of fully editable controls to help you view, update, and merge the products in your cart and mini-cart, including image thumbnails, pricing, descriptions, quantities, estimated shipping and taxes, order summary, merging guest and authenticated carts, and more. ## Supported Commerce features The following table provides an overview of the Adobe Commerce features that the cart supports: | Feature | Status | | ---------------------------------------------------------------- | ------------------------------------------ | | Adobe Experience Platform Audiences | Roadmap | | All product types | Supported | | Apply coupons | Supported | | Apply gift cards | Supported | | Apply gift options | Supported | | Cart API extensibility | Supported | | Cart layout templates | Supported | | Cart rules | Supported | | Cart with 100+ products | Supported | | Commerce segments | Supported | | Customer cart | Supported | | Edit product configuration in cart | Supported | | Estimate tax/shipping | Supported | | Guest cart | Supported | | Low product stock alert | Supported | | Mini-cart | Supported | | No-code UI configurations | Supported | | Out of stock/insufficient quantity products (optional inclusion in the cart summary list) | Supported | | Product line discounts (catalog rule, special price, tier price) | Supported | | Save to wishlist | Supported | | Slots for extensibility | Supported | | Taxes: Fixed | Supported | | Taxes: Sales, VAT | Supported | | Undo remove product from cart | Supported | ## Section topics The topics in this section will help you understand how to customize and use the cart effectively within your storefront. ### Quick Start Provides quick reference information and a getting started guide for the Cart drop-in. This topic covers package details, import paths, and basic usage examples to help you integrate shopping cart functionality into your site. Visit the [Cart quick start](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/quick-start/) page to get started. ### Styles Describes how to customize the appearance of the cart using CSS. We provide guidelines and examples for applying styles to various components within the drop-in. This customization allows brands to align the drop-in component's look and feel with their overall design aesthetic, enhancing brand consistency across the platform. Visit the [cart styles](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/styles/) page to learn more. ### Containers Describes the structural elements of the cart, specifically focusing on how containers manage and display content. It includes information on configuration options and how to leverage these settings to customize the user experience. Understanding containers is essential for developers looking to optimize the layout and styling of the cart. Visit the cart containers page to learn more. ### Slots Slots allow developers to customize the appearance of the cart by adding or modifying content within specific sections of the drop-in component. Visit the [cart slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/slots/) page to learn more. ### Functions Describes the API functions available in the Cart drop-in. These functions allow developers to retrieve and display detailed cart information dynamically. Visit the [Cart Functions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/functions/) page to learn more. --- # Cart initialization The **Cart initializer** configures how the cart manages shopping cart data, including items, pricing, discounts, and customer information. Use initialization to customize cart behavior, enable guest cart features, and transform cart data models to match your storefront requirements. Version: 3.3.1 ## Configuration options The following table describes the configuration options available for the **Cart** initializer: | Parameter | Type | Req? | Description | |---|---|---|---| | `langDefinitions` | [`LangDefinitions`](#langdefinitions) | No | Language definitions for internationalization (i18n). Override dictionary keys for localization or branding. | | `models` | [`Record`](#models) | No | Custom data models for type transformations. Extend or modify default models with custom fields and transformers. | | `disableGuestCart` | `boolean` | No | When set to \`true\`, prevents guest users from creating or accessing shopping carts, requiring authentication before cart operations. | ## Default configuration The initializer runs with these defaults when no configuration is provided: ```javascript title="scripts/initializers/cart.js" // All configuration options are optional await initializers.mountImmediately(initialize, { langDefinitions: {}, // Uses built-in English strings models: {}, // Uses default data models // Drop-in-specific defaults: // disableGuestCart: undefined // See configuration options below }); ``` ## Language definitions Override dictionary keys for localization or branding. The `langDefinitions` object maps locale keys to custom strings that override default text for the drop-in. ```javascript title="scripts/initializers/cart.js" const customStrings = { 'AddToCart': 'Add to Bag', 'Checkout': 'Complete Purchase', 'Price': 'Cost', }; const langDefinitions = { default: customStrings, }; await initializers.mountImmediately(initialize, { langDefinitions }); ``` > For complete dictionary customization including all available keys and multi-language support, see the [Cart Dictionary](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/dictionary/) page. ## Customizing data models Extend or transform data models by providing custom transformer functions. Use the `models` option to add custom fields or modify existing data structures returned from the backend. ### Available models The following models can be customized through the `models` configuration option: | Model | Description | |---|---| | [`CartModel`](#cartmodel) | Transforms cart data from `GraphQL` including items, totals, discounts, taxes, gift options, addresses, and payment methods. Use this to add custom fields or modify existing cart data structures. | The following example shows how to customize the `CartModel` model for the **Cart** drop-in: ```javascript title="scripts/initializers/cart.js" const models = { CartModel: { transformer: (data) => ({ // Add custom fields from backend data customField: data?.custom_field, promotionBadge: data?.promotion?.label, // Transform existing fields displayPrice: data?.price?.value ? `${data.price.value}` : 'N/A', }), }, }; await initializers.mountImmediately(initialize, { models }); ``` ## Drop-in configuration The **Cart initializer** configures how the cart manages shopping cart data, including items, pricing, discounts, and customer information. Use initialization to customize cart behavior, enable guest cart features, and transform cart data models to match your storefront requirements. ```javascript title="scripts/initializers/cart.js" await initializers.mountImmediately(initialize, { disableGuestCart: true, langDefinitions: {}, models: {}, }); ``` > Refer to the [Configuration options](#configuration-options) table for detailed descriptions of each option. ## Configuration types The following TypeScript definitions show the structure of each configuration object: ### langDefinitions Maps locale identifiers to dictionaries of key-value pairs. The `default` locale is used as the fallback when no specific locale matches. Each dictionary key corresponds to a text string used in the drop-in UI. ```typescript langDefinitions?: { [locale: string]: { [key: string]: string; }; }; ``` ### models Maps model names to transformer functions. Each transformer receives data from GraphQL and returns a modified or extended version. Use the `Model` type from `@dropins/tools` to create type-safe transformers. ```typescript models?: { [modelName: string]: Model; }; ``` ## Model definitions The following TypeScript definitions show the structure of each customizable model: ### CartModel ```typescript export interface CartModel { totalGiftOptions: { giftWrappingForItems: Price; giftWrappingForItemsInclTax: Price; giftWrappingForOrder: Price; giftWrappingForOrderInclTax: Price; printedCard: Price; printedCardInclTax: Price; }; cartGiftWrapping: { uid: string; design: string; selected: boolean; image: WrappingImage; price: Price; }[]; giftReceiptIncluded: boolean; printedCardIncluded: boolean; giftMessage: { recipientName: string; senderName: string; message: string; }; appliedGiftCards: AppliedGiftCardProps[]; id: string; totalQuantity: number; totalUniqueItems: number; errors?: ItemError[]; items: Item[]; miniCartMaxItems: Item[]; total: { includingTax: Price; excludingTax: Price; }; discount?: Price; subtotal: { excludingTax: Price; includingTax: Price; includingDiscountOnly: Price; }; appliedTaxes: TotalPriceModifier[]; totalTax?: Price; appliedDiscounts: TotalPriceModifier[]; shipping?: Price; isVirtual?: boolean; addresses: { shipping?: { countryCode: string; zipCode?: string; regionCode?: string; }[]; }; isGuestCart?: boolean; hasOutOfStockItems?: boolean; hasFullyOutOfStockItems?: boolean; appliedCoupons?: Coupon[]; } ``` --- # Cart Quick Start The Cart drop-in is one of the most commonly used components in the Commerce boilerplate. It provides a complete shopping cart experience with features like product management, coupon codes, gift cards, and shipping estimates. Version: 3.3.0 ## Quick example The Cart drop-in is included in the https://github.com/hlxsites/aem-boilerplate-commerce. This example shows the basic pattern: ```js // 1. Import initializer (handles all setup) // 2. Import the container you need // 3. Import the provider // 4. Render in your block export default async function decorate(block) { await provider.render(CartSummaryGrid, { // Configuration options - see Containers page })(block); } ``` **New to drop-ins?** See the [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/) guide for complete step-by-step instructions. ## Quick reference **Import paths:** - Initializer: `import '../../scripts/initializers/cart.js'` - Containers: `import ContainerName from '@dropins/storefront-cart/containers/ContainerName.js'` - Provider: `import { render } from '@dropins/storefront-cart/render.js'` **Package:** `@dropins/storefront-cart` **Version:** 3.3.0 (verify compatibility with your Commerce instance) **Example container:** `CartSummaryGrid` ## Learn more - [Containers](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/containers/) - Available UI components and configuration options - [Initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/initialization/) - Customize initializer settings and data models - [Functions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/functions/) - Control drop-in behavior programmatically - [Events](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/) - Listen to and respond to drop-in state changes - [Slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/slots/) - Extend containers with custom content --- # Cart Slots The Cart drop-in exposes slots for customizing specific UI sections. Use slots to replace or extend container components. For default properties available to all slots, see [Extending drop-in components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/extending/). Version: 3.3.0 > **Slot usage best practice** Do not use context methods inside other context methods (for example, `appendChild()` inside `onChange()`). See [Slots best practices](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/slots/#best-practice-for-dynamic-slot-content) for details and examples. | Container | Slots | |-----------|-------| | [`CartSummaryGrid`](#cartsummarygrid-slots) | `Thumbnail` | | [`CartSummaryList`](#cartsummarylist-slots) | `Heading`, `EmptyCart`, `Footer`, `RowTotalFooter`, `Thumbnail`, `ProductAttributes`, `CartSummaryFooter`, `CartItem`, `UndoBanner`, `ConfirmDeleteBanner`, `ItemTitle`, `ItemPrice`, `ItemQuantity`, `ItemTotal`, `ItemSku`, `ItemRemoveAction` | | [`CartSummaryTable`](#cartsummarytable-slots) | `Item`, `Price`, `Quantity`, `Subtotal`, `Thumbnail`, `ProductTitle`, `Sku`, `Configurations`, `ItemAlert`, `ItemWarning`, `Actions`, `UndoBanner`, `EmptyCart` | | [`GiftOptions`](#giftoptions-slots) | `SwatchImage` | | [`MiniCart`](#minicart-slots) | `ProductList`, `ProductListFooter`, `PreCheckoutSection`, `Thumbnail`, `Heading`, `EmptyCart`, `Footer`, `RowTotalFooter`, `ProductAttributes`, `CartSummaryFooter`, `CartItem`, `UndoBanner`, `ConfirmDeleteBanner`, `ItemTitle`, `ItemPrice`, `ItemQuantity`, `ItemTotal`, `ItemSku`, `ItemRemoveAction` | | [`OrderSummary`](#ordersummary-slots) | `EstimateShipping`, `Coupons`, `GiftCards` | ## CartSummaryGrid slots The slots for the `CartSummaryGrid` container allow you to customize its appearance and behavior. ```typescript interface CartSummaryGridProps { slots?: { Thumbnail?: SlotProps<{ item: CartModel['items'][number], defaultImageProps: ImageProps }>; }; } ``` ### Thumbnail slot The Thumbnail slot allows you to customize the thumbnail section of the `CartSummaryGrid` container. #### Example ```js await provider.render(CartSummaryGrid, { slots: { Thumbnail: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Thumbnail'; ctx.appendChild(element); } } })(block); ``` ## CartSummaryList slots The slots for the `CartSummaryList` container allow you to customize its appearance and behavior. ```typescript interface CartSummaryListProps { slots?: { Heading?: SlotProps; EmptyCart?: SlotProps; Footer?: SlotProps; Thumbnail?: SlotProps<{ item: CartModel['items'][number]; defaultImageProps: ImageProps; }>; ProductAttributes?: SlotProps; RowTotalFooter?: SlotProps<{ item: CartModel['items'][number] }>; CartSummaryFooter?: SlotProps; CartItem?: SlotProps; UndoBanner?: SlotProps<{ item: CartModel['items'][0]; loading: boolean; error?: string; onUndo: () => void; onDismiss: () => void; }>; ConfirmDeleteBanner?: SlotProps<{ item: CartModel['items'][number]; onConfirm: () => void; onCancel: () => void; }>; ItemTitle?: SlotProps<{ item: CartModel['items'][number] }>; ItemPrice?: SlotProps<{ item: CartModel['items'][number] }>; ItemQuantity?: SlotProps<{ item: CartModel['items'][number]; enableUpdateItemQuantity: boolean; handleItemQuantityUpdate: ( item: CartModel['items'][number], quantity: number ) => void; itemsLoading: Set; handleItemsError: (uid: string, message?: string) => void; handleItemsLoading: (uid: string, state: boolean) => void; onItemUpdate?: ({ item }: { item: CartModel['items'][number] }) => void; }>; ItemTotal?: SlotProps<{ item: CartModel['items'][number] }>; ItemSku?: SlotProps<{ item: CartModel['items'][number] }>; ItemRemoveAction?: SlotProps<{ item: CartModel['items'][number]; enableRemoveItem: boolean; handleItemQuantityUpdate: ( item: CartModel['items'][number], quantity: number ) => void; handleItemsError: (uid: string, message?: string) => void; handleItemsLoading: (uid: string, state: boolean) => void; onItemUpdate?: ({ item }: { item: CartModel['items'][number] }) => void; itemsLoading: Set; }>; }; } ``` ### Heading slot The Heading slot allows you to customize the heading section of the `CartSummaryList` container. #### Example ```js await provider.render(CartSummaryList, { slots: { Heading: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Heading'; ctx.appendChild(element); } } })(block); ``` ### EmptyCart slot The `EmptyCart` slot allows you to customize the empty cart section of the `CartSummaryList` container. #### Example ```js await provider.render(CartSummaryList, { slots: { EmptyCart: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom EmptyCart'; ctx.appendChild(element); } } })(block); ``` ### Footer slot The Footer slot allows you to customize the footer section of the `CartSummaryList` container. #### Example ```js await provider.render(CartSummaryList, { slots: { Footer: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Footer'; ctx.appendChild(element); } } })(block); ``` ### RowTotalFooter slot The `RowTotalFooter` slot lets you show custom content beneath each cart item’s total price. Use it to display promotions, special offers, or other relevant information based on your business logic. #### Context The slot receives the following context: | Property | Type | Description | |----------|------|-------------| | `item` | `CartModel['items'][number]` | The cart item data for the current row | #### Example ```js await provider.render(CartSummaryList, { slots: { RowTotalFooter: (ctx) => { // Display a promotional message based on item data const promoMessage = document.createElement('div'); promoMessage.style.color = 'var(--color-positive-500)'; promoMessage.style.fontSize = '0.875rem'; promoMessage.innerText = 'Special offer applied!'; ctx.appendChild(promoMessage); } } })(block); ``` #### Example with conditional content ```js await provider.render(CartSummaryList, { slots: { RowTotalFooter: (ctx) => { // Only show message for discounted items if (ctx.item.discounted) { const savings = document.createElement('span'); savings.style.color = 'var(--color-alert-800)'; savings.innerText = 'You saved on this item!'; ctx.appendChild(savings); } } } })(block); ``` ### Thumbnail slot The Thumbnail slot allows you to customize the thumbnail section of the `CartSummaryList` container. #### Example ```js await provider.render(CartSummaryList, { slots: { Thumbnail: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Thumbnail'; ctx.appendChild(element); } } })(block); ``` ### ProductAttributes slot The `ProductAttributes` slot allows you to customize the product attributes section of the `CartSummaryList` container. #### Example ```js await provider.render(CartSummaryList, { slots: { ProductAttributes: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ProductAttributes'; ctx.appendChild(element); } } })(block); ``` ### CartSummaryFooter slot The `CartSummaryFooter` slot allows you to customize the cart summary footer section of the `CartSummaryList` container. #### Example ```js await provider.render(CartSummaryList, { slots: { CartSummaryFooter: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom CartSummaryFooter'; ctx.appendChild(element); } } })(block); ``` ### CartItem slot The `CartItem` slot allows you to customize the cart item section of the `CartSummaryList` container. #### Example ```js await provider.render(CartSummaryList, { slots: { CartItem: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom CartItem'; ctx.appendChild(element); } } })(block); ``` ### ConfirmDeleteBanner slot The `ConfirmDeleteBanner` slot lets you replace the default confirmation banner that appears when `confirmBeforeDelete={true}` and a shopper reduces an item's quantity to 0 or clicks the remove button. Use it to customize the banner's appearance, message text, and action buttons. #### Context The slot receives the following context: | Property | Type | Description | |----------|------|-------------| | `item` | `CartModel['items'][number]` | The cart item pending deletion. | | `onConfirm` | `() => void` | Call to confirm deletion and trigger the remove API. | | `onCancel` | `() => void` | Call to dismiss the banner and restore the item row with its original quantity. | #### Example ```js await provider.render(CartSummaryList, { confirmBeforeDelete: true, slots: { ConfirmDeleteBanner: (ctx) => { const banner = document.createElement('div'); banner.innerHTML = ` Remove "${ctx.item.name}" from your cart? `; banner.querySelector('#confirm').addEventListener('click', ctx.onConfirm); banner.querySelector('#cancel').addEventListener('click', ctx.onCancel); ctx.appendChild(banner); } } })(block); ``` ### ItemTitle slot The `ItemTitle` slot allows you to customize the item title section of the `CartSummaryList` container. #### Example ```js await provider.render(CartSummaryList, { slots: { ItemTitle: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ItemTitle'; ctx.appendChild(element); } } })(block); ``` ### ItemPrice slot The `ItemPrice` slot allows you to customize the item price section of the `CartSummaryList` container. #### Example ```js await provider.render(CartSummaryList, { slots: { ItemPrice: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ItemPrice'; ctx.appendChild(element); } } })(block); ``` ### ItemTotal slot The `ItemTotal` slot allows you to customize the item total section of the `CartSummaryList` container. #### Example ```js await provider.render(CartSummaryList, { slots: { ItemTotal: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ItemTotal'; ctx.appendChild(element); } } })(block); ``` ### ItemSku slot The `ItemSku` slot allows you to customize the item sku section of the `CartSummaryList` container. #### Example ```js await provider.render(CartSummaryList, { slots: { ItemSku: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ItemSku'; ctx.appendChild(element); } } })(block); ``` ## CartSummaryTable slots The slots for the `CartSummaryTable` container allow you to customize its appearance and behavior. ```typescript interface CartSummaryTableProps { slots?: { Item?: SlotProps<{ item: CartModel['items'][number] }>; Price?: SlotProps<{ item: CartModel['items'][number] }>; Quantity?: SlotProps<{ item: CartModel['items'][number]; isUpdating: boolean; quantityInputValue: number; handleInputChange: (e: Event) => void; itemUpdateErrors: Map; }>; Subtotal?: SlotProps<{ item: CartModel['items'][number] }>; Thumbnail?: SlotProps<{ item: CartModel['items'][number]; defaultImageProps: ImageProps; index: number; }>; ProductTitle?: SlotProps<{ item: CartModel['items'][number] }>; Sku?: SlotProps<{ item: CartModel['items'][number] }>; Configurations?: SlotProps<{ item: CartModel['items'][number] }>; ItemAlert?: SlotProps<{ item: CartModel['items'][number] }>; ItemWarning?: SlotProps<{ item: CartModel['items'][number] }>; Actions?: SlotProps<{ item: CartModel['items'][number]; itemsUpdating: Map; setItemUpdating: (uid: string, state: boolean) => void; setItemUpdateError: (uid: string, error: string) => void; }>; UndoBanner?: SlotProps<{ item: CartModel['items'][number]; loading: boolean; error?: string; onUndo: () => void; onDismiss: () => void; }>; EmptyCart?: SlotProps; }; } ``` ## GiftOptions slots The slots for the `GiftOptions` container allow you to customize its appearance and behavior. ```typescript interface GiftOptionsProps { slots?: { SwatchImage?: SlotProps<{ item: Item | ProductGiftOptionsConfig imageSwatchContext: ImageNodeRenderProps['imageSwatchContext'] defaultImageProps: ImageProps }>; }; } ``` ### SwatchImage slot The `SwatchImage` slot allows you to customize the swatch image section of the `GiftOptions` container. #### Example ```js await provider.render(GiftOptions, { slots: { SwatchImage: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom SwatchImage'; ctx.appendChild(element); } } })(block); ``` ## MiniCart slots The slots for the `MiniCart` container allow you to customize its appearance and behavior. ```typescript interface MiniCartProps { slots?: { ProductList?: SlotProps; ProductListFooter?: SlotProps; PreCheckoutSection?: SlotProps; Thumbnail?: SlotProps<{ item: CartModel['items'][number]; defaultImageProps: ImageProps; }>; Heading?: SlotProps; EmptyCart?: SlotProps; Footer?: SlotProps; ProductAttributes?: SlotProps; RowTotalFooter?: SlotProps<{ item: CartModel['items'][number] }>; CartSummaryFooter?: SlotProps; CartItem?: SlotProps; UndoBanner?: SlotProps<{ item: CartModel['items'][0]; loading: boolean; error?: string; onUndo: () => void; onDismiss: () => void; }>; ConfirmDeleteBanner?: SlotProps<{ item: CartModel['items'][number]; onConfirm: () => void; onCancel: () => void; }>; ItemTitle?: SlotProps<{ item: CartModel['items'][number] }>; ItemPrice?: SlotProps<{ item: CartModel['items'][number] }>; ItemQuantity?: SlotProps<{ item: CartModel['items'][number]; enableUpdateItemQuantity: boolean; handleItemQuantityUpdate: ( item: CartModel['items'][number], quantity: number ) => void; itemsLoading: Set; handleItemsError: (uid: string, message?: string) => void; handleItemsLoading: (uid: string, state: boolean) => void; onItemUpdate?: ({ item }: { item: CartModel['items'][number] }) => void; }>; ItemTotal?: SlotProps<{ item: CartModel['items'][number] }>; ItemSku?: SlotProps<{ item: CartModel['items'][number] }>; ItemRemoveAction?: SlotProps<{ item: CartModel['items'][number]; enableRemoveItem: boolean; handleItemQuantityUpdate: ( item: CartModel['items'][number], quantity: number ) => void; handleItemsError: (uid: string, message?: string) => void; handleItemsLoading: (uid: string, state: boolean) => void; onItemUpdate?: ({ item }: { item: CartModel['items'][number] }) => void; itemsLoading: Set; }>; }; } ``` ### ProductList slot The `ProductList` slot allows you to customize the product list section of the `MiniCart` container. #### Example ```js await provider.render(MiniCart, { slots: { ProductList: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ProductList'; ctx.appendChild(element); } } })(block); ``` ### ProductListFooter slot The `ProductListFooter` slot allows you to customize the product list footer section of the `MiniCart` container. #### Example ```js await provider.render(MiniCart, { slots: { ProductListFooter: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ProductListFooter'; ctx.appendChild(element); } } })(block); ``` ### PreCheckoutSection slot The `PreCheckoutSection` slot allows you to customize the pre-checkout section of the `MiniCart` container. #### Example ```js await provider.render(MiniCart, { slots: { PreCheckoutSection: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom PreCheckoutSection'; ctx.appendChild(element); } } })(block); ``` ### Thumbnail slot The Thumbnail slot allows you to customize the thumbnail section of the `MiniCart` container. #### Example ```js await provider.render(MiniCart, { slots: { Thumbnail: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Thumbnail'; ctx.appendChild(element); } } })(block); ``` ### Heading slot The Heading slot allows you to customize the heading section of the `MiniCart` container. #### Example ```js await provider.render(MiniCart, { slots: { Heading: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Heading'; ctx.appendChild(element); } } })(block); ``` ### EmptyCart slot The `EmptyCart` slot allows you to customize the empty cart section of the `MiniCart` container. #### Example ```js await provider.render(MiniCart, { slots: { EmptyCart: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom EmptyCart'; ctx.appendChild(element); } } })(block); ``` ### Footer slot The Footer slot allows you to customize the footer section of the `MiniCart` container. #### Example ```js await provider.render(MiniCart, { slots: { Footer: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Footer'; ctx.appendChild(element); } } })(block); ``` ### RowTotalFooter slot The RowTotalFooter slot lets you show custom content beneath each cart item’s total price. Use it to display promotions, special offers, or other relevant information based on your business logic. #### Context The slot receives the following context: | Property | Type | Description | |----------|------|-------------| | `item` | `CartModel['items'][number]` | The cart item data for the current row | #### Example ```js await provider.render(MiniCart, { slots: { RowTotalFooter: (ctx) => { // Display a promotional message based on item data const promoMessage = document.createElement('div'); promoMessage.style.color = 'var(--color-positive-500)'; promoMessage.style.fontSize = '0.875rem'; promoMessage.innerText = 'Special offer applied!'; ctx.appendChild(promoMessage); } } })(block); ``` ### ProductAttributes slot The `ProductAttributes` slot allows you to customize the product attributes section of the `MiniCart` container. #### Example ```js await provider.render(MiniCart, { slots: { ProductAttributes: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ProductAttributes'; ctx.appendChild(element); } } })(block); ``` ### CartSummaryFooter slot The `CartSummaryFooter` slot allows you to customize the cart summary footer section of the `MiniCart` container. #### Example ```js await provider.render(MiniCart, { slots: { CartSummaryFooter: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom CartSummaryFooter'; ctx.appendChild(element); } } })(block); ``` ### CartItem slot The `CartItem` slot allows you to customize the cart item section of the `MiniCart` container. #### Example ```js await provider.render(MiniCart, { slots: { CartItem: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom CartItem'; ctx.appendChild(element); } } })(block); ``` ### ConfirmDeleteBanner slot The `ConfirmDeleteBanner` slot lets you replace the default confirmation banner that appears when `confirmBeforeDelete={true}` and a shopper clicks the remove button. Use it to customize the banner's appearance, message text, and action buttons. #### Context The slot receives the following context: | Property | Type | Description | |----------|------|-------------| | `item` | `CartModel['items'][number]` | The cart item pending deletion. | | `onConfirm` | `() => void` | Call to confirm deletion and trigger the remove API. | | `onCancel` | `() => void` | Call to dismiss the banner and restore the item row with its original quantity. | #### Example ```js await provider.render(MiniCart, { confirmBeforeDelete: true, slots: { ConfirmDeleteBanner: (ctx) => { const banner = document.createElement('div'); banner.innerHTML = ` Remove "${ctx.item.name}" from your cart? `; banner.querySelector('#confirm').addEventListener('click', ctx.onConfirm); banner.querySelector('#cancel').addEventListener('click', ctx.onCancel); ctx.appendChild(banner); } } })(block); ``` ### ItemTitle slot The `ItemTitle` slot allows you to customize the item title section of the `MiniCart` container. #### Example ```js await provider.render(MiniCart, { slots: { ItemTitle: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ItemTitle'; ctx.appendChild(element); } } })(block); ``` ### ItemPrice slot The `ItemPrice` slot allows you to customize the item price section of the `MiniCart` container. #### Example ```js await provider.render(MiniCart, { slots: { ItemPrice: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ItemPrice'; ctx.appendChild(element); } } })(block); ``` ### ItemTotal slot The `ItemTotal` slot allows you to customize the item total section of the `MiniCart` container. #### Example ```js await provider.render(MiniCart, { slots: { ItemTotal: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ItemTotal'; ctx.appendChild(element); } } })(block); ``` ### ItemSku slot The `ItemSku` slot allows you to customize the item sku section of the `MiniCart` container. #### Example ```js await provider.render(MiniCart, { slots: { ItemSku: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ItemSku'; ctx.appendChild(element); } } })(block); ``` ## OrderSummary slots The slots for the `OrderSummary` container allow you to customize its appearance and behavior. ```typescript interface OrderSummaryProps { slots?: { EstimateShipping?: SlotProps; Coupons?: SlotProps; GiftCards?: SlotProps; }; } ``` ### EstimateShipping slot The `EstimateShipping` slot allows you to customize the estimate shipping section of the `OrderSummary` container. #### Example ```js await provider.render(OrderSummary, { slots: { EstimateShipping: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom EstimateShipping'; ctx.appendChild(element); } } })(block); ``` ### Coupons slot The Coupons slot allows you to customize the coupons section of the `OrderSummary` container. #### Example ```js await provider.render(OrderSummary, { slots: { Coupons: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Coupons'; ctx.appendChild(element); } } })(block); ``` ### GiftCards slot The `GiftCards` slot allows you to customize the gift cards section of the `OrderSummary` container. #### Example ```js await provider.render(OrderSummary, { slots: { GiftCards: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom GiftCards'; ctx.appendChild(element); } } })(block); ``` --- # Cart styles Customize the Cart drop-in using CSS classes and design tokens. This page covers the Cart-specific container classes and customization examples. For comprehensive information about design tokens, responsive breakpoints, and styling best practices, see [Styling Drop-In Components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/). Version: 3.3.0 ## Customization example Add this to https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/commerce-cart/commerce-cart.css to customize the Cart drop-in. For a complete list of available design tokens (colors, spacing, typography, and more), see the [Design tokens reference](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/#design-tokens-reference). ```css title="styles/styles.css" del={2-3} ins={4-5} .cart-estimate-shipping { gap: var(--spacing-xsmall); color: var(--color-neutral-800); gap: var(--spacing-small); color: var(--color-brand-800); } ``` ## Container classes The Cart drop-in uses BEM-style class naming. Use the browser DevTools to inspect elements and find specific class names. ```css /* CartSummaryGrid */ .cart-cart-summary-grid {} .cart-cart-summary-grid__content {} .cart-cart-summary-grid__content--empty {} .cart-cart-summary-grid__empty-cart {} .cart-cart-summary-grid__item-container {} /* CartSummaryList */ .cart-cart-summary-list {} .cart-cart-summary-list--include-out-of-stock {} .cart-cart-summary-list-accordion {} .cart-cart-summary-list-accordion__section {} .cart-cart-summary-list-footer__action {} .cart-cart-summary-list__background--secondary {} .cart-cart-summary-list__content {} .cart-cart-summary-list__content--empty {} .cart-cart-summary-list__empty-cart {} .cart-cart-summary-list__heading {} .cart-cart-summary-list__heading--full-width {} .cart-cart-summary-list__heading-divider {} .cart-cart-summary-list__out-of-stock-message {} .dropin-cart-item__quantity {} /* CartSummaryTable */ .cart-cart-summary-table {} .cart-cart-summary-table__body {} .cart-cart-summary-table__cell-item {} .cart-cart-summary-table__cell-price {} .cart-cart-summary-table__cell-qty {} .cart-cart-summary-table__cell-qty-input {} .cart-cart-summary-table__cell-qty-updater {} .cart-cart-summary-table__cell-qty-updater--disabled {} .cart-cart-summary-table__cell-qty-updater--error {} .cart-cart-summary-table__cell-subtotal {} .cart-cart-summary-table__header {} .cart-cart-summary-table__header-price {} .cart-cart-summary-table__header-qty {} .cart-cart-summary-table__header-subtotal {} .cart-cart-summary-table__item-actions {} .cart-cart-summary-table__item-footer {} .cart-cart-summary-table__item-price {} .cart-cart-summary-table__item-price-tax-label {} .cart-cart-summary-table__item-subtotal {} .cart-cart-summary-table__item-subtotal-tax-label {} .cart-cart-summary-table__mobile-label {} .cart-cart-summary-table__row {} .cart-cart-summary-table__row--error {} .cart-cart-summary-table__row--updating {} .cart-cart-summary-table__skeleton {} .elsie-skeleton-row {} /* Item */ .cart-cart-summary-table__item {} .cart-cart-summary-table__item-configuration {} .cart-cart-summary-table__item-configuration-label {} .cart-cart-summary-table__item-configuration-value {} .cart-cart-summary-table__item-configurations {} .cart-cart-summary-table__item-details {} .cart-cart-summary-table__item-image-wrapper {} .cart-cart-summary-table__item-name {} .cart-cart-summary-table__item-qty {} .cart-cart-summary-table__item-quantity-alert-icon {} .cart-cart-summary-table__item-quantity-alert-text {} .cart-cart-summary-table__item-quantity-alert-wrapper {} .cart-cart-summary-table__item-quantity-warning-icon {} .cart-cart-summary-table__item-quantity-warning-text {} .cart-cart-summary-table__item-quantity-warning-wrapper {} .cart-cart-summary-table__item-remove-button {} .cart-cart-summary-table__sku {} /* Coupons */ .cart-coupons__accordion-section {} .cart-gift-cards {} .coupon-code-form__action {} .coupon-code-form__applied {} .coupon-code-form__applied-item {} .coupon-code-form__codes {} .coupon-code-form__error {} .dropin-accordion-section__content-container {} .dropin-accordion-section__title-container {} .dropin-input-container {} .dropin-tag-container {} /* EmptyCart */ .cart-empty-cart {} .cart-empty-cart__wrapper {} .dropin-card {} .dropin-card--secondary {} /* EstimateShipping */ .cart-estimate-shipping {} .cart-estimate-shipping--edit {} .cart-estimate-shipping--hide {} .cart-estimate-shipping--loading {} .cart-estimate-shipping--state {} .cart-estimate-shipping--zip {} .cart-estimate-shippingLink {} .cart-estimate-shipping__caption {} .cart-estimate-shipping__label {} .cart-estimate-shipping__label--bold {} .cart-estimate-shipping__label--muted {} .cart-estimate-shipping__link {} .cart-estimate-shipping__price {} .cart-estimate-shipping__price--bold {} .cart-estimate-shipping__price--muted {} /* GiftOptions */ .cart-gift-options-readonly__checkboxes {} .cart-gift-options-readonly__form {} .cart-gift-options-readonly__header {} .cart-gift-options-view {} .cart-gift-options-view--loading {} .cart-gift-options-view--order {} .cart-gift-options-view--product {} .cart-gift-options-view--readonly {} .cart-gift-options-view__field-gift-wrap {} .cart-gift-options-view__footer {} .cart-gift-options-view__icon--success {} .cart-gift-options-view__modal {} .cart-gift-options-view__modal-content {} .cart-gift-options-view__modal-grid {} .cart-gift-options-view__modal-wrapper {} .cart-gift-options-view__spinner {} .cart-gift-options-view__top {} .cart-gift-options-view__top--hidden {} .dropin-accordion-section__content-container {} .dropin-accordion-section__flex {} .dropin-accordion-section__heading {} .dropin-accordion-section__title {} .dropin-accordion-section__title-container {} .dropin-button {} .dropin-card {} .dropin-card--primary {} .dropin-card__content {} .dropin-checkbox__label {} .dropin-checkbox__label--medium {} .dropin-content-grid {} .dropin-content-grid__content {} .dropin-divider {} .dropin-field {} .dropin-iconButton {} .dropin-modal {} .dropin-modal--dim {} .dropin-modal__body--centered {} .dropin-modal__content {} .dropin-modal__header {} .dropin-modal__header-title {} .dropin-modal__header-title-content {} .dropin-price {} .dropin-textarea {} .dropin-textarea--error {} .dropin-textarea__label--floating {} .dropin-textarea__label--floating--error {} .dropin-textarea__label--floating--text {} /* MiniCart */ .cart-cart-summary-list__heading {} .cart-mini-cart {} .cart-mini-cart__empty-cart {} .cart-mini-cart__footer {} .cart-mini-cart__footer__ctas {} .cart-mini-cart__footer__estimated-total {} .cart-mini-cart__footer__estimated-total-excluding-taxes {} .cart-mini-cart__heading {} .cart-mini-cart__heading-divider {} .cart-mini-cart__preCheckoutSection {} .cart-mini-cart__productListFooter {} .cart-mini-cart__products {} .dropin-cart-item__configurations {} /* OrderSummary */ .cart-order-summary {} .cart-order-summary--loading {} .cart-order-summary__applied-gift-cards {} .cart-order-summary__caption {} .cart-order-summary__content {} .cart-order-summary__coupon__code {} .cart-order-summary__coupons {} .cart-order-summary__discount {} .cart-order-summary__divider-primary {} .cart-order-summary__divider-secondary {} .cart-order-summary__entry {} .cart-order-summary__gift-cards {} .cart-order-summary__heading {} .cart-order-summary__label {} .cart-order-summary__price {} .cart-order-summary__primary {} .cart-order-summary__primaryAction {} .cart-order-summary__secondary {} .cart-order-summary__shipping--edit {} .cart-order-summary__shipping--hide {} .cart-order-summary__shipping--state {} .cart-order-summary__shipping--zip {} .cart-order-summary__shippingLink {} .cart-order-summary__spinner {} .cart-order-summary__taxEntry {} .cart-order-summary__taxes {} .cart-order-summary__total {} .dropin-accordion {} .dropin-accordion-section__content-container {} .dropin-divider {} /* OrderSummaryLine */ .cart-order-summary__label {} .cart-order-summary__label--bold {} .cart-order-summary__label--muted {} .cart-order-summary__price {} .cart-order-summary__price--bold {} .cart-order-summary__price--muted {} ``` --- # Add messages to mini cart This tutorial shows you how to add inline and overlay feedback messages that appear in the mini cart when products are added or updated to the cart. These messages provide visual feedback to shoppers about their cart actions. Inline messages appear at the top of the mini cart for a brief period (three seconds by default) and then automatically disappear, providing immediate feedback to users about their cart actions. ![Minicart inline message](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/minicart-inline.png) *Minicart inline message* Overlay messages are displayed at the top center of the mini cart with a semi-transparent background when the same events occur. ![Minicart overlay message](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/minicart-overlay.png) *Minicart overlay message* You can customize the appearance and behavior of the inline and overlay messages by modifying the following: - **Message text**: Update the translations in the content placeholders sheet under the `Cart.MiniCart.Message` namespace. - **Message styling**: Modify the CSS classes in `commerce-mini-cart.css`. The styles use design tokens (prefixed with `--`) to maintain consistency with the design system. Overlays can be customized as follows: - Background opacity using the alpha value in the overlay's `background-color` (default is 50%) - Message position using the `top`, `left`, and `transform` properties - Colors, spacing, shadows, and other visual properties using design tokens - **Message position**: For inline messages, change where the message appears in the mini cart by modifying the insertion point in the DOM. - **Display duration**: Change the timeout value in the `showMessage` function (default is 3000ms). ## Prerequisites Before implementing inline messages, ensure you have: - Access to the content folder to manage message localization through placeholders. - Understanding of the design system tokens used in the Commerce boilerplate template. - The `commerce-mini-cart.css` file in your `blocks/commerce-mini-cart/` directory. ## Events The inline and overlay messages respond to two cart events: - `cart/product/added`: Triggered when products are added to the cart - `cart/product/updated`: Triggered when products in the cart are updated ## Implementation To add inline or overlay messages to your mini cart, follow these steps: ### 1. Retrieve translations for message texts using placeholders Get translations for custom messages from the content folder. ```javascript const placeholders = await fetchPlaceholders(); // Access the message texts from the Cart.MiniCart.Message namespace const MESSAGES = { ADDED: placeholders?.Cart?.MiniCart?.Message?.added, UPDATED: placeholders?.Cart?.MiniCart?.Message?.updated, }; ``` ### 2. Create the appropriate message containers Inline messages require a container for the update message and a shadow wrapper to display the message. Overlay messages require an overlay container and a message container. ### Inline messages ```javascript // Create a container for the update message const updateMessage = document.createElement('div'); updateMessage.className = 'commerce-mini-cart__update-message'; // Create a shadow wrapper const shadowWrapper = document.createElement('div'); shadowWrapper.className = 'commerce-mini-cart__message-wrapper'; shadowWrapper.appendChild(updateMessage); ``` ### Overlay messages ```javascript // Create an overlay container const overlay = document.createElement('div'); overlay.className = 'commerce-mini-cart__overlay'; // Create a message container const messageContainer = document.createElement('div'); messageContainer.className = 'commerce-mini-cart__message'; overlay.appendChild(messageContainer); ``` ### 3. Create a function to show and hide messages Create a function that displays the message in the container and then hides it after a specified duration, such as three seconds. ### Inline messages ```javascript const showMessage = (message) => { updateMessage.textContent = message; updateMessage.classList.add('commerce-mini-cart__update-message--visible'); shadowWrapper.classList.add('commerce-mini-cart__message-wrapper--visible'); setTimeout(() => { updateMessage.classList.remove('commerce-mini-cart__update-message--visible'); shadowWrapper.classList.remove('commerce-mini-cart__message-wrapper--visible'); }, 3000); }; ``` ### Overlay messages ```javascript const showMessage = (message) => { messageContainer.textContent = message; overlay.classList.add('commerce-mini-cart__overlay--visible'); setTimeout(() => { overlay.classList.remove('commerce-mini-cart__overlay--visible'); }, 3000); }; ``` ### 4. Add event listeners for cart updates Listen for the `cart/product/added` and `cart/product/updated` events and display the appropriate message. ```javascript events.on('cart/product/added', () => showMessage(MESSAGES.ADDED), { eager: true, }); events.on('cart/product/updated', () => showMessage(MESSAGES.UPDATED), { eager: true, }); ``` ### 5. Insert the message container into the mini cart block Add the message container to the mini cart block to display the messages. ### Inline messages ```javascript // Find the products container and add the message div at the top const productsContainer = block.querySelector('.cart-mini-cart__products'); if (productsContainer) { productsContainer.insertBefore(shadowWrapper, productsContainer.firstChild); } else { console.info('Products container not found, appending message to block'); block.appendChild(shadowWrapper); } ``` ### Overlay messages ```javascript block.appendChild(overlay); ``` ### 6. Update the CSS styles Add styles to your `commerce-mini-cart.css` file. ### Inline messages ```css .commerce-mini-cart__update-message { display: none; font: var(--type-body-2-default-font); letter-spacing: var(--type-body-2-default-letter-spacing); } .commerce-mini-cart__message-wrapper { background-color: var(--color-positive-200); border-radius: var(--shape-border-radius-1); padding: var(--spacing-xsmall); display: none; margin-bottom: var(--spacing-small); } .commerce-mini-cart__message-wrapper--visible, .commerce-mini-cart__update-message--visible { display: block; } ``` ### Overlay messages ```css .commerce-mini-cart__overlay { background-color: rgb(0 0 0 / 50%); display: none; position: absolute; inset: 0; z-index: 1000; border-radius: var(--shape-border-radius-1); } .commerce-mini-cart__message { background-color: var(--color-positive-200); border-radius: var(--shape-border-radius-1); padding: var(--spacing-small); position: absolute; top: var(--spacing-medium); left: 50%; transform: translateX(-50%); font: var(--type-body-2-default-font); letter-spacing: var(--type-body-2-default-letter-spacing); box-shadow: var(--shape-shadow-3); width: 90%; max-width: 400px; text-align: center; } .commerce-mini-cart__overlay--visible { display: block; } ``` ## Complete example Here's a complete example of implementing inline and overlay messages in your `commerce-mini-cart.js` block file: ### Inline messages ```javascript // Initializers export default async function decorate(block) { const { 'start-shopping-url': startShoppingURL = '', 'cart-url': cartURL = '', 'checkout-url': checkoutURL = '', } = readBlockConfig(block); // Get translations for custom messages const placeholders = await fetchPlaceholders(); const MESSAGES = { ADDED: placeholders?.Cart?.MiniCart?.Message?.added, UPDATED: placeholders?.Cart?.MiniCart?.Message?.updated, }; // Create a container for the update message const updateMessage = document.createElement('div'); updateMessage.className = 'commerce-mini-cart__update-message'; // Create shadow wrapper const shadowWrapper = document.createElement('div'); shadowWrapper.className = 'commerce-mini-cart__message-wrapper'; shadowWrapper.appendChild(updateMessage); const showMessage = (message) => { updateMessage.textContent = message; updateMessage.classList.add('commerce-mini-cart__update-message--visible'); shadowWrapper.classList.add('commerce-mini-cart__message-wrapper--visible'); setTimeout(() => { updateMessage.classList.remove('commerce-mini-cart__update-message--visible'); shadowWrapper.classList.remove('commerce-mini-cart__message-wrapper--visible'); }, 3000); }; // Add event listeners for cart updates events.on('cart/product/added', () => showMessage(MESSAGES.ADDED), { eager: true, }); events.on('cart/product/updated', () => showMessage(MESSAGES.UPDATED), { eager: true, }); block.innerHTML = ''; // Render MiniCart first await provider.render(MiniCart, { routeEmptyCartCTA: startShoppingURL ? () => rootLink(startShoppingURL) : undefined, routeCart: cartURL ? () => rootLink(cartURL) : undefined, routeCheckout: checkoutURL ? () => rootLink(checkoutURL) : undefined, routeProduct: (product) => rootLink(`/products/${product.url.urlKey}/${product.topLevelSku}`), })(block); // Find the products container and add the message div at the top const productsContainer = block.querySelector('.cart-mini-cart__products'); if (productsContainer) { productsContainer.insertBefore(shadowWrapper, productsContainer.firstChild); } else { console.info('Products container not found, appending message to block'); block.appendChild(shadowWrapper); } return block; } ``` ### Overlay messages ```javascript // Initializers export default async function decorate(block) { const { 'start-shopping-url': startShoppingURL = '', 'cart-url': cartURL = '', 'checkout-url': checkoutURL = '', } = readBlockConfig(block); // Get translations for custom messages const placeholders = await fetchPlaceholders(); const MESSAGES = { ADDED: placeholders?.Cart?.MiniCart?.Message?.added, UPDATED: placeholders?.Cart?.MiniCart?.Message?.updated, }; block.innerHTML = ''; // Render MiniCart first await provider.render(MiniCart, { routeEmptyCartCTA: startShoppingURL ? () => rootLink(startShoppingURL) : undefined, routeCart: cartURL ? () => rootLink(cartURL) : undefined, routeCheckout: checkoutURL ? () => rootLink(checkoutURL) : undefined, routeProduct: (product) => rootLink(`/products/${product.url.urlKey}/${product.topLevelSku}`), })(block); // Create overlay container const overlay = document.createElement('div'); overlay.className = 'commerce-mini-cart__overlay'; // Create message container const messageContainer = document.createElement('div'); messageContainer.className = 'commerce-mini-cart__message'; overlay.appendChild(messageContainer); block.appendChild(overlay); const showMessage = (message) => { messageContainer.textContent = message; overlay.classList.add('commerce-mini-cart__overlay--visible'); setTimeout(() => { overlay.classList.remove('commerce-mini-cart__overlay--visible'); }, 3000); }; // Add event listeners for cart updates events.on('cart/product/added', () => showMessage(MESSAGES.ADDED), { eager: true, }); events.on('cart/product/updated', () => showMessage(MESSAGES.UPDATED), { eager: true, }); return block; } ``` And here's the accompanying CSS file (`commerce-mini-cart.css`): ### Inline messages ```css .commerce-mini-cart__update-message { display: none; font: var(--type-body-2-default-font); letter-spacing: var(--type-body-2-default-letter-spacing); } .commerce-mini-cart__message-wrapper { background-color: var(--color-positive-200); border-radius: var(--shape-border-radius-1); padding: var(--spacing-xsmall); display: none; margin-bottom: var(--spacing-small); } .commerce-mini-cart__message-wrapper--visible, .commerce-mini-cart__update-message--visible { display: block; } ``` ### Overlay messages ```css .commerce-mini-cart__overlay { background-color: rgb(0 0 0 / 50%); display: none; position: absolute; inset: 0; z-index: 1000; border-radius: var(--shape-border-radius-1); } .commerce-mini-cart__message { background-color: var(--color-positive-200); border-radius: var(--shape-border-radius-1); padding: var(--spacing-small); position: absolute; top: var(--spacing-medium); left: 50%; transform: translateX(-50%); font: var(--type-body-2-default-font); letter-spacing: var(--type-body-2-default-letter-spacing); box-shadow: var(--shape-shadow-3); width: 90%; max-width: 400px; text-align: center; } .commerce-mini-cart__overlay--visible { display: block; } ``` --- # Add custom product lines to the cart summary This tutorial describes how to make the following customizations to the `CartSummaryList` container using the Adobe Commerce Boilerplate: - Add text from a custom product attribute - Display promotional information in the footer of each product in the cart ## Prerequisites This tutorial requires that you create the following entities in the Adobe Commerce Admin: - A custom product attribute. Here, the product attribute is assigned the label `Shipping Notes`, and the **Catalog Input Type for Store Owner* is set to **Text Field**. You can optionally set the **Used for Sorting in Product Listing** option to **Yes** to increase the visibility of products using the product attribute in the Products grid. https://experienceleague.adobe.com/en/docs/commerce-admin/catalog/product-attributes/product-attributes describes how to create a custom product attribute. In addition, you must assign the product attribute to one or more products. In this tutorial, the text fields will contain the strings "These item(s) are available to ship on Nov 1, 2024" and "FINAL SALE: This item ships separately and is ineligible for return.". - A custom cart price rule. In this tutorial, a cart price rule named `25% Off $75+ with Code BOO24` has been created. Its definition defines the coupon code, the discount amount, and the conditions that must be met to apply the discount. https://experienceleague.adobe.com/en/docs/commerce-admin/marketing/promotions/cart-rules/price-rules-cart describes how to create a cart price rule. ## Step-by-step The following steps describe how to modify the https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/commerce-cart/commerce-cart.js block file in the boilerplate template to add custom content to the `CartSummaryList` container. ### 1. Add text from a custom product attribute In this task, we'll add text that provides shipping information when certain conditions apply. For example, an item might be out of stock, and therefore cannot be shipped immediately. Or maybe the product is on clearance and cannot be returned. The `CartSummaryList` component is extended to display text defined by a merchant in the Admin using a custom product attribute. If the custom product attribute is not assigned to a product, then no additional information is displayed. The following images show how these custom lines can be rendered: ![Cart item with custom shipping notification](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/cart-product-line-shipping.png) ** ![Cart item with custom final sale notification](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/cart-product-line-final.png) ** 1. Open the `blocks/commerce-cart/commerce-cart.js` boilerplate file. This file imports the `CartSummaryList` container, and we want to use a slot to display the custom product attribute. Find the `provider.render(CartSummaryList, {` line in the file and insert a `ProductAttributes` slot with the following code: ```javascript slots: { ProductAttributes: (ctx) => { // Prepend Product Attributes const ProductAttributes = ctx.item?.productAttributes; ProductAttributes?.forEach((attr) => { if(attr.code === "shipping_notes") { if(attr.selected_options) { const selectedOptions = attr.selected_options .filter((option) => option.label.trim() !== '') .map((option) => option.label) .join(', '); if(selectedOptions) { const productAttribute = document.createElement('div'); productAttribute.innerText = `${attr.code}: ${selectedOptions}`; ctx.appendChild(productAttribute); } } else if (attr.value) { const productAttribute = document.createElement('div'); productAttribute.innerText = `${attr.code}: ${attr.value}`; ctx.appendChild(productAttribute); } } }) }, ``` This code creates a slot named `ProductAttributes` that displays the custom product attribute `Shipping Notes`, if it is assigned to a product. If the corresponding attribute is found, the slot creates a new `div` element and appends the attribute code and value to the element. The element is then appended to the `ctx` element, which is the product line in the cart summary. 1. Save the file and generate the page to see the changes. ### 2. Display promotional information in the footer of a cart item Now we'll add information defined in a custom cart price rule to the footer of the `CartSummaryList` container. If the conditions set in the cart price rules are not met, then no additional information is displayed. For example, if a specific coupon has not been applied or if the subtotal threshold has not been met, then this information is not displayed. ![Display coupon information](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/cart-product-line-footer.png) *Display coupon information* 1. Add a `Footer` slot beneath the `ProductAttributes` slot. ```javascript slots: { ProductAttributes: (ctx) => { ... } Footer: (ctx) => { // Runs on mount const wrapper = document.createElement('div'); ctx.appendChild(wrapper); // Append Product Promotions on every update ctx.onChange((next) => { wrapper.innerHTML = ''; next.item?.discount?.label?.forEach((label) => { const discount = document.createElement('div'); discount.style.color = '#3d3d3d'; discount.innerText = label; wrapper.appendChild(discount); }); }); }, ``` This code creates a slot named `Footer`, which displays the promotional information defined in the custom cart price rule. If the conditions set in the cart price rule are met, the slot creates a new `div` element and appends the promotional information to the element. The element is then appended to the `ctx` element, which is the product line in the cart summary. 1. Save the file. Add products that total at least $75 and apply the BOO24 coupon code to the cart. The page displays the rule name beneath each item in the cart. --- # Customize the cart summary block This tutorial describes how to make the following customizations to the `CartSummaryList` container using the Adobe Commerce Boilerplate: - Change the product quantity selector to a dropdown menu. - Configure how to display savings. - Configure the savings display from the Cart content document. ## Step-by-step The following steps describe how to modify the https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/commerce-cart/commerce-cart.js block file in the boilerplate template to add custom content to the `CartSummaryList` container. ### 1. Change the product quantity selector to a dropdown menu By default, the product quantity selector is a stepper, as shown below: ![Stepper quantity selector](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/cart-summary-stepper.png) *Stepper quantity selector* In this task, you'll change the quantity selector to a dropdown menu. The dropdown allows shoppers to select a maximum of 20 items. ![Dropdown quantity selector](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/cart-summary-dropdown.png) *Dropdown quantity selector* 1. Navigate to the `blocks/commerce-cart/commerce-cart.js` file and enable the dropdown selector by adding the following lines to the `provider.render(CartSummaryList)` method: ```js quantityType: 'dropdown', dropdownOptions, ``` The `quantityType` property specifies the type of quantity selector to use. The `dropdownOptions` property specifies the values to display in the dropdown. It is defined in the next step. 1. Define the `dropdownOptions` constant at the top of the file, in the `export default async function decorate(block){}` statement. ```js const DROPDOWN_MAX_QUANTITY = 20; const dropdownOptions = Array.from( { length: parseInt(DROPDOWN_MAX_QUANTITY, 10) }, (_, i) => { const quantityOption = i + 1; return { value: `${quantityOption}`, text: `${quantityOption}`, }; } ); ``` This code creates an array of objects with `value` and `text` properties. The `value` property is the quantity value, and the `text` property is the text displayed in the dropdown. 1. Save the file and generate the page to see the changes. ### 2. Display savings as a percentage or a fixed amount In order to encourage shoppers to buy more, you can display the savings they'll get by purchasing more items. You can display the savings on an item that's on sale as a percentage or as a fixed amount. ![Savings expressed as a percentage](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/cart-summary-percentage.png) *Savings expressed as a percentage* ![Savings expressed as a percentage](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/cart-summary-total.png) *Savings expressed as a total* 1. Add the following lines to the `provider.render(CartSummaryList)` method, below the `dropdownOptions,` line: ```js showDiscount: true, //showSavings: true ``` Comment out one of the lines to choose between displaying the discount as a percentage or a fixed amount. 1. Save the file and generate the page to see the changes. ### 3. Configure the savings display from the Cart content document To allow a merchandiser or other non-developer to configure how to display savings values, you need to make more changes to the `commerce-cart.js` file and the relevant content documents. For guidance on starter content and the Sidekick browser extension, review https://experienceleague.adobe.com/developer/commerce/storefront/get-started/. 1. Comment out the savings properties from the `provider.render(CartSummaryList)` method. ```js //showDiscount: true, //showSavings: true ``` 1. Add the following lines to the constant definitions in the `export default async function decorate(block){}` statement: ```js 'show-discount': showDiscount = 'false', 'show-savings': showSavings = 'false', ``` 1. Add new lines in the `provider.render(CartSummaryList)` method to check whether `showDiscount` or `showSavings` is set to `true`: ```js showDiscount: showDiscount === 'true', showSavings: showSavings === 'true', ``` 1. Save the file. When you generate the page, discounts are not displayed because the default values are `false`. 1. Find the `cart` content document in your site's content folder, and add two rows to the Commerce Cart table that set the visibility values for these properties. ![Commerce Cart table](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/cart-table.png) *Commerce Cart table* Set the values of the Show Discount and Show Savings rows to either `true` or `false`. 1. Preview the changes with the Sidekick browser extension. Then publish the changes to your staging or production environment. --- # Configuring Product Variation Updates in the Cart This tutorial shows you how to configure the Edit feature for product variations in both the cart and mini-cart. The **Edit** button allows shoppers to update product variations (like size or color) directly from the cart pages. The implementation is already available in the codebase. This tutorial focuses on how to *enable* or *disable* this feature through the AEM block configuration. ## How it Works The **Edit** button feature is controlled by a configuration flag (`enable-updating-product`) that can be set on both the `commerce-cart` and `commerce-mini-cart` blocks in AEM. When activated, it opens a modal interface with a mini Product Detail Page (PDP) that allows shoppers to modify their selected options contextually. ### Cart In the `commerce-cart.js` implementation, the code checks for this flag and conditionally renders an **Edit** button in the `Footer` slot for configurable products: ```javascript // First, the configuration is read from the block with a default of 'false' const { 'hide-heading': hideHeading = 'false', 'max-items': maxItems, // ... other config properties ... 'checkout-url': checkoutURL = '', 'enable-updating-product': enableUpdatingProduct = 'false', } = readBlockConfig(block); // Later in the code, inside the Footer slot if (ctx.item?.itemType === 'ConfigurableCartItem' && enableUpdatingProduct === 'true') { const editLink = document.createElement('div'); editLink.className = 'cart-item-edit-link'; UI.render(Button, { children: placeholders?.Global?.CartEditButton, variant: 'tertiary', size: 'medium', icon: h(Icon, { source: 'Edit' }), onClick: () => handleEditButtonClick(ctx.item), })(editLink); ctx.appendChild(editLink); } ``` When a shopper clicks the **Edit** button, a modal opens with a mini-PDP interface that allows them to modify their product options. An auto-dismissing notification appears after a successful update. ### Mini Cart Similarly, in the `commerce-mini-cart.js` implementation, the code uses the same configuration flag to determine whether to display an **Edit** button for each configurable product in the mini-cart, implementing it in the `Thumbnail` slot: ```javascript // First, the configuration is read from the block with a default of 'false' const { 'start-shopping-url': startShoppingURL = '', 'cart-url': cartURL = '', 'checkout-url': checkoutURL = '', 'enable-updating-product': enableUpdatingProduct = 'false', } = readBlockConfig(block); // Later in the code, inside the Thumbnail slot if (item?.itemType === 'ConfigurableCartItem' && enableUpdatingProduct === 'true') { const editLinkContainer = document.createElement('div'); editLinkContainer.className = 'cart-item-edit-container'; const editLink = document.createElement('div'); editLink.className = 'cart-item-edit-link'; UI.render(Button, { children: placeholders?.Global?.CartEditButton, variant: 'tertiary', size: 'medium', icon: h(Icon, { source: 'Edit' }), onClick: () => handleEditButtonClick(item), })(editLink); editLinkContainer.appendChild(editLink); ctx.appendChild(editLinkContainer); } ``` When enabled, this provides a convenient modal-based editing experience. Success messages appear in both the mini-cart and main cart notification areas simultaneously, ensuring consistent user feedback across all cart interfaces. ## Configuration Steps To modify this feature's configuration, follow these steps: ### 1. Configure the Cart Summary Block The cart block shows **Edit** buttons *by default* when configurable products are present. If you want to disable it: 1. In your AEM authoring environment, navigate to the page containing your `commerce-cart` block. 2. Select the `commerce-cart` block and open its properties dialog. 3. Locate the existing property with the *Key* `Enable Updating Product`. 4. Change its *Value* to `false` to disable the feature. 5. Save the changes. 6. Preview the changes by clicking the **Preview** button. 7. Publish the changes by clicking the **Publish** button. > The configuration is already provided in the content block, so you don't need to add a new property - just modify the existing one as needed. ### 2. Configure the Mini Cart Block The `enable-updating-product` property is *already set to `false` by default* in the mini-cart block. If you want to enable it: 1. In your AEM authoring environment, navigate to the page or header that contains your `commerce-mini-cart` block. 2. Select the `commerce-mini-cart` block and open its properties dialog. 3. Locate the existing property with the *Key* `Enable Updating Product`. 4. Change its *Value* to `true` to enable the feature. 5. Save the changes. 6. Preview the changes by clicking the **Preview** button. 7. Publish the changes by clicking the **Publish** button. ### 3. Example Block Configurations Here's how your block configuration should look like: **Cart Block (Enabled by Default):** | Key | Value | | :------------------------ | :---- | | `Enable Updating Product` | `true`| | `Checkout URL` | `/checkout` | *(Example of another common property)* **Mini Cart Block (Disabled by Default):** | Key | Value | | :------------------------ | :---- | | `Enable Updating Product` | `false`| | `Checkout URL` | `/checkout` | *(Example of another common property)* > The property appears as `Enable Updating Product` (with spaces) in the AEM properties dialog, but is converted to kebab-case (`enable-updating-product`) when processed by the code. ## Testing the Configuration After configuring the feature, you should test it to ensure it's working as expected: 1. Add a configurable product to your cart. 2. View your cart page: - If enabled, you should see an **Edit** button for each configurable product. - If disabled, no **Edit** button should appear. 3. Open the mini cart: - If enabled, you should see an `Edit` option for configurable products. - If disabled, no `Edit` option should be visible. ## Feature Behavior When the **Edit** button is clicked, the following happens: 1. **Modal Interface**: A mini-PDP modal opens directly over the current page, maintaining user context. 2. **Pre-populated Options**: The modal displays the product with current selections already chosen. 3. **In-place Updates**: Changes are applied to the existing cart item. 4. **Comprehensive Messaging**: Success notifications appear in: - The main cart notification area (if present) - The mini-cart message system - Both locations simultaneously for consistent feedback 5. **Auto-dismissing Notifications**: Messages automatically disappear for better UX. > Using modals ensures users don't lose their shopping context when making product modifications. With this simple configuration, you can provide your shoppers with a more convenient shopping experience by allowing them to modify product variations directly from the cart. --- # Add gift options to a product detail page The [`GiftOptions` container](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/containers/gift-options/) allows you to add gift options, such as gift wrapping or personalized messages, at various places on the storefront, including product detail pages. The gift option features enhance the shopping experience by enabling customers to select these options at multiple times during their shopping experience, such as when adding a product to the cart or during checkout. The code examples provided here demonstrate the general approach to building custom integrations with the `GiftOptions` container. > This tutorial is not a fully functional integration and should only be used as a reference. ## Step-by-step The following steps describe how to render the `GiftOptions` container on the PDP page and apply the selected gift options to the cart when the product is added. ### 1. Import required modules Import the `GiftOptions` container and `CartProvider`. ```js ``` ### 2. Define gift options configuration for an item In this step, we will define the gift options configuration for a specific item. This can be done in different ways, such as by fetching configurations from the backend using API methods or retrieving them from product data. #### Example 1: Use `cartItem` data Use this technique when the product has already been added to the cart, such as on the cart page: ```js ​​const cartItem = JSON.parse( sessionStorage.getItem('DROPIN__CART__CART__DATA'), )?.items?.find((el) => el.sku === product.sku); ``` #### Example 2: Use a custom integration configuration This configuration can be composed using product data available on the PDP and a store configuration query. :::tip It is crucial that the manually-composed configuration matches the actual backend configurations. For example, the available gift wrappings must be fetched from the backend. Otherwise, they will not be applied correctly. The [`GiftOptions` container](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/containers/gift-options/) lists the relevant configuration screens in the Admin. ::: ```js type ProductGiftOptionsConfig = { giftWrappingAvailable: boolean; giftMessageAvailable: boolean; giftWrappingPrice?: Price; giftMessage?: { recipientName?: string; senderName?: string; message?: string; }; productGiftWrapping: GiftWrappingConfigProps[]; }; const predefinedConfig = { giftWrappingAvailable: true, giftMessageAvailable: true, productGiftWrapping: [ { design: 'Glossy Print Paper', uid: 'Mg==', selected: false, image: { url: 'https://aemshop.example.com/media/wrapping/glossy.png', label: 'glossy.png', }, price: { currency: 'USD', value: 25, }, }, { design: 'Foil Finish Paper', uid: 'NQ==', selected: false, image: { url: 'https://aemshop.example.com/media/wrapping/random-grid.jpg', label: 'random-grid.jpg', }, price: { currency: 'USD', value: 30, }, }, { design: 'Kraft Brown Paper', uid: 'OA==', selected: false, image: { url: 'https://mcstaging.aemshop.net/media/wrapping/brown-paper.jpg', label: 'brown-paper.jpg', }, price: { currency: 'USD', value: 45, }, }, ], }; ``` ### 3. Render the GiftOptions container For custom integration, we must pass an item prop, which can be either a `cartItem` or a manually-composed gift options configuration. In addition, we need to pass the `onGiftOptionsChange` callback. When provided, the container will not automatically save the gift options. Instead, the integration layer must handle this. The callback receives the updated gift options whenever they change. ```js CartProvider.render(GiftOptions, { item: cartItem ?? predefinedConfig, view: 'product', onGiftOptionsChange: async (data) => { console.info('onGiftOptionsChange :>> ', data); if (data) { sessionStorage.setItem('updatedGiftOptions', JSON.stringify(data)); } }, })($giftOptions); ``` ### 4. Update the Add to Cart button At this stage, we extend the **Add to Cart** button functionality by calling the `updateProductsFromCart` API method provided by the cart drop-in component to apply gift options after adding the product to the cart. > Gift options must be applied after adding the product to the cart. Adobe Commerce does not support applying gift options before adding the product. ```js // Configuration - Button - Add to Cart UI.render(Button, { children: labels.PDP?.Product?.AddToCart?.label, icon: Icon({ source: 'Cart' }), onClick: async () => { try { addToCart.setProps((prev) => ({ ...prev, children: labels.Custom?.AddingToCart?.label, disabled: true, })); // get the current selection values const values = pdpApi.getProductConfigurationValues(); const valid = pdpApi.isProductConfigurationValid(); // add the product to the cart if (valid) { const { addProductsToCart, updateProductsFromCart } = await import( '@dropins/storefront-cart/api.js' ); await addProductsToCart([{ ...values }]).then(async (response) => { const updatedGiftOptions = JSON.parse( sessionStorage.getItem('updatedGiftOptions'), ); if (!updatedGiftOptions) return; const { items } = response; const dropinCartData = items.find((el) => el.sku === values.sku); const { recipientName, senderName, message, giftWrappingId, isGiftWrappingSelected, } = updatedGiftOptions; const giftOptions = { gift_message: { to: recipientName, from: senderName, message, }, gift_wrapping_id: isGiftWrappingSelected ? giftWrappingId : null, }; await updateProductsFromCart([ { uid: dropinCartData.uid, quantity: dropinCartData.quantity, giftOptions, }, ]); }); } // reset any previous alerts if successful inlineAlert?.remove(); } catch (error) { // add alert message inlineAlert = await UI.render(InLineAlert, { heading: 'Error', description: error.message, icon: Icon({ source: 'Warning' }), 'aria-live': 'assertive', role: 'alert', onDismiss: () => { inlineAlert.remove(); }, })($alert); // Scroll the alertWrapper into view $alert.scrollIntoView({ behavior: 'smooth', block: 'center', }); } finally { addToCart.setProps((prev) => ({ ...prev, children: labels.PDP?.Product?.AddToCart?.label, disabled: false, })); } }, })($addToCart); ``` As a result of these customizations, the default `GiftOption` container is rendered as follows: ![Default GiftOption container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/pdp-gift-options-default.png) *Default GiftOption container* When the shopper makes a selection, the container is rendered as follows: ![Default GiftOption container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/pdp-gift-options-selected.png) *Default GiftOption container* After clicking **Add to Cart**, the product is added to the cart, and the selected gift options are applied. The cart page displays the applied gift options. ![Default GiftOption container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/pdp-gift-options-cart.png) *Default GiftOption container* --- # Order Summary Lines The Cart drop-in allows you to customize the lines of the Order Summary to meet your requirements. You might want to group and sort the order summary lines into sections using the Accordion component from the Storefront SDK (Elsie). See the /sdk/components/accordion/ component reference in this documentation. :::note[Note] For the full set of UI primitives (including icons), start from the /sdk/components/overview/ in this documentation. ::: You can specify the line items shown in the accordion and decide the order in which they are displayed. You can also customize the title and content of these order summary lines. ## Customize the lines of the Order Summary to meet your needs This customization is possible thanks to the attribute `updateLineItems` from the `OrderSummary` container. This attribute allows you to modify the order summary lines before they are rendered. ```typescript export const OrderSummary: Container = ({ ... updateLineItems = (lineItems) => lineItems, ... }); ``` It doesn't matter if you want to customize the existing order summary lines, add new ones, or skip some of them. The `OrderSummary` container passes the `updateLineItems` to the `OrderSummary` component, which performs the appropriate actions. `updateLineItems` is an optional function that receives the line items as an argument and returns the updated line items. In both cases, `lineItems` are an array of `OrderSummaryLineItem` object. ```typescript export interface OrderSummaryLineItem { key: string; title?: string; className?: string; sortOrder: number; content: | string | JSXInternal.Element | VNode> | OrderSummaryLineItem[] | undefined; } ``` There are default order summary lines in the `OrderSummary` component. If no customization is needed, and therefore nothing is passed using the `updateLineItems` attribute, the default order summary lines will be rendered. Let's imagine that `lineItems` contains the following lines: ```typescript const lineItems: Array = [ { key: 'subTotalContent', sortOrder: 100, content: subTotalContent, }, { key: 'discountsContent', sortOrder: 300, content: discountsContent, }, { key: 'taxContent', sortOrder: 400, content: taxContent, }, ]; ``` In the example above, the `OrderSummary` component renders the sub-total, discounts, and tax lines, in that order. The value of the `sortOrder` attribute determines the order in which the lines are rendered. The larger the `sortOrder` value, the lower the line will be rendered in the order summary. The `content` attribute can be a string, a JSX element, or an array of `OrderSummaryLineItem` objects (whenever is not `undefined`). For instance, you could choose to render a JSX element in a form of a `OrderSummaryLine` container. This `OrderSummaryLine` container it is defined as follows: ```typescript export interface OrderSummaryLineProps extends HTMLAttributes { label: string; price: VNode>; classSuffixes?: Array; labelClassSuffix?: string; testId?: string; children?: any; } ``` See an example of how to use the `OrderSummaryLine` container below, where only the mandatory props are passed: ```html {children} ); ``` Note that the `OrderSummaryLine` container behaves like a wrapper for the `OrderSummaryLine` component. The component ultimately decides how to render the line item based on the `children` attribute. ```typescript export interface OrderSummaryLineComponentProps extends HTMLAttributes { label: string; price: VNode>; classSuffixes?: Array; labelClassSuffix?: string; testId?: string; children?: any; } ``` ### Where to perform the customizations To customize the order summary lines, you need to render the `Cart` component passing the `OrderSummary` component as a slot. When rendering the `OrderSummary` component, you can pass the `updateLineItems` attribute to customize the order summary lines as needed. ```typescript // Cart provider.render(Cart, { slots: { OrderSummary: (ctx) => { const orderSummary = document.createElement('div'); provider.render(OrderSummary, { updateLineItems: (lineItems) => { // Customize the order summary lines here return lineItems; } } } } }); ``` ## Examples For the examples shown below, assume that this is how `Order Summary` looks originally: ![Cart without any customization](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/cart-originally.png) *Cart without any customization* ### Remove Item: Remove total saved The following example removes the Total saved line: ```typescript updateLineItems: (lineItems) => { const index = lineItems.map(item => item.key).indexOf('totalSavedContent'); lineItems.splice(index, 1); return lineItems; } ``` ![Cart after removing Total Saved line](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/cart-after-remove-item.png) *Cart after removing Total Saved line* ### Reorder items: Move primary action to the beginning The following example moves the Checkout button to the top: ```typescript updateLineItems: (lineItems) => { lineItems.map(lineItem => { if (lineItem.key === 'primaryActionContent') { lineItem.sortOrder = 50; } return lineItem; }); return lineItems; }; ``` ![Cart after moving primary action to the beginning](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/cart-after-reorder-items.png) *Cart after moving primary action to the beginning* ### Group items: Group subtotal and tax in an accordion The following example groups the subtotal and tax in an accordion: ```typescript updateLineItems: (lineItems) => { const totalsIndex = lineItems.map(item => item.key).indexOf('taxContent'); const taxContent = lineItems.splice(totalsIndex, 1)[0]; const subtotalIndex = lineItems.map(item => item.key).indexOf('subTotalContent'); const subTotalContent = lineItems.splice(subtotalIndex, 1)[0]; lineItems.push({ key: 'subtotalTaxGrouped', sortOrder: 50, title: 'Subtotal and Tax', content: [ taxContent, subTotalContent, ], }); return lineItems; } ``` ![Cart after grouping subtotal and tax in an accordion](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/cart-after-group-items.png) *Cart after grouping subtotal and tax in an accordion* ### Add item: Add a new order summary line The following example adds the FPT line: ```typescript updateLineItems: (lineItems) => { const totalFpt = ctx.data.items.reduce((allItemsFpt, item) => { const itemFpt = item.fixedProductTaxes.reduce((accumulator, fpt) => { accumulator.labels.push(fpt.label); accumulator.total += fpt.amount.value; return accumulator; }, { labels: [], total: 0 }); allItemsFpt.labels = [...allItemsFpt.labels, ...itemFpt.labels]; allItemsFpt.total += itemFpt.total; return allItemsFpt; }, { labels: [], total: 0 }); lineItems.push({ key: 'fpt', sortOrder: 350, title: 'Fixed Product Tax', content: OrderSummaryLine({label: "FPT(" + totalFpt.labels.join(',') + ')', price: Price({amount: totalFpt.total}), classSuffix: 'fpt'}) }) return lineItems; }; ``` ![Cart after adding a new order summary line](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/cart/cart-after-add-item.png) *Cart after adding a new order summary line* --- # AddressValidation container The `AddressValidation` container displays a suggested shipping address (from a third-party verification service) alongside the entered address, allowing shoppers to choose between them. Typically invoked from a modal during checkout after calling your address verification service. ## AddressValidation configurations The `AddressValidation` container provides the following configuration options: ```text [ ['Option', 'Type', 'Req?', 'Description'], ['suggestedAddress', 'CartAddressInput | null', 'No', 'Address suggestion to present to the shopper.'], ['handleSelectedAddress', 'function', 'No', 'Async callback fired when the shopper selects an address. Receives the selection and the chosen address.'], ] ``` ### AddressValidationProps interface The `AddressValidation` container receives an object that implements the following interface: ```ts interface AddressValidationProps { suggestedAddress: Partial | null; handleSelectedAddress?: (payload: { selection: 'suggested' | 'original'; address: CartAddressInput | null | undefined; }) => void; } ``` - `suggestedAddress` - The normalized address to propose to the shopper. - `handleSelectedAddress` - Called when the shopper selects an address. Use this to persist the selection or continue checkout. ## CartAddressInput type The `CartAddressInput` type has this shape: ```ts interface CartAddressInput { city: string; countryCode: string; postcode: string; region: string; street: string[]; } ``` > **Get the current address** The container automatically maps the current shipping address to `CartAddressInput` from checkout events. Only pass `suggestedAddress` when available. > **Normalization** Transform your address verification service output to `CartAddressInput` format (with fields like `street`, `city`, `region`, `countryCode`, `postcode`) before passing to the container. Missing properties default to the original address values. ## Example For a complete walkthrough, see the [Validate shipping address](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/tutorials/validate-shipping-address/) tutorial. --- # BillToShippingAddress container The `BillToShippingAddress` container includes a checkbox that allows users to indicate if the billing address is the same as the shipping address. If unchecked, the billing address form will be displayed. This container provides internal business logic to hide itself in case the cart is empty or virtual. ## BillToShippingAddress configurations The `BillToShippingAddress` container provides the following configuration options: ```text [ ['Option', 'Type', 'Req?', 'Description'], ['active', 'boolean', 'No', 'Activates/deactivates the container (default value is true).'], ['autoSync', 'boolean', 'No', 'Synchronizes/does not synchronize the container local state with the backend (default value is true).'], ['onCartSyncError', 'function', 'No', 'A function that takes an error as argument. It is called when the setBillingAddressOnCart() API throws an error when bill to shipping address checkbox is clicked to be stored to the backend.'], ['onChange', 'function', 'No', 'Callback function that is called when the checkbox state changes.'], ] ``` These configuration options implement the `BillToShippingAddressProps` interface: ### BillToShippingAddressProps interface The `BillToShippingAddress` container receives an object as a parameter which implements the `BillToShippingAddressProps` interface with the following properties: ```ts interface CartSyncError { error: Error; } export interface BillToShippingAddressProps extends Omit, 'onChange'> { active?: boolean; autoSync?: boolean; onCartSyncError?: (error: CartSyncError) => void; onChange?: (checked: boolean) => void; } ``` - Set the `active` property to _true_ to have the container in reactive mode (it is visible and responds to system events). If it is set to _false_, the container is deactivated (it does not subscribe to system events and is not rendered). - Set the `autoSync` property to _true_ to automatically synchronize the container state changes with the backend via API calls. If it is set to _false_ the container does not automatically synchronize its state, but still maintains local updates. - The `onCartSyncError` property is a handler used to perform actions called when bill to shipping address checkbox is clicked and the setBillingAddressOnCart() API throws an error. It could be used as a callback in the integration layer by the merchant to show errors or perform other actions. - The `onChange` property is a handler used to perform actions called when the checkbox is checked/unchecked. ## Example The following example renders the `BillToShippingAddress` container on a checkout page. It handles changes to the billing address form visibility and validation. If the billing address form is shown, it validates the form data and updates the billing address on the cart. Finally, an error message is shown in case there is an issue saving the billing address to the backend. ```ts const DEBOUNCE_TIME = 1000; const $billToShipping = checkoutFragment.querySelector( '.checkout__bill-to-shipping', ); const $billingForm = checkoutFragment.querySelector( '.checkout__billing-form', ); const billingFormRef = { current: null }; CheckoutProvider.render(BillToShippingAddress, { onCartSyncError: (error) => { const billToShippingMsg = document.createElement('div'); billToShippingMsg.style.color = 'red'; billToShippingMsg.innerText = `Error saving the Billing address with the Shipping address information: ${error.message}`; $billToShipping.appendChild(billToShippingMsg); }, onChange: (checked) => { $billingForm.style.display = checked ? 'none' : 'block'; if (!checked && billingFormRef?.current) { const { formData, isDataValid } = billingFormRef.current; setAddressOnCart({ api: checkoutApi.setBillingAddress, debounceMs: DEBOUNCE_TIME, placeOrderBtn: placeOrder, })({ data: formData, isDataValid }); } }, })($billToShipping), ``` --- # EstimateShipping container The `EstimateShipping` container is designed to estimate and display shipping costs during the checkout process. This container is read-only, unlike the editable [`EstimateShipping`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/containers/estimate-shipping/) container in the cart drop-in component. Initially, it displays estimated shipping costs. After a customer provides a shipping address and selects a shipping method, it shows the actual shipping cost. This container is designed to be used as a slot within the `OrderSummary` container from the cart, where the estimated shipping information is displayed. ## EstimateShipping configurations The `EstimateShipping` container provides the following configuration options: ```text [ ['Option', 'Type', 'Req?', 'Description'], ['active', 'boolean', 'No', 'Activates/deactivates the container (default value is true).'], ] ``` These configuration options implement the `EstimateShippingProps` interface: ### EstimateShippingProps interface The `EstimateShipping` container receives an object as a parameter which implements the `EstimateShippingProps` interface with the following properties: ```ts export interface EstimateShippingProps { active?: boolean; } ``` - Set the `active` property to _true_ to have the container in reactive mode (it is visible and responds to system events). If it is set to _false_, the container is deactivated (it does not subscribe to system events and is not rendered). ## Example The following example renders an `OrderSummary` container within a checkout page and includes a slot for estimating shipping: ```ts const $orderSummary = checkoutFragment.querySelector( '.checkout__order-summary', ); CartProvider.render(OrderSummary, { slots: { EstimateShipping: (esCtx) => { const estimateShippingForm = document.createElement('div'); CheckoutProvider.render(EstimateShipping)(estimateShippingForm); esCtx.appendChild(estimateShippingForm); }, }, })($orderSummary), ``` --- # Checkout Containers The **Checkout** drop-in provides pre-built container components for integrating into your storefront. Version: 3.3.0 ## What are Containers? Containers are pre-built UI components that combine functionality, state management, and presentation. They provide a complete solution for specific features and can be customized through props, slots, and CSS. ## Available Containers | Container | Description | | --------- | ----------- | | [AddressValidation](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/containers/address-validation/) | Configure the `AddressValidation` container to present suggested vs. | | [BillToShippingAddress](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/containers/bill-to-shipping-address/) | Configure the `BillToShippingAddress` container to manage and display the billing address form during checkout. | | [EstimateShipping](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/containers/estimate-shipping/) | Learn how the `EstimateShipping` container displays shipping costs during checkout. | | [LoginForm](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/containers/login-form/) | Configure the `LoginForm` container to handle user email input and validation during checkout. | | [MergedCartBanner](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/containers/merged-cart-banner/) | Configure the `MergedCartBanner` container to display notifications when items from an old cart are merged into the current cart. | | [OutOfStock](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/containers/out-of-stock/) | Configure the `OutOfStock` container to handle and display out-of-stock items in the cart. | | [PaymentMethods](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/containers/payment-methods/) | Configure the `PaymentMethods` container to manage and display available payment methods during checkout. | | [PaymentOnAccount](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/containers/payment-on-account/) | *Enrichment needed - add description to `_dropin-enrichments/checkout/containers.json`* | | [PlaceOrder](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/containers/place-order/) | Configure the `PlaceOrder` container to handle the final checkout step, including place order action, button disablement, and main slot management. | | [PurchaseOrder](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/containers/purchase-order/) | *Enrichment needed - add description to `_dropin-enrichments/checkout/containers.json`* | | [ServerError](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/containers/server-error/) | Configure the `ServerError` container to handle and display server error messages during checkout. | | [ShippingMethods](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/containers/shipping-methods/) | Configure the `ShippingMethods` container to manage and display available shipping methods during checkout. | | [TermsAndConditions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/containers/terms-and-conditions/) | Configure the `TermsAndConditions` container to manage and display the terms and conditions form during checkout. | > Each container is designed to work independently but can be composed together to create comprehensive user experiences. --- # LoginForm container The `LoginForm` container handles user email input and validation within the checkout process. ## LoginForm configurations The `LoginForm` container provides the following configuration options: ```text [ ['Option', 'Type', 'Req?', 'Description'], ['displayTitle (*)', 'boolean', 'No', 'Displays the container title (default value is true).'], ['active', 'boolean', 'No', 'Activates/deactivates the container (default value is true).'], ['autoSync', 'boolean', 'No', 'Synchronizes/does not synchronize the container local state with the backend (default value is true).'], ['displayHeadingContent', 'boolean', 'No', 'Displays the container heading content (default value is true).'], ['onSignInClick', 'function', 'No', 'A function that handles the sign-in button click. It takes the email (string or null) as an argument.'], ['onSignOutClick', 'function', 'No', 'A function that handles the sign-out button click. It takes no arguments.'], ['onCartSyncError', 'function', 'No', 'A function that takes an error and the email address as arguments. It is called when the setGuestEmailOnCart() API throws an error when filling in the email address to be stored to the backend.'], ['onValidationError', 'function', 'No', 'A function that takes the email validated with the type of error and its message as arguments. It is called when the email form field is validated with an error (due to it\'s missing or has an invalid format).'], ['slots', 'object', 'No', 'Object with the content to be displayed on the LoginForm container. This slot allows setting the heading content dynamically based on the user authentication status.'], ] ``` (*) Properties inherited from `TitleProps` These configuration options are implementing the `LoginFormProps` interface: ### LoginFormProps interface The `LoginForm` container receives an object as a parameter which implements the `LoginFormProps` interface with the following properties: ```ts interface ValidationError { email: string; message: string; type: 'missing' | 'invalid'; } interface CartSyncError { email: string; error: Error; } export interface LoginFormProps extends HTMLAttributes, TitleProps { active?: boolean; autoSync?: boolean; displayHeadingContent?: boolean; onSignInClick?: (email: string) => void; onSignOutClick?: () => void; onCartSyncError?: (error: CartSyncError) => void; onValidationError?: (error: ValidationError) => void; slots?: { Heading?: SlotProps<{ authenticated: boolean; }>; Preferences?: SlotProps<{ email: string; isEmailValid: boolean; isAuthenticated: boolean; }>; } & TitleProps['slots']; } ``` - The `displayTitle (*)` property inherits from the `TitleProps` interface to display or hide the title. - Set the `active` property to _true_ to have the container in reactive mode (it is visible and responds to system events). If it is set to _false_, the container is deactivated (it does not subscribe to system events and is not rendered). - Set the `autoSync` property to _true_ to automatically synchronize the container state changes with the backend via API calls. If it is set to _false_ the container does not automatically synchronize its state, but still maintains local updates. - Set the `displayHeadingContent` property to _true_ to display the heading content with the sign-in/sign-out button. - The `onSignInClick` property is a handler used to perform actions called when the sign-in button is clicked. It accepts an email as an input parameter. - The `onSignOutClick` property is a handler used to perform actions called when the sign-out button is clicked. - The `onCartSyncError` property is a handler used to perform actions called when filling in the email address and the setGuestEmailOnCart() API throws an error. It could be used as a callback in the integration layer by the merchant to show errors or perform other actions. - The `onValidationError` property is a handler used to perform actions called when the email address form field is validated with an error. It could be used as a callback in the integration layer by the merchant to show errors or perform other actions. - The `slots` property is an object containing the following properties: - Use the `Title (*)` property to render a custom title. This property is inherited from `TitleProps` interface. - The `Heading` property is a handler used to render a customized heading content based on the authenticated status provided by the context. - The `Preferences` property is a handler used to render custom marketing preference fields (such as newsletter subscriptions or promotional consent checkboxes). The slot receives context with the current email address, email validation state, and authentication status. ## Example 1: Render with title and heading content by default The following example renders the `LoginForm` container on a checkout page, which includes rendering the [`AuthCombine`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/user-auth/containers/auth-combine/) container from the user auth drop-in component in a modal for authentication: ```ts const LOGIN_FORM_NAME = 'login-form'; const $loader = checkoutFragment.querySelector('.checkout__loader'); const $login = checkoutFragment.querySelector('.checkout__login'); let loader; const displayOverlaySpinner = async () => { if (loader) return; loader = await UI.render(ProgressSpinner, { className: '.checkout__overlay-spinner', })($loader); }; CheckoutProvider.render(LoginForm, { name: LOGIN_FORM_NAME, onSignInClick: async (initialEmailValue) => { const signInForm = document.createElement('div'); AuthProvider.render(AuthCombine, { signInFormConfig: { renderSignUpLink: true, initialEmailValue, onSuccessCallback: () => { displayOverlaySpinner(); }, }, signUpFormConfig: { slots: { ...authPrivacyPolicyConsentSlot, }, }, resetPasswordFormConfig: {}, })(signInForm); showModal(signInForm); }, onSignOutClick: () => { authApi.revokeCustomerToken(); }, })($login), ``` ## Example 2: Render without title and heading content The following example renders the `LoginForm` container on a checkout page but without displaying both title and heading content: ```ts const LOGIN_FORM_NAME = 'login-form'; const $login = checkoutFragment.querySelector('.checkout__login'); CheckoutProvider.render(LoginForm, { displayTitle: false, displayHeadingContent: false, })($login), ``` ## Example 3: Render with customized title and heading content The following example renders the `LoginForm` container on a checkout page providing customized title and heading content: ```ts const LOGIN_FORM_NAME = 'login-form'; const $login = checkoutFragment.querySelector('.checkout__login'); CheckoutProvider.render(LoginForm, { name: LOGIN_FORM_NAME, onSignInClick: async (initialEmailValue) => { . . . }, onSignOutClick: () => { . . . }, slots: { Title: (ctx) => { const content = document.createElement('div'); content.innerText = 'Custom title'; ctx.replaceWith(content); }, Heading: (ctx) => { const content = document.createElement('div'); if (ctx.authenticated) { // Put here a customized content when the user has signed-in } else { // Put here a customized content when the user still has not signed-in } ctx.replaceWith(content); }, }, })($login), ``` ## Example 4: Render with callbacks for error handling The following example renders the `LoginForm` container on a checkout page providing handlers for validation and API errors: ```ts const LOGIN_FORM_NAME = 'login-form'; const $login = checkoutFragment.querySelector('.checkout__login'); CheckoutProvider.render(LoginForm, { name: LOGIN_FORM_NAME, onSignInClick: async (initialEmailValue) => { . . . }, onSignOutClick: () => { . . . }, onCartSyncError: ({ email, error }) => { const loginFormMsg = document.createElement('div'); loginFormMsg.style.color = 'red'; loginFormMsg.innerText = `Error saving the email address ${email}: ${error.message}`; $login.appendChild(loginFormMsg); }, onValidationError: ({ email, message, type }) => { const loginFormMsg = document.createElement('div'); loginFormMsg.style.color = 'red'; loginFormMsg.innerText = `Validation error (${type}) introducing the email address ${email}: ${message}`; $login.appendChild(loginFormMsg); }, })($login), ``` ## Example 5: Render with marketing preferences slot The following example renders the `LoginForm` container with a custom marketing preferences slot that allows merchants to capture newsletter subscription consent: ```ts const LOGIN_FORM_NAME = 'login-form'; const $login = checkoutFragment.querySelector('.checkout__login'); CheckoutProvider.render(LoginForm, { name: LOGIN_FORM_NAME, onSignInClick: async (initialEmailValue) => { . . . }, onSignOutClick: () => { . . . }, slots: { Preferences: (ctx) => { // Only show preferences when email is valid and user is not authenticated if (!ctx.isEmailValid || ctx.isAuthenticated) { return; } const label = document.createElement('label'); label.className = 'checkout__preference-item'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.name = 'newsletter'; checkbox.id = 'newsletter-subscription'; const text = document.createElement('span'); text.textContent = 'Subscribe to our newsletter for exclusive offers'; label.appendChild(checkbox); label.appendChild(text); ctx.appendChild(label); }, }, })($login), ``` --- # MergedCartBanner container Use the `MergedCartBanner` container to display a notification banner when items from an old cart are merged into the current cart. When a customer signs in, if they had items in a previous cart, a banner will notify them that the items from their previous cart have been merged with the current cart. You can apply styles to the banner by passing a CSS `className` prop to the container. ## MergedCartBanner configurations The `MergedCartBanner` container provides the following configuration options: ```text [ ['Option', 'Type', 'Req?', 'Description'], ['active', 'boolean', 'No', 'Activates/deactivates the container (default value is true).'], ] ``` These configuration options are implementing the `MergedCartBannerProps` interface: ### MergedCartBannerProps interface The `MergedCartBanner` container receives an object as a parameter which implements the `MergedCartBannerProps` interface with the following properties: ```ts export interface MergedCartBannerProps extends AlertBannerProps { active?: boolean; } ``` - Set the `active` property to _true_ to have the container in reactive mode (it is visible and responds to system events). If it is set to _false_, the container is deactivated (it does not subscribe to system events and is not rendered). ## Example The following example renders the `MergedCartBanner` container with a custom class name: ```ts const $mergedCartBanner = checkoutFragment.querySelector( '.checkout__merged-cart-banner' ); CheckoutProvider.render(MergedCartBanner, { className: 'checkout__merged-cart-banner--custom', })($mergedCartBanner); ``` --- # OutOfStock container The `OutOfStock` container is designed to handle and display items in the shopping cart that are out of stock or have insufficient quantity. You can configure it to handle the removal of out-of-stock items and provide a route to the cart page. ## OutOfStock configurations The `OutOfStock` container provides the following configuration options: ```text [ ['Option', 'Type', 'Req?', 'Description'], ['active', 'boolean', 'No', 'Activates/deactivates the container (default value is true).'], ['onCartProductsUpdate', 'function', 'No', 'Handles the removal of out-of-stock items. It takes the list of items that are out of stock as an argument.'], ['routeCart', 'function', 'No', 'The route to the cart page.'], ] ``` These configuration options implement the `OutOfStockProps` interface: ### OutOfStockProps interface The `OutOfStock` container receives an object as a parameter which implements the `OutOfStockProps` interface with the following properties: ```ts export type UpdateProductsFromCart = Array<{ uid: string; quantity: number; }>; export interface OutOfStockProps extends Omit, 'icon'> { active?: boolean; onCartProductsUpdate?: (items: UpdateProductsFromCart) => void; routeCart?: () => string; } ``` - Set the `active` property to _true_ to have the container in reactive mode (it is visible and responds to system events). If it is set to _false_, the container is deactivated (it does not subscribe to system events and is not rendered). - The `onCartProductsUpdate` property is a handler used to perform actions called when there are out-of-stock items. It takes the list of items (array with pairs of _uid_ and _quantity_ values) as an input parameter. - The `routeCart` property is a handler used to indicate the route to the cart page. ## Example The following example renders the `OutOfStock` container to handle and display out-of-stock items in the cart: ```ts const $outOfStock = checkoutFragment.querySelector('.checkout__out-of-stock'); CheckoutProvider.render(OutOfStock, { routeCart: () => '/cart', onCartProductsUpdate: (items) => { cartApi.updateProductsFromCart(items).catch(console.error); }, })($outOfStock), ``` --- # PaymentMethods container Use the `PaymentMethods` container to manage and display the available payment methods during the checkout process. Configuration options: - Set the payment method automatically or manually (starting without a selected payment method) - Show an icon beside of the label - Display or hide the label - Provide a specific handler to render the payment method ## PaymentMethods configurations The `PaymentMethods` container provides the following configuration options: ```text [ ['Option', 'Type', 'Req?', 'Description'], ['displayTitle (*)', 'boolean', 'No', 'Displays the container title (default value is true).'], ['active', 'boolean', 'No', 'Activates/deactivates the container (default value is true).'], ['autoSync', 'boolean', 'No', 'Synchronizes/does not synchronize the container local state with the backend (default value is true).'], ['onCartSyncError', 'function', 'No', 'A function that takes a PaymentMethod object and an error as arguments. It is called when the setPaymentMethodOnCart() API throws an error when a payment method is selected to be stored to the backend.'], ['onSelectionChange', 'function', 'No', 'A function that takes a PaymentMethod object as an argument. It is called when a payment method is selected.'], ['slots', 'object', 'No', 'Object with a list of configurations for existing payment methods.'], ['UIComponentType', 'string', 'No', 'String with the UI component type to be used as selector (default value is \'ToggleButton\').'], ] ``` (*) Properties inherited from `TitleProps` These configuration options are implementing the `PaymentMethodsProps` interface: ### PaymentMethodsProps interface The `PaymentMethods` container receives an object as parameter which implements the `PaymentMethodsProps` interface with the following properties: ```ts export type UIComponentType = 'ToggleButton' | 'RadioButton'; interface CartSyncError { method: PaymentMethod; error: Error; } export interface PaymentMethodsProps extends HTMLAttributes, TitleProps { active?: boolean; autoSync?: boolean; onCartSyncError?: (error: CartSyncError) => void; onSelectionChange?: (method: PaymentMethod) => void; slots?: { Methods?: PaymentMethodsSlot; } & TitleProps['slots']; UIComponentType?: UIComponentType; } ``` - The `displayTitle (*)` property is inherited from the `TitleProps` interface. It is used to determine whether to display the title. - Set the `active` property to _true_ to have the container in reactive mode (it is visible and responds to system events). If it is set to _false_, the container is deactivated (it does not subscribe to system events and is not rendered). - Set the `autoSync` property to _true_ to automatically synchronize the container state changes with the backend via API calls. If it is set to _false_ the container does not automatically synchronize its state, but still maintains local updates. - The `onCartSyncError` property is a handler used to perform actions called when a payment method is selected and the setPaymentMethodOnCart() API throws an error. It could be used in the integration layer by the merchant to show errors. - The `onSelectionChange` property is a handler used to perform actions called when a payment method is selected. - The `UIComponentType` property is a string containing the name of the UI component type to be used as a selector for each payment method. The available UI components are: `ToggleButton` or `RadioButton`. - The `slots` property is an object containing the following properties: - Use the `Title (*)` property to render a custom title. This property is inherited from `TitleProps` interface. - The `Methods` property is an object which implements the `PaymentMethodsSlot` interface: ```ts export interface PaymentMethodsSlot { [code: string]: PaymentMethodConfig; } ``` It consists on a list of payment method codes providing a set of configurations to customize the payment method. Each payment method will have its own set of configurations implementing the `PaymentMethodConfig` interface: ```ts export type SlotProps = ( ctx: T & DefaultSlotContext, element: HTMLDivElement | null ) => Promise | void; export interface PaymentMethodRenderCtx { cartId: string; replaceHTML: (domElement: HTMLElement) => void; additionalData?: Record; setAdditionalData: (data: Record) => void; } export interface PaymentMethodConfig { displayLabel?: boolean; enabled?: boolean; icon?: string; autoSync?: boolean; render?: SlotProps; } ``` - The `PaymentMethodConfig` interface is composed by: - The `displayLabel` configuration hides the payment method label (for instance, if you only want to display the icon). - The `enabled` configuration allows merchants to individually hide payment methods filtering them from the available payment methods list (for instance, it is useful when a payment provider has enabled a payment method in the backend, which is configured with more than one payment option and you don't want to display one of them). - The `icon` configuration specifies the name of the icon to be shown beside of the label. The icon name must exist within the list of available icons defined on the /sdk/components/icon/. - The `autoSync` configuration sets the payment method automatically when it is selected. Only if a payment method is specifically set to _false_, the container will not automatically set the payment method to the cart when selected (for instance, if a payment method needs more information obtained during the place order action). This specific configuration has more priority than the generic one declared on the `PaymentMethodsProps`. In case this configuration is not provided, then it will be used the generic `autoSync` property. - The `render` configuration is a handler used to render and configure the payment method. ## Example 1: Render the available payment methods with callbacks The following example renders the `PaymentMethods` container on a checkout page, displaying the available payment methods in the element with the class `checkout__payment-methods`. It includes configurations to show a message if the chosen payment method is Credit Card, and show an error message in case there was an issue saving the selected payment method to the backend. ```ts // Checkout Dropin // Payment Services Dropin const $paymentMethods = checkoutFragment.querySelector( '.checkout__payment-methods', ); CheckoutProvider.render(PaymentMethods, { onCartSyncError: ({ method, error }) => { const paymentMsg = document.createElement('div'); paymentMsg.style.color = 'red'; paymentMsg.innerText = `Error selecting the Payment Method ${method.code} ${method.title}: ${error.message}`; $paymentMethods.appendChild(paymentMsg); }, onSelectionChange: (method) => { if (method.code === PaymentMethodCode.CREDIT_CARD) { const paymentMsg = document.createElement('div'); paymentMsg.innerText = 'Payment method not available for the country selected'; $paymentMethods.appendChild(paymentMsg); } }, })($paymentMethods), ``` ## Example 2: Render with the `displayLabel` and `icon` configurations The following example renders the `PaymentMethods` container on a checkout page, displaying the available payment methods in the element with the class `checkout__payment-methods`, providing an icon for `checkmo` and `banktransfer`, and hiding the label for `banktransfer`. ```ts // Checkout Dropin const $paymentMethods = checkoutFragment.querySelector( '.checkout__payment-methods', ); CheckoutProvider.render(PaymentMethods, { slots: { Methods: { checkmo: { icon: 'Wallet', render: (ctx) => { const $content = document.createElement('div'); $content.innerText = 'Pay later with Checkmo config handler'; ctx.replaceHTML($content); }, }, banktransfer: { displayLabel: false, icon: 'Card', }, }, }, })($paymentMethods), ``` ## Example 3: Render with the `autoSync` and `render` configurations The following example renders the `PaymentMethods` container on a checkout page, displaying the available payment methods in the element with the class `checkout__payment-methods`, providing a specific handler for `braintree` payment method indicating it cannot be set to the cart when selected. ```ts // Checkout Dropin const $paymentMethods = checkoutFragment.querySelector( '.checkout__payment-methods', ); let braintreeInstance; CheckoutProvider.render(PaymentMethods, { slots: { Methods: { braintree: { autoSync: false, render: async (ctx) => { const container = document.createElement('div'); window.braintree.dropin.create({ authorization: 'sandbox_cstz6tw9_sbj9bzvx2ngq77n4', container, }, (err, dropinInstance) => { if (err) { console.error(err); } braintreeInstance = dropinInstance; }); ctx.replaceHTML(container); }, }, }, }, })($paymentMethods), ``` ## Example 4: Render with the `enabled` configurations The following example renders the `PaymentMethods` container on a checkout page, displaying the available payment methods in the element with the class `checkout__payment-methods`, providing a specific handler for the credit card payment option but disabling the rest of payment options from `PaymentServices` payment method. ```ts // Checkout Dropin // Payment Services Dropin const $paymentMethods = checkoutFragment.querySelector( '.checkout__payment-methods', ); // Container and component references const creditCardFormRef = { current: null }; // Adobe Commerce GraphQL endpoint const commerceCoreEndpoint = await getConfigValue('commerce-core-endpoint'); CheckoutProvider.render(PaymentMethods, { slots: { Methods: { [PaymentMethodCode.CREDIT_CARD]: { render: (ctx) => { const $content = document.createElement('div'); PaymentServicesProvider.render(CreditCard, { apiUrl: commerceCoreEndpoint, getCustomerToken: getUserTokenCookie, getCartId: () => ctx.cartId, creditCardFormRef, })($content); ctx.replaceHTML($content); }, }, [PaymentMethodCode.SMART_BUTTONS]: { enabled: false, }, [PaymentMethodCode.APPLE_PAY]: { enabled: false, }, [PaymentMethodCode.GOOGLE_PAY]: { enabled: false, }, [PaymentMethodCode.VAULT]: { enabled: false, }, }, }, })($paymentMethods), ``` ## Example 5: Render with custom title and radio button as selector The following example renders the `PaymentMethods` container on a checkout page to display a custom title and radio buttons instead of toggle buttons for selecting the payment options. ```ts // Checkout Dropin const $paymentMethods = checkoutFragment.querySelector( '.checkout__payment-methods', ); CheckoutProvider.render(PaymentMethods, { UIComponentType: 'RadioButton', displayTitle: true, slots: { Title: (ctx) => { const content = document.createElement('div'); content.innerText = 'Custom title'; ctx.replaceWith(content); }, }, })($paymentMethods), ``` --- # PaymentOnAccount Container Version: 3.3.0 ## Configuration The `PaymentOnAccount` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `initialReferenceNumber` | `string` | No | | | `onReferenceNumberChange` | `function` | No | Callback function triggered when reference number change | | `onReferenceNumberBlur` | `function` | No | Callback function triggered when reference number blur | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `PaymentOnAccount` container: ```js await provider.render(PaymentOnAccount, { initialReferenceNumber: "example", onReferenceNumberChange: onReferenceNumberChange, onReferenceNumberBlur: onReferenceNumberBlur, })(block); ``` --- # PlaceOrder container The `PlaceOrder` container handles the final step in the checkout process, where the user confirms and places an order. Configure it to disable the button, perform validations before submitting the form, handle the place order action, and manage the content slot for the place order button. ## PlaceOrder configurations The `PlaceOrder` container provides the following configuration options: ```text [ ['Option', 'Type', 'Req?', 'Description'], ['active', 'boolean', 'No', 'Activates/deactivates the container (default value is true).'], ['disabled', 'boolean', 'No', 'Disables the Place Order button.'], ['handleValidation', 'function', 'No', 'Performs validation checks and returns a boolean or Promise<boolean>. Supports asynchronous validation for server-side fraud checks. The order proceeds only when this function returns true (or a promise that resolves to true).'], ['handlePlaceOrder', 'function', 'Yes', 'Handles the order placement process asynchronously. Receives a context object containing the selected payment method code and the cart ID.'], ['slots', 'object', 'No', 'Sets the PlaceOrder container content dynamically based on the selected payment method.'], ] ``` These configuration options implement the `PlaceOrderProps` interface: ### PlaceOrderProps interface The `PlaceOrder` container receives an object that implements the `PlaceOrderProps` interface: ```ts export interface PlaceOrderProps extends HTMLAttributes { active?: boolean; disabled?: boolean; handleValidation?: () => boolean | Promise; handlePlaceOrder: (ctx: HandlePlaceOrderContext) => Promise; slots?: { Content?: SlotProps; }; } ``` - The `active` property controls whether the container responds to system events and renders. Set to _true_ (default) for reactive mode. Set to _false_ to deactivate and hide the container. - The `disabled` property forces the Place Order button into a disabled state when set to _true_. - The `handleValidation` property performs validation checks before submitting the checkout forms and placing the order. It returns a `boolean` synchronously or a `Promise` for asynchronous validation (for example, server-side fraud checks). - The `handlePlaceOrder` property executes when the user clicks the Place Order button and `handleValidation` returns _true_ (when provided). It accepts a context parameter that implements the `HandlePlaceOrderContext` interface: ```ts export interface HandlePlaceOrderContext { code: string; cartId: string; } ``` - The `slots` property contains the following: - The `Content` slot renders PlaceOrder container content based on the selected payment method code: ```ts export type SlotProps = ( ctx: T & DefaultSlotContext, element: HTMLDivElement | null ) => Promise | void; export interface ContentSlotContext { code: string; } ``` ## Example 1: Render performing validations and a handler for order placement The following example renders the `PlaceOrder` container on a checkout page using the `PaymentServices` drop-in component as a payment method. It includes functionality to validate login, shipping, billing, and terms & conditions forms before placing an order. If the validation passes, it attempts to place the order and handles any errors. ```ts // Checkout Dropin // Order Dropin Modules // Payment Services Dropin const LOGIN_FORM_NAME = 'login-form'; const SHIPPING_FORM_NAME = 'selectedShippingAddress'; const BILLING_FORM_NAME = 'selectedBillingAddress'; const TERMS_AND_CONDITIONS_FORM_NAME = 'checkout-terms-and-conditions__form'; const $placeOrder = checkoutFragment.querySelector('.checkout__place-order'); const shippingFormRef = { current: null }; const billingFormRef = { current: null }; const creditCardFormRef = { current: null }; CheckoutProvider.render(PlaceOrder, { handleValidation: () => { let success = true; const { forms } = document; const loginForm = forms[LOGIN_FORM_NAME]; if (loginForm) { success = loginForm.checkValidity(); if (!success) scrollToElement($login); } const shippingForm = forms[SHIPPING_FORM_NAME]; if ( success && shippingFormRef.current && shippingForm && shippingForm.checkVisibility() ) { success = shippingFormRef.current.handleValidationSubmit(false); } const billingForm = forms[BILLING_FORM_NAME]; if ( success && billingFormRef.current && billingForm && billingForm.checkVisibility() ) { success = billingFormRef.current.handleValidationSubmit(false); } const termsAndConditionsForm = forms[TERMS_AND_CONDITIONS_FORM_NAME]; if (success && termsAndConditionsForm) { success = termsAndConditionsForm.checkValidity(); if (!success) scrollToElement($termsAndConditions); } return success; }, handlePlaceOrder: async ({ cartId, code }) => { await displayOverlaySpinner(); try { // Payment Services credit card if (code === PaymentMethodCode.CREDIT_CARD) { if (!creditCardFormRef.current) { console.error('Credit card form not rendered.'); return; } if (!creditCardFormRef.current.validate()) { // Credit card form invalid; abort order placement return; } // Submit Payment Services credit card form await creditCardFormRef.current.submit(); } // Place order await orderApi.placeOrder(cartId); } catch (error) { console.error(error); throw error; } finally { removeOverlaySpinner(); } }, })($placeOrder), ``` ## Example 2: Render providing explicit content for the button The following example renders the `PlaceOrder` container on a checkout page providing different text for the Place Order button depending on the selected payment method. ```ts // Checkout Dropin // Order Dropin Modules const $placeOrder = checkoutFragment.querySelector('.checkout__place-order'); CheckoutProvider.render(PlaceOrder, { handlePlaceOrder: async ({ cartId }) => { orderApi.placeOrder(cartId).catch(console.error); }, slots: { Content: (ctx) => { const content = document.createElement('span'); ctx.appendChild(content); function setContent(currentCtx) { switch (currentCtx.code) { case 'checkmo': { content.textContent = 'Pay Now'; break; } case 'banktransfer': { content.textContent = 'Make a transfer'; break; } default: { content.textContent = currentCtx.dictionary.Checkout.PlaceOrder.button; } } } setContent(ctx); ctx.onChange(setContent); }, }, })($placeOrder), ``` --- # PurchaseOrder Container Version: 3.3.0 ## Configuration The `PurchaseOrder` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `initialReferenceNumber` | `string` | No | | | `onReferenceNumberChange` | `function` | No | Callback function triggered when reference number change | | `onReferenceNumberBlur` | `function` | No | Callback function triggered when reference number blur | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `PurchaseOrder` container: ```js await provider.render(PurchaseOrder, { initialReferenceNumber: "example", onReferenceNumberChange: onReferenceNumberChange, onReferenceNumberBlur: onReferenceNumberBlur, })(block); ``` --- # ServerError container The `ServerError` container is designed to handle and display server error messages during the checkout process. You can configure it to display an error message and handle click events. ## ServerError configurations The `ServerError` container provides the following configuration options: ```text [ ['Option', 'Type', 'Req?', 'Description'], ['active', 'boolean', 'No', 'Activates/deactivates the container (default value is true).'], ['autoScroll', 'boolean', 'No', 'Scrolls the element`s ancestor containers such that the error message is visible to the user.'], ['onRetry', 'function', 'No', 'A function to handle retry actions.'], ['onServerError', 'function', 'No', 'A function to handle when there are server errors.'], ] ``` These configuration options are implementing the `ServerErrorProps` interface: ### ServerErrorProps interface The `ServerError` container receives an object as parameter which implements the `ServerErrorProps` interface with the following properties: ```ts export interface ServerErrorProps { active?: boolean; autoScroll?: boolean; onRetry?: () => void; onServerError?: (error: string) => void; } ``` - Set the `active` property to _true_ to have the container in reactive mode (it is visible and responds to system events). If it is set to _false_, the container is deactivated (it does not subscribe to system events and is not rendered). - The `autoScroll` property is a boolean to indicate if the page should scroll to the element containing the error message and put the focus on it to be visible to the user. - The `onRetry` property is a handler used to perform actions called when the retry button is clicked. - The `onServerError` property is a handler used to perform actions called when there is a new error message. ## Example The following example renders the `ServerError` container on a checkout page. It provides functionality to handle retry actions by removing an error class from the content element and to handle server errors by adding an error class to the content element. The page will scroll to the element containing the error message focusing on it. ```ts const $serverError = checkoutFragment.querySelector( '.checkout__server-error' ); CheckoutProvider.render(ServerError, { autoScroll: true, onRetry: () => { $content.classList.remove('checkout__content--error'); }, onServerError: () => { $content.classList.add('checkout__content--error'); }, })($serverError), ``` --- # ShippingMethods container The `ShippingMethods` container is designed to manage and display the selection of available shipping methods during the checkout process. You can configure it to handle the selection of shipping methods, display the available shipping methods, and manage the main slot for the shipping methods. This container includes internal business logic to hide itself if the cart is empty or virtual. Finally, if an error is thrown selecting a shipping method, a callback function is provided in order to handle that error in the integration layer; a rollback will be performed to the last valid shipping method selected by the user. ## ShippingMethods configurations The `ShippingMethods` container provides the following configuration options: ```text [ ['Option', 'Type', 'Req?', 'Description'], ['displayTitle (*)', 'boolean', 'No', 'Displays the container title (default value is true).'], ['active', 'boolean', 'No', 'Activates/deactivates the container (default value is true).'], ['autoSync', 'boolean', 'No', 'Synchronizes/does not synchronize the container local state with the backend (default value is true).'], ['onCartSyncError', 'function', 'No', 'A function that takes a ShippingMethod object and an error as arguments. It is called when the setShippingMethodsOnCart() API throws an error when a shipping method is selected to be stored to the backend.'], ['onSelectionChange', 'function', 'No', 'A function that takes a ShippingMethod object as an argument. It is called when a shipping method is selected.'], ['slots (*)', 'object', 'No', 'Object with the title to be displayed on the `ShippingMethods` container and optional `ShippingMethodItem` slot for a fully custom row per method (icons, descriptions, badges, layout) while keeping selection behavior. See [ShippingMethods slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/slots/#shippingmethods-slots).'], ['UIComponentType', 'string', 'No', 'String with the UI component type to be used as selector (default value is \'RadioButton\').'], ] ``` (*) Properties inherited from `TitleProps` These configuration options are implementing the `ShippingMethodsProps` interface: ### ShippingMethodsProps interface The `ShippingMethods` container receives an object as a parameter which implements the `ShippingMethodsProps` interface with the following properties: ```ts interface CartSyncError { method: ShippingMethod; error: Error; } /** Context for the ShippingMethodItem slot (published `ShippingMethodItemContext`). */ export interface ShippingMethodItemContext { method: ShippingMethod; isSelected: boolean; onSelect: () => void; } export interface ShippingMethodsProps extends HTMLAttributes, TitleProps { active?: boolean; autoSync?: boolean; onCartSyncError?: (error: CartSyncError) => void; onSelectionChange?: (method: ShippingMethod) => void; UIComponentType?: UIComponentType; slots?: { ShippingMethodItem?: SlotProps; } & TitleProps['slots']; } ``` - The `displayTitle (*)` property is inherited from the `TitleProps` interface. It is used to determine whether to display the title. - Set the `active` property to _true_ to have the container in reactive mode (it is visible and responds to system events). If it is set to _false_, the container is deactivated (it does not subscribe to system events and is not rendered). - Set the `autoSync` property to _true_ to automatically synchronize the container state changes with the backend via API calls. If it is set to _false_ the container does not automatically synchronize its state, but still maintains local updates. - The `onCartSyncError` property is a handler used to perform actions called when a shipping method is selected and the setShippingMethodsOnCart() API throws an error. It could be used in the integration layer by the merchant to show errors. - The `onSelectionChange` property is a handler used to perform actions called when a shipping method is selected. - The `UIComponentType` property is a string containing the name of the UI component type to be used as a selector for each shipping method. The available UI components are: `ToggleButton` or `RadioButton`. - The `slots (*)` property is inherited from the `TitleProps` interface. It is an object that contains the following properties: - Use the `Title (*)` property to render a custom title. This property is inherited from `TitleProps` interface. - Use the `ShippingMethodItem` property to fully replace the default UI for each shipping method (for example, add an icon, description, or badge next to the price). The slot context provides `method`, `isSelected`, and `onSelect` per `ShippingMethodItemContext`. See [ShippingMethods slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/slots/#shippingmethods-slots) for details, including how `busy` relates to the internal presentation component rather than the slot context. ### ShippingMethod model Each shipping method is represented by the following model: ```ts type ShippingMethod = { amount: Money; carrier: { code: string; title: string; }; code: string; title: string; value: string; amountExclTax?: Money; amountInclTax?: Money; originalAmount?: Money; }; ``` ## Strikethrough pricing The `ShippingMethods` container supports displaying strikethrough pricing for discounted shipping methods. When a shipping method includes an `originalAmount` field, the component automatically displays the original price crossed out next to the discounted price, making promotional shipping offers more visible to customers. To enable this feature, merchants must: 1. **Extend the GraphQL schema on the backend** to add an `original_amount` field to the `AvailableShippingMethod` type with `value` and `currency` subfields. 2. **Extend the checkout GraphQL fragment** to request the `original_amount` field by modifying the `build.mjs` script: ```js title='build.mjs' overrideGQLOperations([ { npm: '@dropins/storefront-checkout', operations: [ ` fragment CHECKOUT_DATA_FRAGMENT on Cart { shipping_addresses { available_shipping_methods { original_amount { value currency } } } } `, ], }, ]); ``` When the `original_amount` field is present in the GraphQL response, the component automatically renders it with a strikethrough style next to the discounted price. ## Example The following example renders the `ShippingMethods` container on a checkout page. It includes configurations to hide the title, show a message if the chosen shipping method is `Best Way` (Table Rate), and show an error message in case there was an issue saving the selected shipping method to the backend. ```ts const $delivery = checkoutFragment.querySelector('.checkout__delivery'); CheckoutProvider.render(ShippingMethods, { displayTitle: false, onCartSyncError: ({ method, error }) => { const shippingMsg = document.createElement('div'); shippingMsg.style.color = 'red'; shippingMsg.innerText = `Error selecting the Shipping Method ${method.code} for the carrier ${method.carrier.title}: ${error.message}`; $delivery.appendChild(shippingMsg); }, onSelectionChange: (method) => { if (method.carrier.code === 'tablerate' && method.code === 'bestway') { const shippingMsg = document.createElement('div'); shippingMsg.innerText = 'Shipping method not available for Canary Islands'; $delivery.appendChild(shippingMsg); } }, })($delivery), ``` --- # TermsAndConditions container The `TermsAndConditions` container displays a checkbox that users must select to agree to the terms and conditions of the sale before confirming their purchase. During the checkout process, users must check all required agreements before placing an order. If an agreement is unchecked, a validation error appears when the user clicks the **Place Order** button. > **TermsAndConditions not displayed for any reason?** - The `TermsAndConditions` container requires a store configuration to be enabled; so it won't be displayed if the component is not properly configured. Visit the [Terms & Conditions setup](https://experienceleague.adobe.com/developer/commerce/storefront/merchants/content-customizations/terms-and-conditions/) documentation for more information on how to enable the Terms and Conditions feature. > **TermsAndConditions requirements** In order to use the `TermsAndConditions` container, the **Storefront Compatibility Package (SCP) 4.7.1-beta8** (or higher) module must be installed. The **SCP 4.7.1-beta8** added support for retrieving Terms and Conditions configuration setting via the _StoreConfig_ GraphQL query. This setting is required by `TermsAndConditions` container to allow frontend applications to dynamically enable and configure agreements by store-view in checkout page. ## TermsAndConditions configurations The `TermsAndConditions` container provides the following configuration options: ```text [ ['Option', 'Type', 'Req?', 'Description'], ['active', 'boolean', 'No', 'Activates/deactivates the container (default value is true).'], ['slots', 'object', 'No', 'Object with a list of agreements to be accepted by the user.'], ] ``` These configuration options implement the `TermsAndConditionsProps` interface: ### TermsAndConditionsProps interface The `TermsAndConditions` container receives an object as a parameter which implements the `TermsAndConditionsProps` interface with the following properties: ```ts export interface TermsAndConditionsProps { active?: boolean; slots?: { Agreements?: SlotProps<{ appendAgreement: SlotMethod<{ name: string; mode: AgreementMode; text?: string; translationId?: string; }>; }>; }; } ``` - Set the `active` property to _true_ to have the container in reactive mode (it is visible and responds to system events). If it is set to _false_, the container is deactivated (it does not subscribe to system events and is not rendered). - The `slots` property is an object containing the following properties: - The `Agreements` property is a handler used to render and configure the list of agreements. It provides a context by including the method `appendAgreement()` to add a new agreement: ```ts export type SlotProps = ( ctx: T & DefaultSlotContext, element: HTMLDivElement | null ) => Promise | void; export type SlotMethod

= ( callback: (next: unknown, state: State) => P ) => void; export enum AgreementMode { MANUAL = 'manual', AUTO = 'auto', } . . . Agreements?: SlotProps<{ appendAgreement: SlotMethod<{ name: string; mode: AgreementMode; text?: string; translationId?: string; }>; }>; . . . ``` - The `appendAgreement` configuration is a callback function which accepts the following attributes to configure an agreement: - **`name`** The agreement identifier - **`mode`** Specifies the mode how the checkbox should appear: - 'manual': the user is required to manually check and accept the conditions to place an order - 'auto': the checkbox will appear checked by default, conditions are automatically accepted upon checkout - **`text`** Optional attribute that contains directly the text to show, and it accepts HTML with links to a specific page in EDS. In case this attribute is not provided, the `translationId` must to. Finally, if both `text` and `translationId` are provided, the `text` has more preference and its content will be shown - **`translationId`** - This attribute references the translation label that contains the checkbox text. It first looks in the placeholders/checkout.json file for this label identifier, otherwise it looks up the entry in the dictionary. This attribute must be provided if it is not. As a reminder, if both `text` and `translationId` are provided, the `text` has more preference and its content will be shown. ## Example 1: Render a custom agreement The following example renders the `TermsAndConditions` container on the checkout page, displaying a custom agreement that directly includes the label to show along with the link to the EDS page, within the element having the class `.checkout__terms-and-conditions`: ```ts // Checkout Dropin const $termsAndConditions = checkoutFragment.querySelector( '.checkout__terms-and-conditions', ); CheckoutProvider.render(TermsAndConditions, { slots: { Agreements: (ctx) => { ctx.appendAgreement(() => ({ name: 'custom', mode: 'auto', text: 'Custom terms and conditions [Terms & Conditions](/en/terms-and-conditions).', })); }, }, })($termsAndConditions), ``` ## Example 2: Render three different agreements using the translations configured in EDS The following example renders the `TermsAndConditions` container on the checkout page. The container displays three different agreements using the labels from the translations in the **`placeholders`** sheet, within the element with the class `.checkout__terms-and-conditions`: ```ts // Checkout Dropin const $termsAndConditions = checkoutFragment.querySelector( '.checkout__terms-and-conditions', ); CheckoutProvider.render(TermsAndConditions, { slots: { Agreements: (ctx) => { ctx.appendAgreement(() => ({ name: 'default', mode: 'auto', translationId: 'Checkout.TermsAndConditions.label', })); ctx.appendAgreement(() => ({ name: 'terms', mode: 'manual', translationId: 'Checkout.TermsAndConditions.terms_label', })); ctx.appendAgreement(() => ({ name: 'privacy', mode: 'auto', translationId: 'Checkout.TermsAndConditions.privacy_label', })); }, }, })($termsAndConditions), ``` ## Example 3: Render the available agreements configured in the Admin Panel The following example renders the `TermsAndConditions` container on a checkout page, displaying the available agreements configured in the Admin Panel retrieved using the `getCheckoutAgreements()` API function, in the element with the class `.checkout__terms-and-conditions`: ```ts // Checkout Dropin const $termsAndConditions = checkoutFragment.querySelector( '.checkout__terms-and-conditions', ); CheckoutProvider.render(TermsAndConditions, { slots: { Agreements: async (ctx) => { const agreements = await checkoutApi.getCheckoutAgreements(); agreements.forEach((agreement) => { ctx.appendAgreement(() => ({ name: agreement.name, mode: agreement.mode, text: agreement.text, })); }); }, }, })($termsAndConditions), ``` --- # Checkout Dictionary The **Checkout dictionary** contains all user-facing text, labels, and messages displayed by this drop-in. Customize the dictionary to: - **Localize** the drop-in for different languages and regions - **Customize** labels and messages to match your brand voice - **Override** default text without modifying source code for the drop-in Dictionaries use the **i18n (internationalization)** pattern, where each text string is identified by a unique key path. Version: 3.3.0 ## How to customize Override dictionary values during drop-in initialization. The drop-in deep-merges your custom values with the defaults. ```javascript await initialize({ langDefinitions: { en_US: { "Checkout": { "AddressValidation": { "title": "My Custom Title", "subtitle": "My Custom Title" } } } } }); ``` You only need to include the keys you want to change. For multi-language support and advanced patterns, see the [Dictionary customization guide](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/dictionaries/). ## Default keys and values Below are the default English (`en_US`) strings provided by the **Checkout** drop-in: ```json title="en_US.json" { "Checkout": { "AddressValidation": { "title": "Verify your address", "subtitle": "To ensure accurate delivery, we suggest the changes highlighted below. Please choose which address you would like to use. If neither option is correct, edit your address.", "suggestedAddress": "Suggested Address", "originalAddress": "Original Address" }, "BillToShippingAddress": { "cartSyncError": "We were unable to save your changes. Please try again later.", "title": "Bill to shipping address" }, "EmptyCart": { "button": "Start shopping", "title": "Your cart is empty" }, "EstimateShipping": { "estimated": "Estimated Shipping", "freeShipping": "Free", "label": "Shipping", "taxToBeDetermined": "TBD", "withoutTaxes": "Excluding taxes", "withTaxes": "Including taxes" }, "LoginForm": { "account": "Already have an account?", "ariaLabel": "Email", "emailExists": { "alreadyHaveAccount": "It looks like you already have an account.", "forFasterCheckout": "for a faster checkout.", "signInButton": "Sign in" }, "floatingLabel": "Email *", "invalidEmailError": "Please enter a valid email address.", "missingEmailError": "Enter an email address.", "cartSyncError": "We were unable to save your changes. Please try again later.", "placeholder": "Enter your email address", "signIn": "Sign In", "signOut": "Sign Out", "switch": "Do you want to switch account?", "title": "Contact details" }, "MergedCartBanner": { "items": { "many": "{{count}} items from a previous session were added to your cart. Please review your new subtotal.", "one": "1 item from a previous session was added to your cart. Please review your new subtotal." } }, "OutOfStock": { "actions": { "removeOutOfStock": "Remove out of stock items", "reviewCart": "Review cart" }, "alert": "Out of stock!", "lowInventory": { "many": "Only {{count}} left!", "one": "Last item!" }, "message": "The following items are out of stock:", "title": "Your cart contains items that are out of stock" }, "PaymentMethods": { "cartSyncError": "We were unable to save your changes. Please try again later.", "emptyState": "No payment methods available", "title": "Payment" }, "PaymentOnAccount": { "referenceNumberLabel": "Custom Reference Number", "referenceNumberPlaceholder": "Enter custom reference number", "referenceNumberHint": "", "availableCreditLabel": "Available Credit", "exceedLimitWarning": "The credit limit is {{creditLimit}}. It will be exceeded by {{exceededAmount}} with this order.", "exceedLimitWarningPrefix": "The credit limit is", "exceedLimitWarningMiddle": ". It will be exceeded by", "exceedLimitWarningSuffix": "with this order.", "exceedLimitError": "Payment On Account cannot be used for this order because your order amount exceeds your credit amount." }, "PurchaseOrder": { "missingReferenceNumberError": "Reference number is required", "referenceNumberHint": "", "referenceNumberLabel": "Custom Reference Number", "referenceNumberPlaceholder": "Enter custom reference number" }, "PlaceOrder": { "button": "Place Order" }, "ServerError": { "button": "Try again", "contactSupport": "If you continue to have issues, please contact support.", "title": "We were unable to process your order", "unexpected": "An unexpected error occurred while processing your order. Please try again later.", "permissionDenied": "You do not have permission to complete checkout. Please contact your administrator for assistance." }, "Quote": { "permissionDenied": "You do not have permission to checkout with this quote.", "dataError": "We were unable to retrieve the quote data. Please try again later." }, "ShippingMethods": { "cartSyncError": "We were unable to save your changes. Please try again later.", "emptyState": "This order can't be shipped to the address provided. Please review the address details you entered and make sure they're correct.", "title": "Shipping options", "accessibleOptionLabel": "Shipping option" }, "Summary": { "Edit": "Edit", "heading": "Your Cart ({count})" }, "Addresses": { "billToNewAddress": "Bill to new address", "shippingAddressTitle": "Shipping address", "billingAddressTitle": "Billing address" }, "TermsAndConditions": { "error": "Please accept the Terms and Conditions to continue.", "label": "I have read, understand, and accept our [Terms of Use, Terms of Sales, Privacy Policy, and Return Policy](https://www.adobe.com/legal/terms.html)." }, "title": "Checkout" } } ``` --- # Error handling Errors that occur during the checkout process must be caught and logged with clear context for quick resolution. This prevents unnecessary error propagation and provides better user experience and debugging capabilities. The checkout drop-in component must implement an error handling mechanism to improve observability and debugging capabilities. It is critical to resolve errors promptly to avoid inconsistent states and clearly inform users about what occurred. This prevents data inconsistencies between the local application and the backend, which could result in incorrect orders. ## Generic strategy Most issues arise from API call errors. The system must focus on how these errors propagate from API calls to the user interface and how they are presented to users in a friendly manner across different scenarios. Each container requires a centralized error handling system that captures errors as they occur, enabling control over error management and decision-making about subsequent actions. ## "Optimistic" UI updates with rollback pattern The system implements optimistic UI updates with a rollback mechanism. This technique improves user experience by making the application feel more responsive to user interactions. In an optimistic update, the UI behaves as though a change was successfully completed before receiving confirmation from the backend that it actually occurred. The system optimistically assumes it will eventually receive confirmation rather than an error. This approach allows for a more responsive user experience. When a user performs an action that changes the state, the system immediately sends the information to the backend and optimistically updates the user interface (UI) to reflect the change. This process is called "optimistic" because the system updates the UI with the expectation that the backend will accept the state change. If the system waited for backend confirmation before updating the UI, the delay would negatively impact the user experience. If the backend returns an error, the system performs a rollback to revert to the previous state (when possible) and displays an error message such as an inline alert. Additionally, the containers provide callback functions that merchants can use in the integration layer to display custom error messages. --- # Event handling The checkout drop-in component implements an event-driven architecture that uses the `@adobe-commerce/event-bus` package to facilitate communication between components. This event system enables containers to respond to application state changes, maintain loose coupling between components, and keep their state synchronized with the cart. ## Event system architecture The system uses a publish-subscribe pattern where containers can: 1. Subscribe to specific events using `events.on()` 2. Emit events using `events.emit()` 3. Unsubscribe using `subscription.off()` ## Events declaration The following code snippet shows the contracts that define the relationship between each event and its payload: ```js title='event-bus.d.ts' declare module '@adobe-commerce/event-bus' { interface Events { 'cart/initialized': CartModel | null; 'cart/updated': CartModel | null; 'cart/reset': void; 'cart/merged': { oldCartItems: any[] }; 'checkout/initialized': CheckoutData | null; 'checkout/updated': CheckoutData | null; 'checkout/values': ValuesModel; 'shipping/estimate': ShippingEstimate; authenticated: boolean; error: { source: string; type: string; error: Error }; } interface Cart extends CartModel {} } ``` ## Event subscription If a component wants to listen for an event fired in another component, the component must subscribe to that event. ### Subscription configuration To subscribe to an event, you must provide the following information: 1. The name of the event. 2. The event handler, which is a callback function to be executed when a new event is fired (the payload is passed as a parameter). 3. Event subscriptions can include an additional configuration parameter: - `eager: true`: The handler executes immediately if the event has been emitted previously. - `eager: false`: The handler only responds to future emissions of the event. ```js const subscription = events.on('event-name', handler, { eager: true/false }); ``` ### Events subscribed by containers The following list shows the events subscribed by the checkout drop-in component containers: #### (i) External When the event is fired by external components: - `authenticated`: Indicates that a user has authenticated. - `cart/initialized`: Indicates that a new cart has been created and initialized. - `cart/reset`: Indicates that the order has been placed and the cart is not active any more. - `cart/updated`: Indicates that the cart data has been added or updated. - `cart/merged`: Indicates that a guest cart (created during the anonymous checkout) has been merged with a customer cart (recovered from a previous checkout process). - `cart/data`: Provides cart data. - `locale`: Indicates that the locale has been changed. #### (ii) Internal When the event is fired by internal checkout drop-in components: - `checkout/initialized`: Indicates that the checkout drop-in has been initialized with cart data. - `checkout/updated`: Indicates that the checkout data has been added or updated. - `shipping/estimate`: Provides shipping estimate based on shipping method selected within a shipping address. ### Example Listen to the checkout initialization event: ```js events.on('checkout/initialized', (data) => { // Handle checkout data }); ``` ## Event emission Each component can emit an event if it wants to share information with other components or drop-ins. ### Emission configuration To emit an event, you must provide the following information: 1. The name of the event 2. The payload containing the data to be shared ```js events.emit('event-name', payload); ``` ### Events emitted by containers The following list shows the events emitted by the checkout drop-in component containers: - `checkout/initialized`: Indicates that the checkout drop-in has been initialized with cart data. - `checkout/updated`: Indicates that the checkout data has been added or updated. - `checkout/values`: Provides the local state values. - `shipping/estimate`: Provides shipping estimate based on shipping method selected within a shipping address. - `error`: Indicates that the system has received a network error type. ### Example Emit the checkout values event: ```js events.emit('checkout/values', data); ``` --- # Checkout Data & Events The **Checkout** drop-in uses the [event bus](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/events/) to emit and listen to events for communication between drop-ins and external integrations. Version: 3.3.0 ## Events reference {/* EVENTS_TABLE_START */} | Event | Direction | Description | |-------|-----------|-------------| | [checkout/values](#checkoutvalues-emits) | Emits | Emitted when form or configuration values change. | | [cart/data](#cartdata-listens) | Listens | Fired by Cart (`cart`) when data is available or changes. | | [cart/initialized](#cartinitialized-listens) | Listens | Fired by Cart (`cart`) when the component completes initialization. | | [cart/merged](#cartmerged-listens) | Listens | Fired by Cart (`cart`) when data is merged. | | [cart/reset](#cartreset-listens) | Listens | Fired by Cart (`cart`) when the component state is reset. | | [quote-management/quote-data](#quote-managementquote-data-listens) | Listens | Fired by Quote-management (`quote-management`) when a specific condition or state change occurs. | | [checkout/error](#checkouterror-emits-and-listens) | Emits and listens | Triggered when an error occurs. | | [checkout/initialized](#checkoutinitialized-emits-and-listens) | Emits and listens | Triggered when the component completes initialization. | | [checkout/updated](#checkoutupdated-emits-and-listens) | Emits and listens | Triggered when the component state is updated. | | [shipping/estimate](#shippingestimate-emits-and-listens) | Emits and listens | Triggered when an estimate is calculated. | | [authenticated](#authenticated-listens) | Listens | Fired by Auth (`auth`) when the user authentication state changes. | {/* EVENTS_TABLE_END */} ## Event details The following sections provide detailed information about each event, including its direction, event payload, and usage examples. ### `cart/data` (listens) Triggered when cart data is available or changes. This event provides the current cart state including items, totals, and addresses. #### Event payload ```typescript Cart | null ``` See [`Cart`](#cart) for full type definition. #### Example ```js events.on('cart/data', (payload) => { console.log('cart/data event received:', payload); // Add your custom logic here }); ``` ### `cart/initialized` (listens) Fired by Cart (`cart`) when the component completes initialization. #### Event payload ```typescript CartModel | null ``` See [`CartModel`](#cartmodel) for full type definition. #### Example ```js events.on('cart/initialized', (payload) => { console.log('cart/initialized event received:', payload); // Add your custom logic here }); ``` ### `cart/merged` (listens) Fired by Cart (`cart`) when data is merged. #### Event payload ```typescript { oldCartItems: any[] } ``` #### Example ```js events.on('cart/merged', (payload) => { console.log('cart/merged event received:', payload); // Add your custom logic here }); ``` ### `cart/reset` (listens) Fired by Cart (`cart`) when the component state is reset. #### Event payload #### Example ```js events.on('cart/reset', (payload) => { console.log('cart/reset event received:', payload); // Add your custom logic here }); ``` ### `checkout/error` (emits and listens) Triggered when an error occurs during checkout operations such as address validation, payment processing, or order placement. #### Event payload ```typescript CheckoutError ``` See [`CheckoutError`](#checkouterror) for full type definition. #### Example ```js events.on('checkout/error', (payload) => { console.log('checkout/error event received:', payload); // Add your custom logic here }); ``` ### `checkout/initialized` (emits and listens) Triggered when the checkout component completes initialization with either cart or negotiable quote data. This indicates the checkout is ready for user interaction. #### Event payload ```typescript Cart | NegotiableQuote | null ``` See [`Cart`](#cart), [`NegotiableQuote`](#negotiablequote) for full type definitions. #### Example ```js events.on('checkout/initialized', (payload) => { console.log('checkout/initialized event received:', payload); // Add your custom logic here }); ``` ### `checkout/updated` (emits and listens) Triggered when the checkout state is updated, such as when shipping methods are selected, addresses are entered, or payment methods are chosen. #### Event payload ```typescript Cart | NegotiableQuote | null ``` See [`Cart`](#cart), [`NegotiableQuote`](#negotiablequote) for full type definitions. #### Example ```js events.on('checkout/updated', (payload) => { console.log('checkout/updated event received:', payload); // Add your custom logic here }); ``` ### `checkout/values` (emits) Emitted when form or configuration values change in the checkout. This event is useful for tracking user input, validating form fields, or synchronizing state across components. #### Event payload ```typescript ValuesModel ``` See [`ValuesModel`](#valuesmodel) for full type definition. #### Example ```js events.on('checkout/values', (payload) => { console.log('checkout/values event received:', payload); // Add your custom logic here }); ``` ### `quote-management/quote-data` (listens) Fired by Quote-management (`quote-management`) when a specific condition or state change occurs. #### Event payload ```typescript { quote: NegotiableQuoteModel; permissions: { requestQuote: boolean; editQuote: boolean; deleteQuote: boolean; checkoutQuote: boolean; } } ``` See [`NegotiableQuoteModel`](#negotiablequotemodel) for full type definition. #### Example ```js events.on('quote-management/quote-data', (payload) => { console.log('quote-management/quote-data event received:', payload); // Add your custom logic here }); ``` ### `shipping/estimate` (emits and listens) Triggered when shipping cost estimates are calculated for a given address. This event provides both the address used for estimation and the resulting shipping method with its cost. #### Event payload ```typescript ShippingEstimate ``` See [`ShippingEstimate`](#shippingestimate) for full type definition. #### Example ```js events.on('shipping/estimate', (payload) => { console.log('shipping/estimate event received:', payload); // Add your custom logic here }); ``` ### `authenticated` (listens) Fired by Auth (`auth`) when the user authentication state changes. Checkout listens to this event to update the `LoginForm` display — hiding the sign-in prompt when a user is authenticated and restoring it when they sign out. #### Event payload ```typescript boolean ``` The payload is `true` if the user is authenticated, `false` otherwise. #### Example ```js events.on('authenticated', (isAuthenticated) => { console.log('authenticated event received:', isAuthenticated); // Add your custom logic here }); ``` ## Data Models The following data models are used in event payloads for this drop-in. ### Cart The `Cart` interface represents a shopping cart including items, pricing, addresses, and shipping/payment methods. Used in: [`cart/data`](#cartdata-listens), [`checkout/initialized`](#checkoutinitialized-emits-and-listens), [`checkout/updated`](#checkoutupdated-emits-and-listens). ```ts interface Cart { type: 'cart'; availablePaymentMethods?: PaymentMethod[]; billingAddress?: CartAddress; email?: string; id: string; isEmpty: boolean; isGuest: boolean; isVirtual: boolean; selectedPaymentMethod?: PaymentMethod; shippingAddresses: CartShippingAddress[]; } ``` ### CartModel Used in: [`cart/initialized`](#cartinitialized-listens). ```ts interface CartModel { id: string; totalQuantity: number; errors?: ItemError[]; items: Item[]; miniCartMaxItems: Item[]; total: { includingTax: Price; excludingTax: Price; }; discount?: Price; subtotal: { excludingTax: Price; includingTax: Price; includingDiscountOnly: Price; }; appliedTaxes: TotalPriceModifier[]; totalTax?: Price; appliedDiscounts: TotalPriceModifier[]; shipping?: Price; isVirtual?: boolean; addresses: { shipping?: { countryCode: string; zipCode?: string; regionCode?: string; }[]; }; isGuestCart?: boolean; } ``` ### CheckoutError Used in: [`checkout/error`](#checkouterror-emits-and-listens). ```ts interface CheckoutError { /** * The primary, user-friendly error message. This should be safe to display * directly in the UI. * @example "Your card was declined." */ message: string; /** * An optional, unique error code for programmatic handling. This allows the * ServerError component to show specific icons, links, or actions. * @example "payment_intent_declined" */ code?: string; } ``` ### NegotiableQuote The `NegotiableQuote` interface represents a B2B negotiable quote, which functions similarly to a cart but includes additional negotiation features like price adjustments and approval workflows. Used in: [`checkout/initialized`](#checkoutinitialized-emits-and-listens), [`checkout/updated`](#checkoutupdated-emits-and-listens). ```ts interface NegotiableQuote { type: 'quote'; availablePaymentMethods?: PaymentMethod[]; billingAddress?: Address; email?: string; isEmpty: boolean; isVirtual: boolean; name: string; selectedPaymentMethod?: PaymentMethod; shippingAddresses: ShippingAddress[]; status: NegotiableQuoteStatus; uid: string; } ``` ### NegotiableQuoteModel Used in: [`quote-management/quote-data`](#quote-managementquote-data-listens). ```ts interface NegotiableQuoteModel { uid: string; name: string; createdAt: string; salesRepName: string; expirationDate: string; updatedAt: string; status: NegotiableQuoteStatus; buyer: { firstname: string; lastname: string; }; templateName?: string; comments?: { uid: string; createdAt: string; author: { firstname: string; lastname: string; }; text: string; attachments?: { name: string; url: string; }[]; }[]; history?: NegotiableQuoteHistoryEntry[]; prices: { appliedDiscounts?: Discount[]; appliedTaxes?: Tax[]; discount?: Currency; grandTotal?: Currency; grandTotalExcludingTax?: Currency; shippingExcludingTax?: Currency; shippingIncludingTax?: Currency; subtotalExcludingTax?: Currency; subtotalIncludingTax?: Currency; subtotalWithDiscountExcludingTax?: Currency; totalTax?: Currency; }; items: NegotiableQuoteCartItem[]; shippingAddresses?: ShippingAddress[]; canCheckout: boolean; canSendForReview: boolean; } ``` ### ShippingEstimate Used in: [`shipping/estimate`](#shippingestimate-emits-and-listens). ```ts interface ShippingEstimate { address: PartialShippingAddress; availableShippingMethods?: ShippingMethod[]; shippingMethod: ShippingEstimateShippingMethod | null; success?: boolean; } ``` ### ValuesModel Used in: [`checkout/values`](#checkoutvalues-emits). ```ts interface ValuesModel { email: string; isBillToShipping: boolean | undefined; selectedPaymentMethod: PaymentMethod | null; selectedShippingMethod: ShippingMethod | null; } ``` --- # Extending the checkout drop-in component The checkout drop-in component follows the Adobe Commerce out-of-process extensibility (OOPE) pattern, which requires components to be flexible and extensible. When the checkout drop-in component lacks a specific feature, it provides mechanisms that allow developers to easily expand and customize its functionality. ## GraphQL API To extend the data payload of the drop-in, developers must use the GraphQL Extensibility API. This API allows developers to extend existing GraphQL operations to meet additional data requirements without increasing code complexity or negatively impacting performance. The API provides a flexible and efficient way to customize GraphQL fragments by integrating build-time modifications into the storefront's development pipeline. GraphQL fragments are reusable pieces of GraphQL that developers can use to extend or customize the API for a drop-in component. Drop-in components expose the list of fragments that can be extended in the `fragments.ts` file. If the drop-in component does not expose these fragments, the build process fails when you install the application because it cannot locate the fragment you want to extend. The checkout drop-in component exposes the following fragments: ```js title='fragments.ts' export { BILLING_CART_ADDRESS_FRAGMENT, SHIPPING_CART_ADDRESS_FRAGMENT, } from '@/checkout/api/graphql/CartAddressFragment.graphql'; export { CHECKOUT_DATA_FRAGMENT } from '@/checkout/api/graphql/CheckoutDataFragment.graphql'; export { CUSTOMER_FRAGMENT } from '@/checkout/api/graphql/CustomerFragment.graphql'; export { NEGOTIABLE_QUOTE_BILLING_ADDRESS_FRAGMENT, NEGOTIABLE_QUOTE_SHIPPING_ADDRESS_FRAGMENT, } from '@/checkout/api/graphql/NegotiableQuoteAddressFragment.graphql'; export { NEGOTIABLE_QUOTE_FRAGMENT } from '@/checkout/api/graphql/NegotiableQuoteFragment.graphql'; export { AVAILABLE_PAYMENT_METHOD_FRAGMENT, SELECTED_PAYMENT_METHOD_FRAGMENT, } from '@/checkout/api/graphql/PaymentMethodFragment.graphql'; export { AVAILABLE_SHIPPING_METHOD_FRAGMENT, ESTIMATE_SHIPPING_METHOD_FRAGMENT, SELECTED_SHIPPING_METHOD_FRAGMENT, } from '@/checkout/api/graphql/ShippingMethodFragment.graphql'; ``` The fragment names above match the symbols exported from `@dropins/storefront-checkout` (for example, the package `fragments` entry). The `@/checkout/...` import paths reflect the checkout drop-in source layout; in your storefront, point `build.mjs` at the same fragment names using whatever `fragments.ts` path and re-exports your scaffold provides. The `ESTIMATE_SHIPPING_METHOD_FRAGMENT` applies to the `estimateShippingMethods` mutation. Pair it with the `EstimateShippingModel` initializer model when you need to transform extended fields from the shipping estimate response. `AVAILABLE_SHIPPING_METHOD_FRAGMENT` and `SELECTED_SHIPPING_METHOD_FRAGMENT` cover cart shipping methods on the main checkout flow. ### Extend or customize a fragment To make GraphQL fragments extensible in the drop-in component, you must first update the GraphQL fragment that the drop-in uses to request the additional field. You accomplish this by modifying the `build.mjs` script located at the root of your storefront project. The `build.mjs` script automatically generates a new GraphQL query for the checkout drop-in component when you run the install command. This generated query includes the additional data that you specified in your fragment extensions. #### Example 1: Adding new information The merchant wants to extend the customer information by adding the gender and date of birth data. ```js title='build.mjs' /* eslint-disable import/no-extraneous-dependencies */ overrideGQLOperations([ { npm: '@dropins/storefront-checkout', operations: [ ` fragment CUSTOMER_FRAGMENT on Customer { gender date_of_birth } `, ], }, ]); ``` After extending the API, you must extend the models and transformers during the initialization phase if data transformation is required. You accomplish this by modifying the `/scripts/initializers/checkout.js` script. ```js title='/scripts/initializers/checkout.js' // Initialize checkout await initializeDropin(async () => { // Register the checkout component with models extensibility const models = { CustomerModel: { transformer: (data) => ({ gender: ((gender) => { switch (gender) { case 1: return "Male"; case 2: return "Female"; case 3: return "Not Specified"; default: return ""; } })(data?.gender), dateOfBirth: data?.date_of_birth, }), }, }; // Register initializers return initializers.mountImmediately(initialize, { models }); })(); ``` #### Example 2: Removing information The merchant wants to remove the selected payment method data. ```js title='build.mjs' /* eslint-disable import/no-extraneous-dependencies */ overrideGQLOperations([ { npm: '@dropins/storefront-checkout', skipFragments: ['SELECTED_PAYMENT_METHOD_FRAGMENT'], operations: [], }, ]); ``` > **Extending fragments** If the `build.mjs` script references a fragment that the drop-in component does not expose, the application build process fails. > **Extending drop-in components** See the [GraphQL Extensibility API](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/graphql/) and [Extending drop-in components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/extending/) documentation to learn more about how to extend the API for a drop-in component. --- # Checkout Functions The Checkout drop-in provides API functions that enable you to programmatically control behavior, fetch data, and integrate with Adobe Commerce backend services. Version: 3.3.0 | Function | Description | | --- | --- | | [`authenticateCustomer`](#authenticatecustomer) | API function for the drop-in. | | [`estimateShippingMethods`](#estimateshippingmethods) | Calls the `estimateShippingMethods` mutation. | | [`getCart`](#getcart) | Retrieves the current cart's checkout data from Adobe Commerce. | | [`getCheckoutAgreements`](#getcheckoutagreements) | Returns a list with the available checkout agreements. | | [`getCompanyCredit`](#getcompanycredit) | API function for the drop-in. | | [`getCustomer`](#getcustomer) | API function for the drop-in. | | [`getNegotiableQuote`](#getnegotiablequote) | Retrieves a negotiable quote for B2B customers. | | [`getStoreConfig`](#getstoreconfig) | The `storeConfig` query defines information about a store's configuration. | | [`getStoreConfigCache`](#getstoreconfigcache) | API function for the drop-in. | | [`initializeCheckout`](#initializecheckout) | API function for the drop-in. | | [`isEmailAvailable`](#isemailavailable) | Calls the `isEmailAvailable` query. | | [`resetCheckout`](#resetcheckout) | API function for the drop-in. | | [`setBillingAddress`](#setbillingaddress) | Calls the `setBillingAddressOnCart` mutation. | | [`setGuestEmailOnCart`](#setguestemailoncart) | Calls the `setGuestEmailOnCart` mutation. | | [`setPaymentMethod`](#setpaymentmethod) | Calls the `setPaymentMethodOnCart` mutation. | | [`setShippingAddress`](#setshippingaddress) | Calls the `setShippingAddressesOnCart` mutation. | | [`setShippingMethods`](#setshippingmethods) | Sets one or more shipping methods on the cart. Also exported as `setShippingMethodsOnCart`. | | [`synchronizeCheckout`](#synchronizecheckout) | API function for the drop-in. | ## authenticateCustomer ### Signature ```typescript function authenticateCustomer(authenticated = false): Promise ``` ### Parameters | Parameter | Type | Required | Description | |---|---|---|---| --- ## estimateShippingMethods The `estimateShippingMethods` function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/estimate-shipping-methods/ mutation. ```ts const estimateShippingMethods = async ( input?: EstimateShippingInput ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | `EstimateShippingInput` | No | An object of type EstimateShippingInput, which contains a criteria object including the following fields: country_code, region_name, region_id, and zip. | ### Events Emits the [`shipping/estimate`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/events/#shippingestimate-emits-and-listens) event. ### Returns Returns an array of [`ShippingMethod`](#shippingmethod) objects or `null`. ## getCart The `getCart` function retrieves the current cart's checkout data from Adobe Commerce. It automatically uses the cart ID from internal state and calls either the `getCart` or `customerCart` `GraphQL` query depending on authentication status. The returned data includes billing address, shipping addresses, available and selected payment methods, email, total quantity, and virtual cart status—all the information needed to complete the checkout process. ```ts const getCart = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns a [`Cart`](#cart) model containing complete checkout information. ## getCheckoutAgreements The `getCheckoutAgreements` function returns a list with the available checkout agreements. Each agreement has a name and the mode (manual or automatic). ```ts const getCheckoutAgreements = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns an array of [`CheckoutAgreement`](#checkoutagreement) objects. ## getCompanyCredit ```ts const getCompanyCredit = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns [`CompanyCredit`](#companycredit) or `null`. ## getCustomer ```ts const getCustomer = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns [`Customer`](#customer) or `null`. ## getNegotiableQuote The `getNegotiableQuote` function retrieves a negotiable quote for B2B customers. The function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/b2b/negotiable-quote/queries/quote/ query. ```ts const getNegotiableQuote = async ( input: GetNegotiableQuoteInput = {} ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | `GetNegotiableQuoteInput` | No | Input parameters including the quote UID to retrieve. | ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## getStoreConfig The `storeConfig` query defines information about a store's configuration. You can query a non-default store by changing the header in your `GraphQL` request. ```ts const getStoreConfig = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## getStoreConfigCache ```ts const getStoreConfigCache = async (): any ``` ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## initializeCheckout ### Signature ```typescript function initializeCheckout(input: InitializeInput): Promise ``` ### Parameters | Parameter | Type | Required | Description | |---|---|---|---| | `input` | `InitializeInput` | Yes | | --- ## isEmailAvailable The `isEmailAvailable` function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/customer/queries/is-email-available/ query. ```ts const isEmailAvailable = async ( email: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `email` | `string` | Yes | A string representing the email address to check for availability. | ### Events Does not emit any drop-in events. ### Returns Returns [`EmailAvailability`](#emailavailability). ## resetCheckout ```ts const resetCheckout = async (): any ``` ### Events Emits the [`checkout/updated`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/events/#checkoutupdated-emits-and-listens) event. ### Returns Returns `void`. ## setBillingAddress The `setBillingAddress` function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/set-billing-address/ mutation. ```ts const setBillingAddress = async ( input: BillingAddressInputModel ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | `BillingAddressInputModel` | Yes | The billing address to set on the cart, including street, city, region, country, and postal code. | ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## setGuestEmailOnCart The `setGuestEmailOnCart` function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/set-guest-email/ mutation. ```ts const setGuestEmailOnCart = async ( email: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `email` | `string` | Yes | The guest customer's email address for order confirmation and communication. | ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## setPaymentMethod The `setPaymentMethod` function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/set-payment-method/ mutation. ```ts const setPaymentMethod = async ( input: PaymentMethodInputModel ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | `PaymentMethodInputModel` | Yes | The payment method code and additional payment data required by the selected payment processor. | ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## setShippingAddress The `setShippingAddress` function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/set-shipping-address/ mutation. ```ts const setShippingAddress = async ( input: ShippingAddressInputModel ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | `ShippingAddressInputModel` | Yes | The shipping address to set on the cart, including street, city, region, country, and postal code. | ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## setShippingMethods The `setShippingMethods` function sets one or more shipping methods on the cart. The function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/set-shipping-method/ mutation. ```ts const setShippingMethods = async ( input: Array ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `input` | `Array` | Yes | An array of shipping method objects, each containing a carrier code and method code. | ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## synchronizeCheckout ### Signature ```typescript function synchronizeCheckout(data: SynchronizeInput): Promise ``` ### Parameters | Parameter | Type | Required | Description | |---|---|---|---| | `data` | `SynchronizeInput` | Yes | | --- ## Data Models The following data models are used by functions in this drop-in. ### Cart The `Cart` object is returned by the following functions: [`getCart`](#getcart). ```ts interface Cart { type: 'cart'; availablePaymentMethods?: PaymentMethod[]; billingAddress?: CartAddress; email?: string; id: string; isEmpty: boolean; isGuest: boolean; isVirtual: boolean; selectedPaymentMethod?: PaymentMethod; shippingAddresses: CartShippingAddress[]; } ``` ### CheckoutAgreement The `CheckoutAgreement` object is returned by the following functions: [`getCheckoutAgreements`](#getcheckoutagreements). ```ts interface CheckoutAgreement { content: AgreementContent; id: number; mode: AgreementMode; name: string; text: string; } ``` ### CompanyCredit The `CompanyCredit` object is returned by the following functions: [`getCompanyCredit`](#getcompanycredit). ```ts type CompanyCredit = { availableCredit: Money; exceedLimit?: boolean; }; ``` ### Customer The `Customer` object is returned by the following functions: [`getCustomer`](#getcustomer). ```ts interface Customer { firstName: string; lastName: string; email: string; } ``` ### EmailAvailability The `EmailAvailability` object is returned by the following functions: [`isEmailAvailable`](#isemailavailable). ```ts type EmailAvailability = boolean; ``` ### ShippingMethod The `ShippingMethod` object is returned by the following functions: [`estimateShippingMethods`](#estimateshippingmethods). ```ts type ShippingMethod = { amount: Money; carrier: Carrier; code: string; title: string; value: string; amountExclTax?: Money; amountInclTax?: Money; }; ``` {/* This documentation is auto-generated from the drop-in source repository: REPO_URL */} --- # Checkout overview The checkout drop-in component provides a variety of fully-customizable controls to help complete a purchase. These controls include forms to introduce required information for contact details like email address, delivery and billing addresses, shipping options, and payment methods. Established customers who added items to the cart as a guest have the ability to sign in, automatically loading default addresses and contact details. ## Available resources The checkout drop-in component includes the following resources: - **[API Functions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/functions/)** - Core functions for managing checkout operations like authentication, shipping methods, and order placement - **[Utility Functions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/utilities/)** - Helper functions for DOM manipulation, form handling, data transforms, and more - **[Containers](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/containers/)** - Pre-built UI components for checkout steps - **[Event Handling](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/events/)** - Event-driven architecture for component communication ## Supported Commerce features The following table provides an overview of the Adobe Commerce features that the checkout component supports: | Feature | Status | | ---------------------------------------------------------------------------------- | ----------------------------------------- | | All product types | Supported | | Any checkout flow (BOPIS, one/two step) | Supported | | Any checkout layout | Supported | | Apply coupons to the order | Supported | | Apply gift cards to the order | Supported | | Cart rules | Supported | | Create account after checkout | Supported | | Custom customer address attributes | Supported | | Customer address selection at checkout | Supported | | Customer checkout | Supported | | Customer segments | Supported | | Default customer shipping and billing applied at checkout | Supported | | Extensibility for payment providers | Supported | | Guest checkout | Supported | | Log in during checkout | Supported | | Low product stock alert | Supported | | Out of stock/insufficient quantity products | Supported | | Taxes: Fixed | Supported | | Taxes: Sales, VAT | Supported | | Terms and conditions consent | Supported | | Zero subtotal checkout | Supported | | Multi-step checkout | Supported | | Payment Services vault and other methods that need extra fields (full `additionalData` on `setPaymentMethodOnCart`) | Supported | | Custom row UI per shipping method (`ShippingMethodItem` slot on ShippingMethods) | Supported | | Extensible shipping rate request (`estimateShippingMethods` fragments and models) | Supported | > **Configuration and hosting** Apply gift cards to the order and terms and conditions consent follow your Adobe Commerce checkout configuration. Before you sign off in user acceptance testing, confirm the same rules and extensions are enabled for your hosting model (for example, Adobe Commerce on Cloud Services versus PaaS), because merchants sometimes differ between environments. --- # Checkout initialization The **Checkout initializer** configures the checkout flow, payment processing, shipping options, and order placement. Use initialization to customize checkout behavior, integrate payment providers, and transform checkout data models to match your storefront requirements. Version: 3.3.1 ## Configuration options The following table describes the configuration options available for the **Checkout** initializer: | Parameter | Type | Req? | Description | |---|---|---|---| | `langDefinitions` | [`LangDefinitions`](#langdefinitions) | No | Language definitions for internationalization (i18n). Override dictionary keys for localization or branding. | | `models` | [`Record`](#models) | No | Custom data models for type transformations. Extend or modify default models with custom fields and transformers. | | `defaults` | [`defaults`](#defaults) | No | Configures default checkout behaviors including whether billing address defaults to shipping address and which shipping method is pre-selected. | | `shipping` | [`shipping`](#shipping) | No | Configures shipping method filtering to control which shipping options are available to customers during checkout. | | `features` | [`features`](#features) | No | Enables or disables checkout features including B2B quote functionality and custom login routing. | ## Default configuration The initializer runs with these defaults when no configuration is provided: ```javascript title="scripts/initializers/checkout.js" // All configuration options are optional await initializers.mountImmediately(initialize, { langDefinitions: {}, // Uses built-in English strings models: {}, // Uses default data models // Drop-in-specific defaults: // defaults: undefined // See configuration options below // shipping: undefined // See configuration options below // features: undefined // See configuration options below }); ``` ## Language definitions Override dictionary keys for localization or branding. The `langDefinitions` object maps locale keys to custom strings that override default text for the drop-in. ```javascript title="scripts/initializers/checkout.js" const customStrings = { 'AddToCart': 'Add to Bag', 'Checkout': 'Complete Purchase', 'Price': 'Cost', }; const langDefinitions = { default: customStrings, }; await initializers.mountImmediately(initialize, { langDefinitions }); ``` > For complete dictionary customization including all available keys and multi-language support, see the [Checkout Dictionary](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/dictionary/) page. ## Customizing data models Extend or transform data models by providing custom transformer functions. Use the `models` option to add custom fields or modify existing data structures returned from the backend. ### Available models The following models can be customized through the `models` configuration option: | Model | Description | |---|---| | [`CartModel`](#cartmodel) | Transforms cart data during checkout including items, pricing, shipping, billing, and payment information. Use this to add custom fields specific to the checkout flow. | | [`CustomerModel`](#customermodel) | Transforms `CustomerModel` data from `GraphQL`. | The following example shows how to customize the `CartModel` model for the **Checkout** drop-in: ```javascript title="scripts/initializers/checkout.js" const models = { CartModel: { transformer: (data) => ({ // Add custom fields from backend data customField: data?.custom_field, promotionBadge: data?.promotion?.label, // Transform existing fields displayPrice: data?.price?.value ? `${data.price.value}` : 'N/A', }), }, }; await initializers.mountImmediately(initialize, { models }); ``` ## Drop-in configuration The **Checkout initializer** configures the checkout flow, payment processing, shipping options, and order placement. Use initialization to customize checkout behavior, integrate payment providers, and transform checkout data models to match your storefront requirements. ```javascript title="scripts/initializers/checkout.js" await initializers.mountImmediately(initialize, { defaults: {}, shipping: {}, features: {}, langDefinitions: {}, models: {}, }); ``` > Refer to the [Configuration options](#configuration-options) table for detailed descriptions of each option. ## Configuration types The following TypeScript definitions show the structure of each configuration object: ### defaults Configures default checkout behaviors including whether billing address defaults to shipping address and which shipping method is pre-selected. ```typescript defaults?: { isBillToShipping?: boolean; selectedShippingMethod?: Selector; } ``` ### shipping Configures shipping method filtering to control which shipping options are available to customers during checkout. ```typescript shipping?: { filterOptions?: Filter; } ``` ### features Enables or disables checkout features including B2B quote functionality and custom login routing. ```typescript features?: { b2b?: { quotes?: boolean; routeLogin?: () => string | void; }; } ``` ### langDefinitions Maps locale identifiers to dictionaries of key-value pairs. The `default` locale is used as the fallback when no specific locale matches. Each dictionary key corresponds to a text string used in the drop-in UI. ```typescript langDefinitions?: { [locale: string]: { [key: string]: string; }; }; ``` ### models Maps model names to transformer functions. Each transformer receives data from GraphQL and returns a modified or extended version. Use the `Model` type from `@dropins/tools` to create type-safe transformers. ```typescript models?: { [modelName: string]: Model; }; ``` ## Model definitions The following TypeScript definitions show the structure of each customizable model: ### CartModel ```typescript export interface CartAddress extends Address {} ``` ### CustomerModel ```typescript export interface Customer { firstName: string; lastName: string; email: string; } ``` --- # Checkout Quick Start The Checkout drop-in component provides a customizable UI for the checkout process. The checkout component is designed to be integrated into your storefront and provides a seamless checkout experience for customers. Version: 3.3.0 ## Prerequisites Since the checkout component relies on containers from several other drop-in components, you must install and configure those components before you can use the checkout component. The https://github.com/hlxsites/aem-boilerplate-commerce includes all of the necessary drop-in components and configurations to help you get started quickly, so Adobe recommends relying on the boilerplate instead of installing, configuring, and integrating the drop-in components individually. ## Admin configuration Before you can use the checkout component on your storefront, you must enable and configure https://experienceleague.adobe.com/en/docs/commerce-admin/stores-sales/payments/payments and https://experienceleague.adobe.com/en/docs/commerce-admin/stores-sales/point-of-purchase/checkout/checkout-process in the Adobe Commerce Admin. :::note The checkout [overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/) provides a summary of supported Adobe Commerce features. ::: ## Quick example The Checkout drop-in is included in the https://github.com/hlxsites/aem-boilerplate-commerce. This example shows the basic pattern: ```js // 1. Import initializer (handles all setup) // 2. Import the container you need // 3. Import the provider // 4. Render in your block export default async function decorate(block) { await provider.render(AddressValidation, { // Configuration options - see Containers page })(block); } ``` **New to drop-ins?** See the [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/) guide for complete step-by-step instructions. ## Quick reference **Import paths:** - Initializer: `import '../../scripts/initializers/checkout.js'` - Containers: `import ContainerName from '@dropins/storefront-checkout/containers/ContainerName.js'` - Provider: `import { render } from '@dropins/storefront-checkout/render.js'` **Package:** `@dropins/storefront-checkout` **Version:** 3.3.0 (verify compatibility with your Commerce instance) **Example container:** `AddressValidation` ## Learn more - [Containers](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/containers/) - Available UI components and configuration options - [Initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/initialization/) - Customize initializer settings and data models - [Functions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/functions/) - Control drop-in behavior programmatically - [Events](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/events/) - Listen to and respond to drop-in state changes - [Slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/slots/) - Extend containers with custom content --- # Checkout Slots The Checkout drop-in exposes slots for customizing specific UI sections. Use slots to replace or extend container components. For default properties available to all slots, see [Extending drop-in components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/extending/). Version: 3.3.0 | Container | Slots | |-----------|-------| | [`LoginForm`](#loginform-slots) | `Heading`, `Preferences`, `Title` | | [`PaymentMethods`](#paymentmethods-slots) | `Methods`, `Title` | | [`PlaceOrder`](#placeorder-slots) | `Content` | | [`ShippingMethods`](#shippingmethods-slots) | `ShippingMethodItem`, `Title` | | [`TermsAndConditions`](#termsandconditions-slots) | `Agreements` | ## LoginForm slots The slots for the `LoginForm` container allow you to customize its appearance and behavior. ```typescript interface LoginFormProps { slots?: { Heading?: SlotProps<{ authenticated: boolean; }>; Preferences?: SlotProps<{ email: string; isEmailValid: boolean; isAuthenticated: boolean; }>; Title?: SlotProps>; }; } ``` ### Heading slot The Heading slot allows you to customize the heading section of the `LoginForm` container. #### Example ```js await provider.render(LoginForm, { slots: { Heading: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Heading'; ctx.appendChild(element); } } })(block); ``` ### Preferences slot The Preferences slot allows you to add custom marketing preference fields within the login form. This slot enables merchants to add their own consent options (such as newsletter subscriptions, SMS updates, or promotional offers) based on their specific business needs and compliance requirements. The slot receives a context with the following properties: - `email` - The current email address entered by the user - `isEmailValid` - A boolean indicating whether the email address is valid - `isAuthenticated` - A boolean indicating whether the user is authenticated #### Example ```js await provider.render(LoginForm, { slots: { Preferences: (ctx) => { if (!ctx.isEmailValid || ctx.isAuthenticated) return; const element = document.createElement('div'); element.innerHTML = ` `; ctx.appendChild(element); } } })(block); ``` ### Title slot The Title slot allows you to replace the default section heading rendered by the `LoginForm` container. #### Example ```js await provider.render(LoginForm, { slots: { Title: (ctx) => { const heading = document.createElement('h2'); heading.textContent = 'Sign in to continue'; ctx.replaceWith(heading); } } })(block); ``` ## PaymentMethods slots The slots for the `PaymentMethods` container allow you to customize its appearance and behavior. ```typescript interface PaymentMethodsProps { slots?: { Methods?: PaymentMethodHandlers; Title?: SlotProps>; }; } ``` ### Methods slot The Methods slot allows you to register custom payment method handlers, replacing or augmenting the built-in payment method UI. Pass an object whose keys are payment method codes and values are handler functions. ### Title slot The Title slot allows you to replace the default section heading rendered by the `PaymentMethods` container. #### Example ```js await provider.render(PaymentMethods, { slots: { Title: (ctx) => { const heading = document.createElement('h2'); heading.textContent = 'How would you like to pay?'; ctx.replaceWith(heading); } } })(block); ``` ## PlaceOrder slots The slots for the `PlaceOrder` container allow you to customize its appearance and behavior. ```typescript interface PlaceOrderProps { slots?: { Content?: SlotProps; }; } ``` ### Content slot The Content slot allows you to customize the content section of the `PlaceOrder` container. #### Example ```js await provider.render(PlaceOrder, { slots: { Content: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Content'; ctx.appendChild(element); } } })(block); ``` ## ShippingMethods slots The slots for the `ShippingMethods` container allow you to fully replace the default shipping method UI with a custom implementation. ```typescript interface ShippingMethodsProps { slots?: { ShippingMethodItem?: SlotProps<{ method: ShippingMethod; isSelected: boolean; onSelect: () => void; }>; Title?: SlotProps>; }; } ``` ### ShippingMethodItem slot The ShippingMethodItem slot allows you to replace the default RadioButton or ToggleButton UI for each shipping method with a completely custom element. Use `ctx.replaceWith()` to provide your own UI and `ctx.onRender()` to update it when the context changes. The slot receives a `ShippingMethodItemContext` with the following properties: - `method` - The shipping method data (`ShippingMethod` model with carrier, amount, title, and so on.) - `isSelected` - Whether this method is currently selected - `onSelect` - Callback that selects this shipping method and triggers the API call to set it on the cart The internal presentation component that renders the list also accepts a `busy` flag (see the checkout drop-in `ShippingMethods` UI props) when the flow is waiting on pending checkout updates or a shipping estimate. That state is not part of `ShippingMethodItemContext`. #### Example ```js function buildShippingMethodCard(ctx) { const { method, isSelected } = ctx; const price = method.amount.value === 0 ? 'FREE' : `$${method.amount.value.toFixed(2)}`; const card = document.createElement('label'); card.className = `custom-shipping-card ${isSelected ? 'custom-shipping-card--selected' : ''}`; card.innerHTML = ` ${method.carrier.title} ${method.title} ${price} `; card.querySelector('input').addEventListener('change', () => { ctx.onSelect(); }); return card; } await provider.render(ShippingMethods, { slots: { ShippingMethodItem: (ctx) => { const card = buildShippingMethodCard(ctx); ctx.replaceWith(card); ctx.onRender((updatedCtx) => { card.className = `custom-shipping-card ${updatedCtx.isSelected ? 'custom-shipping-card--selected' : ''}`; card.querySelector('input').checked = updatedCtx.isSelected; }); }, }, })(block); ``` ### Title slot The Title slot allows you to replace the default section heading rendered by the `ShippingMethods` container. #### Example ```js await provider.render(ShippingMethods, { slots: { Title: (ctx) => { const heading = document.createElement('h3'); heading.textContent = 'Choose a delivery option'; ctx.replaceWith(heading); } } })(block); ``` ## TermsAndConditions slots The slots for the `TermsAndConditions` container allow you to customize its appearance and behavior. ```typescript interface TermsAndConditionsProps { slots?: { Agreements?: SlotProps<{ appendAgreement: SlotMethod<{ name: string; mode: AgreementMode; translationId?: string; text?: string; }>; }>; }; } ``` ### Agreements slot The Agreements slot allows you to customize the agreements section of the `TermsAndConditions` container. #### Example ```js await provider.render(TermsAndConditions, { slots: { Agreements: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Agreements'; ctx.appendChild(element); } } })(block); ``` --- # Checkout styles Customize the Checkout drop-in using CSS classes and design tokens. This page covers the Checkout-specific container classes and customization examples. For comprehensive information about design tokens, responsive breakpoints, and styling best practices, see [Styling Drop-In Components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/). Version: 3.3.0 ## Customization example Add this to https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/commerce-checkout/commerce-checkout.css to customize the Checkout drop-in. For a complete list of available design tokens (colors, spacing, typography, and more), see the [Design tokens reference](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/#design-tokens-reference). ```css title="styles/styles.css" del={2-2} ins={3-3} .checkout-out-of-stock__title { color: var(--color-neutral-900); color: var(--color-brand-900); } ``` ## Container classes The Checkout drop-in uses BEM-style class naming. Use the browser DevTools to inspect elements and find specific class names. ```css /* AddressValidation */ .checkout-address-validation {} .checkout-address-validation__option {} .checkout-address-validation__option-title {} .checkout-address-validation__options {} .checkout-address-validation__options--busy {} .checkout-address-validation__subtitle {} .checkout-address-validation__title {} /* BillToShippingAddress */ .checkout-bill-to-shipping-address {} .checkout-bill-to-shipping-address__error {} /* EstimateShipping */ .cart-order-summary__shipping {} .checkout-estimate-shipping {} .checkout-estimate-shipping__caption {} .checkout-estimate-shipping__label {} .checkout-estimate-shipping__label--bold {} .checkout-estimate-shipping__label--muted {} .checkout-estimate-shipping__price {} .checkout-estimate-shipping__price--bold {} .checkout-estimate-shipping__price--muted {} .dropin-skeleton {} /* LoginForm */ .checkout-login-form__content {} .checkout-login-form__customer-details {} .checkout-login-form__customer-email {} .checkout-login-form__customer-name {} .checkout-login-form__heading {} .checkout-login-form__heading-label {} .checkout-login-form__link {} .checkout-login-form__sign-in {} .checkout-login-form__sign-out {} .checkout-login-form__title {} .dropin-field__hint {} /* OutOfStock */ .checkout-out-of-stock {} .checkout-out-of-stock__action {} .checkout-out-of-stock__actions {} .checkout-out-of-stock__item {} .checkout-out-of-stock__items {} .checkout-out-of-stock__message {} .checkout-out-of-stock__title {} .dropin-card {} .dropin-card__content {} /* PaymentMethods */ .checkout-payment-methods--full-width {} .checkout-payment-methods__content {} .checkout-payment-methods__error {} .checkout-payment-methods__methods {} .checkout-payment-methods__spinner {} .checkout-payment-methods__title {} .checkout-payment-methods__wrapper {} .checkout-payment-methods__wrapper--busy {} .checkout__content {} /* PaymentOnAccount */ .checkout-payment-on-account {} .checkout-payment-on-account__credit {} .checkout-payment-on-account__credit-amount {} .checkout-payment-on-account__credit-label {} .checkout-payment-on-account__exceed-message {} .checkout-payment-on-account__form {} .dropin-field {} /* PlaceOrder */ .checkout-place-order {} .checkout-place-order__button {} /* PurchaseOrder */ .checkout-purchase-order {} .checkout-purchase-order__form {} .dropin-field {} /* ServerError */ .checkout-server-error {} .checkout-server-error__icon {} .error-icon {} /* ShippingMethods */ .checkout-shipping-methods__content {} .checkout-shipping-methods__error {} .checkout-shipping-methods__method {} .checkout-shipping-methods__options--busy {} .checkout-shipping-methods__options--toggleButton {} .checkout-shipping-methods__spinner {} .checkout-shipping-methods__title {} .dropin-price {} .dropin-radio-button__label {} .dropin-toggle-button__content {} /* TermsAndConditions */ .checkout-terms-and-conditions {} .checkout-terms-and-conditions__error {} /* MergedCartBanner */ .checkout__banner {} ``` --- # Add a payment method The Checkout drop-in component provides extensibility features for integrating third-party payment providers. Use slots to customize the list of payment methods shown during the checkout process. > **Supported payment providers** The Checkout drop-in supports Adyen payment methods (including Bancontact) and payment extensions in addition to the Braintree example below. See the [release notes](https://experienceleague.adobe.com/developer/commerce/storefront/releases/) for the latest supported providers. ## Step-by-step This tutorial walks you through integrating Braintree as a payment provider with the Commerce boilerplate template. While we use Braintree as an example, you can adapt these same steps for other payment providers. ### 1. Prerequisites For this tutorial, you must configure the Braintree extension on your Adobe Commerce backend before integrating it with the Commerce boilerplate template. The Braintree extension is bundled with Adobe Commerce and can be https://experienceleague.adobe.com/en/docs/commerce-admin/stores-sales/payments/braintree in the Admin. If you choose to integrate with a different payment provider, consider the following: - The provider must be supported by Adobe Commerce. - The provider likely offers an extension that you must install and configure on your Adobe Commerce backend. ### 2. Add the Braintree client SDK To integrate the Braintree payment provider with the Commerce boilerplate template, you must add the Braintree client SDK to your project. ### HTML element Use the following `script` tag to add the Braintree client SDK to an HTML file. ```html ``` ### Import declaration Use the following `import` declaration to add the Braintree client SDK directly to the `commerce-checkout.js` block file. ```js import 'https://js.braintreegateway.com/web/dropin/1.43.0/js/dropin.min.js'; ``` ### 3. Define a custom handler 1. Create a `braintreeInstance` variable to manage the Braintree drop-in instance. ```js let braintreeInstance; ``` 1. Update the [`PaymentMethods`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/containers/payment-methods/) container to include a custom handler for the Braintree payment method. Set `autoSync` to `false` to prevent automatic calls to the [`setPaymentMethod`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/functions/#setpaymentmethod) function when the payment method changes. ```js CheckoutProvider.render(PaymentMethods, { slots: { Methods: { braintree: { autoSync: false, render: async (ctx) => { const container = document.createElement('div'); window.braintree.dropin.create({ authorization: 'sandbox_cstz6tw9_sbj9bzvx2ngq77n4', container, }, (err, dropinInstance) => { if (err) { console.error(err); } braintreeInstance = dropinInstance; }); ctx.replaceHTML(container); }, }, }, }, })($paymentMethods), ``` ### 4. Handle the payment method Implement the Braintree payment logic within the `handlePlaceOrder` handler of the [`PlaceOrder`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/containers/place-order/) container. This involves processing the payment using the Braintree https://developer.paypal.com/braintree/docs/guides/payment-method-nonces. ```js CheckoutProvider.render(PlaceOrder, { handlePlaceOrder: async ({ cartId, code }) => { await displayOverlaySpinner(); try { switch (code) { case 'braintree': { braintreeInstance.requestPaymentMethod(async (err, payload) => { if (err) { removeOverlaySpinner(); console.error(err); return; } await checkoutApi.setPaymentMethod({ code: 'braintree', braintree: { is_active_payment_token_enabler: false, payment_method_nonce: payload.nonce, }, }); await orderApi.placeOrder(cartId); }); break; } default: { // Place order await orderApi.placeOrder(cartId); } } } catch (error) { console.error(error); throw error; } finally { await removeOverlaySpinner(); } }, })($placeOrder), ``` ## Example See https://github.com/hlxsites/aem-boilerplate-commerce/tree/demos/blocks/commerce-checkout-braintree in the `demos` branch of the boilerplate repository for complete JS and CSS code for the Braintree payment method checkout flow. --- # Integrate with a third-party address verification API You might want to enhance the shopper experience by streamlining the process of populating and verifying the shipping address, thereby reducing the risk of user error. You can achieve this by implementing a third-party address lookup and autocomplete APIs, such as those provided by https://mapsplatform.google.com/maps-products/#places-section. This tutorial describes how to override any field in a checkout address form and extend it to integrate with this service. The implementation supports backend-configurable validation and full form submission integration. Upon successful completion of this tutorial, a form similar to the following will be displayed: ![Autocomplete shipping address](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/checkout/address-lookup.png) *Autocomplete shipping address* ## Step-by-step The following steps describe how to integrate the Google Address Validation API with the Commerce boilerplate template using the provided address autocomplete implementation. ### 1. Prerequisites For this tutorial, you must have a valid Google API key. https://developers.google.com/maps/documentation/javascript/get-api-key describes the process to obtain and set up this key. ### 2. Download and configure the address autocomplete implementation 1. **Download the implementation:** Copy the `address-autocomplete.js` file from `/public/samples/address-autocomplete.js` in this documentation repository to your project directory. 2. **Replace the API key placeholder:** Open the `address-autocomplete.js` file and replace `ADD-YOUR-GOOGLE-API-KEY-HERE` with your actual Google API key: ```javascript const CONFIG = { googleApiKey: 'YOUR_ACTUAL_GOOGLE_API_KEY', // ... rest of configuration }; ``` ### 3. Import and initialize the autocomplete service In your `commerce-checkout.js` file, make the following changes to enable address autocomplete: 1. **Import the autocomplete service:** ```javascript import { initializeAutocompleteWhenReady } from './address-autocomplete.js'; ``` 2. **Initialize the autocomplete in the `initializeCheckout` function:** ```javascript const initializeCheckout = async () => { // ... existing checkout initialization code ... // Initialize address autocomplete for shipping form const shippingContainer = document.querySelector('[data-commerce-checkout-shipping]'); if (shippingContainer) { initializeAutocompleteWhenReady(shippingContainer, 'input[name="street"]'); } // ... rest of initialization code ... }; ``` The `initializeAutocompleteWhenReady` function automatically: - Waits for the address form to be rendered - Attaches autocomplete functionality to the street input field - Handles form field population when an address is selected - Manages Google Maps API loading and initialization ## Example The complete address autocomplete implementation is available in `/public/samples/address-autocomplete.js`. This implementation includes: - **AddressAutocompleteService class**: Handles Google Places API integration - **initializeAutocompleteWhenReady function**: Utility function for easy integration - **Automatic form field population**: Populates street, city, country, and postal code fields - **Keyboard navigation**: Arrow keys, Enter, and Escape support - **Error handling**: Graceful fallback when Google Maps API is unavailable For additional customization options and advanced usage, see the implementation comments in the sample file. --- # Buy online, pickup in store Buy online, pickup in store (BOPIS) is a popular fulfillment option that allows customers to purchase items online and pick them up in-store. The Commerce boilerplate template does not include a BOPIS checkout flow by default, but you can easily implement one using Adobe's drop-in components. ## Step-by-step The following steps describe how to modify the https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/commerce-checkout/commerce-checkout.js block file in the boilerplate template to allow users to choose between delivery and in-store pickup during the checkout process. ### 1. Prerequisites Before you start, you must configure https://experienceleague.adobe.com/en/docs/commerce-admin/stores-sales/delivery/basic-methods/shipping-in-store-delivery options in the Adobe Commerce Admin to define pickup locations. The [`fetchPickupLocations`](#fetch-pickup-locations) function retrieves the list of available pickup locations using a GraphQL query. ### 2. Update content fragment 1. To create a new section for the delivery options, additional DOM elements are required. You can add these elements by modifying the content fragment. ```html

Delivery Method

``` 1. You must also add new selectors to render the required components and content. ```javascript const $deliveryButton = checkoutFragment.querySelector('.checkout-delivery-method__delivery-button'); const $inStorePickupButton = checkoutFragment.querySelector('. checkout-delivery-method__in-store-pickup-button'); const $inStorePickup = checkoutFragment.querySelector('.checkout__in-store-pickup'); ``` ![Update content fragment](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/checkout/bopis-content-fragment.png) ### 3. Add toggle buttons During initialization, the code renders two buttons: - Delivery - In-store pickup These buttons allow users to toggle between the two options. ```js UI.render(ToggleButton, { label: 'Delivery', onChange: () => onToggle('delivery'), })($deliveryButton), UI.render(ToggleButton, { label: 'In-store Pickup', onChange: () => onToggle('in-store-pickup'), })($inStorePickupButton), ``` ![Toggle buttons](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/checkout/bopis-toggle-buttons.png) ### 4. Toggle between options The `onToggle` function manages switching between the delivery and in-store pickup options. It updates the selected state of the buttons and toggles the visibility of the corresponding forms. ```js async function onToggle(type) { if (type === 'delivery') { deliveryButton.setProps((prev) => ({ ...prev, selected: true })); inStorePickupButton.setProps((prev) => ({ ...prev, selected: false })); $shippingForm.removeAttribute('hidden'); $delivery.removeAttribute('hidden'); $inStorePickup.setAttribute('hidden', ''); } else { inStorePickupButton.setProps((prev) => ({ ...prev, selected: true })); deliveryButton.setProps((prev) => ({ ...prev, selected: false })); $shippingForm.setAttribute('hidden', ''); $delivery.setAttribute('hidden', ''); $inStorePickup.removeAttribute('hidden'); } } ``` ### 5. Fetch pickup locations The `fetchPickupLocations` function retrieves the list of available pickup locations using a GraphQL query. Users can choose a location where they'd like to pick up their order. ```js async function fetchPickupLocations() { return checkoutApi .fetchGraphQl( `query pickupLocations { pickupLocations { items { name pickup_location_code } total_count } }`, { method: 'GET', cache: 'no-cache' } ) .then((res) => res.data.pickupLocations.items); } ``` ### 6. Render location options After the code fetches the pickup locations, it renders options as radio buttons. The user can select a location, which updates the shipping address with the corresponding pickup location code. ```js const pickupLocations = await fetchPickupLocations(); pickupLocations.forEach((location) => { const { name, pickup_location_code } = location; const locationRadiobutton = document.createElement('div'); UI.render(RadioButton, { label: name, name: 'pickup-location', value: name, onChange: () => { checkoutApi.setShippingAddress({ address: {}, pickupLocationCode: pickup_location_code, }); }, })(locationRadiobutton); $inStorePickup.appendChild(locationRadiobutton); }); ``` ![Pick up location options](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/checkout/bopis-render-pickup-locations.png) ### 7. Finalize the flow After a user selects **In-store pickup** and chooses a location, the pickup form is shown, while the shipping form is hidden. This provides a clear and seamless way for users to choose how they want to receive their order. ## Example See https://github.com/hlxsites/aem-boilerplate-commerce/tree/demos/blocks/commerce-checkout-bopis in the `demos` branch of the boilerplate repository for complete JS and CSS code for the BOPIS checkout flow. --- # Implement multi-step checkout This tutorial provides a customizable example to implement a comprehensive multi-step checkout in your Adobe Commerce storefront that supports **all user scenarios**: guest users, logged-in customers, and virtual products. ## Overview This implementation provides a **complete multi-step checkout** for the Adobe Commerce boilerplate that handles: - **Guest users** - Email capture and address entry - **Logged-in customers** - Saved address selection and account integration - **Virtual products** - Automatic shipping step bypass - **Mixed carts** - Physical + virtual product combinations - **Modular architecture** - Event-driven step coordination ## Implementation Features | Feature | Status | |---------|--------| | Guest users | ✅ | | Logged-in customers | ✅ | | Virtual products | ✅ | | Mixed carts (physical + virtual products) | ✅ | | Custom payment/shipping methods | 🔧 | ## Multi-step Customization Key areas specific to multi-step checkout customization: - **Step progression logic** - Modify `steps.js` for custom user flows and step transitions - **Individual step modules** - Customize step behavior in `steps/` folder - **Step validation** - Control when users can advance between steps - **Fragment management** - Adapt step-specific HTML fragments in `fragments.js` - **Step visibility** - Customize CSS classes for active/inactive step states - **Manual synchronization** - Control when data is saved to the cart ## Architecture ### File Structure The multi-step checkout implementation follows a modular architecture: | File | Purpose | Key Features | |------|---------|--------------| | `commerce-checkout-multi-step.js` | Entry point and block decorator | Initializes the checkout system | | `commerce-checkout-multi-step.css` | Step styling and visibility controls | Step progression, visual states, responsive design | | `steps.js` | Main implementation | Step coordination and state management | | `steps/shipping.js` | Shipping/contact step logic | Login detection, address forms, email validation | | `steps/shipping-methods.js` | Delivery method selection | Shipping options, cost calculation | | `steps/payment-methods.js` | Payment method selection | Payment provider integration | | `steps/billing-address.js` | Billing address step | Conditional billing form rendering | | `fragments.js` | HTML fragment creation | Step-specific DOM structure generation | | `containers.js` | Container rendering functions | Drop-in container management | | `components.js` | UI component functions | Reusable UI elements | | `utils.js` | Utility functions and helpers | Virtual cart detection, validation | | `constants.js` | Shared constants and configuration | CSS classes, form names, storage keys | ### Manual Synchronization Control In multi-step checkout, containers use **`autoSync: false`** to disable automatic backend synchronization, allowing manual control over when data is saved: ```javascript // Containers with manual sync control const containers = [ 'LoginForm', // Manual email/authentication handling 'ShippingMethods', // Manual shipping method selection 'PaymentMethods', // Manual payment method selection 'BillToShippingAddress' // Manual billing address control ]; // Example: ShippingMethods with manual sync CheckoutProvider.render(ShippingMethods, { UIComponentType: 'ToggleButton', autoSync: false, // Disable automatic cart updates })(container); ``` **AutoSync behavior:** - **`autoSync: true` (default)** - Local changes automatically sync with backend via GraphQL mutations - **`autoSync: false`** - Changes maintained locally only, no automatic API calls **Why disable autoSync in multi-step:** - **Controlled timing** - Save data only when step is completed and validated - **Better UX** - Prevent partial/invalid data from being sent to cart - **Step coordination** - Parent step manager controls when to persist data - **Validation first** - Ensure all step requirements met before saving **Manual sync example:** ```javascript // Step completion with manual sync (triggered by continue button) const continueFromStep = async () => { if (!validateStepData()) return; // Manual API call with error handling try { await checkoutApi.setShippingMethodsOnCart([{ carrier_code: selectedMethod.carrier.code, method_code: selectedMethod.code, }]); } catch (error) { console.error('Failed to save step data:', error); return; // Don't proceed if API call fails } // Only continue if API call succeeded await displayStepSummary(selectedMethod); await continueToNextStep(); events.emit('checkout/step/completed', null); }; ``` **Key patterns:** - **Continue button trigger** - API calls happen when user clicks continue, not on selection - **Try-catch wrapping** - All API calls must be wrapped for error handling - **Early return on error** - If API fails, don't proceed to next step - **Success-only progression** - Only move forward if data successfully saved This approach ensures data integrity and provides smooth step transitions without premature backend updates. ### API Reference Step modules rely on the checkout drop-in's API functions for cart management. The complete API reference is available in the [Checkout functions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/functions/) documentation. **Key APIs for multi-step implementation:** | Function | Purpose | Used In Step | |----------|---------|--------------| | `setGuestEmailOnCart()` | Set guest user email | Shipping (email capture) | | `setShippingAddress()` | Set shipping address on cart | Shipping (address collection) | | `setShippingMethodsOnCart()` | Set shipping methods on cart | Shipping Methods | | `setPaymentMethod()` | Set payment method on cart | Payment Methods | | `setBillingAddress()` | Set billing address on cart | Payment Methods, Billing Address | | `isEmailAvailable()` | Check email availability | Order Header (account creation) | | `getStoreConfigCache()` | Get cached store configuration | Address forms (default country) | | `estimateShippingMethods()` | Estimate shipping costs | Address forms (cost calculation) | All step completion logic should use these APIs with proper error handling as shown in the manual sync examples above. **Note:** The implementation uses event-driven data (`events.lastPayload()`) instead of direct `getCart()` or `getCustomer()` calls for performance optimization and real-time state management. ### Component Registry Pattern The `components.js` file implements a registry system specifically for **SDK components and external UI library components**: ```javascript // components.js - Component registry (separate from containers) const registry = new Map(); // Component IDs for UI elements export const COMPONENT_IDS = { CHECKOUT_HEADER: 'checkoutHeader', SHIPPING_STEP_CONTINUE_BTN: 'shippingStepContinueBtn', PAYMENT_STEP_TITLE: 'paymentStepTitle', // ... more component IDs }; // Core component methods export const hasComponent = (id) => registry.has(id); export const removeComponent = (id) => { const component = registry.get(id); if (component) { component.remove(); registry.delete(id); } }; // Render SDK components export const renderCheckoutHeader = (container) => renderComponent( COMPONENT_IDS.CHECKOUT_HEADER, async () => UI.render(Header, { className: 'checkout-header', level: 1, size: 'large', title: 'Checkout', })(container) ); export const renderStepContinueBtn = async (container, stepId, onClick) => renderPrimaryButton(container, stepId, { children: 'Continue', onClick }); ``` **Key distinction from containers:** - **`containers.js`** - Manages **drop-in containers** (LoginForm, AddressForm, ShippingMethods, etc.) - **`components.js`** - Manages **SDK/UI library components** (Button, Header, ProgressSpinner, etc.) **Usage guidelines:** - **Use `components.js` for:** Headers, buttons, spinners, modals, and other UI elements from the SDK - **Use `containers.js` for:** Checkout drop-ins, account drop-ins, cart drop-ins, and other business logic containers - **Recommended approach:** Keep drop-in containers and UI components in separate registries for better organization This ensures clean architecture where `components.js` handles pure UI elements while `containers.js` manages complex business logic containers. ### Container Management The `containers.js` file provides a complete system for managing **drop-in containers** (LoginForm, AddressForm, ShippingMethods, etc.) with registry-based lifecycle management. **Registry System:** ```javascript // containers.js - Registry system for drop-in containers const registry = new Map(); // Core registry methods export const hasContainer = (id) => registry.has(id); export const getContainer = (id) => registry.get(id); export const unmountContainer = (id) => { if (!registry.has(id)) return; const containerApi = registry.get(id); containerApi.remove(); registry.delete(id); }; // Helper to render or get existing container const renderContainer = async (id, renderFn) => { if (registry.has(id)) { return registry.get(id); // Return existing } const container = await renderFn(); // Render new registry.set(id, container); return container; }; ``` **Container IDs and render functions:** Each container is identified by a unique string ID and has a corresponding render function that handles the registry logic: ```javascript // Predefined container identifiers export const CONTAINERS = Object.freeze({ LOGIN_FORM: 'loginForm', SHIPPING_ADDRESS_FORM: 'shippingAddressForm', SHIPPING_METHODS: 'shippingMethods', PAYMENT_METHODS: 'paymentMethods', // ... more containers }); // Usage in container functions export const renderLoginForm = async (container) => renderContainer( CONTAINERS.LOGIN_FORM, async () => CheckoutProvider.render(LoginForm, { /* config */ })(container) ); ``` **Key benefits of the container system:** - **Centralized logic** - Complex container configuration in one place - **Prevents duplicates** - Registry ensures same container isn't rendered multiple times - **Memory management** - Automatic cleanup prevents memory leaks - **State preservation** - Containers maintain state across step transitions **Registry lifecycle:** 1. **Check existing** - `hasContainer()` / `getContainer()` to find existing instances 2. **Render once** - `renderContainer()` creates new containers only if needed 3. **Cleanup** - `unmountContainer()` removes containers and clears references This comprehensive container management approach ensures efficient resource usage and prevents common issues like duplicate event listeners or memory leaks. ### Step Modules The `steps/` folder contains individual step modules that handle specific checkout phases. Each step module implements a consistent interface and manages its own domain logic, UI rendering, and data validation. **Step module structure:** Each step file in the `steps/` folder follows the same architectural pattern: ```javascript // steps/shipping.js - Example step module export const createShippingStep = ({ getElement, api, events, ui }) => { return { async display(data) { // Render step UI using containers (LoginForm, AddressForm) // Handle different user types (guest vs logged-in) // Use manual sync patterns for form data }, async displaySummary(data) { // Show completed step summary using fragment functions // Create edit functionality for step modifications }, async continue() { // Validate step data and make API calls // Handle step progression logic // Emit completion events }, isComplete(data) { // Validate step completion based on cart data // Handle virtual product logic }, isActive() { // Check if step is currently active } }; }; ``` **Available step modules:** - **`shipping.js`** - Handles email capture (LoginForm) and shipping address collection (AddressForm) - **`shipping-methods.js`** - Manages delivery method selection and shipping cost calculation - **`payment-methods.js`** - Handles payment provider integration and method selection - **`billing-address.js`** - Manages conditional billing address form rendering **Step module responsibilities:** - **UI rendering** - Uses container functions to render drop-ins - **Data validation** - Validates step completion - **API integration** - Makes manual API calls with error handling - **Event handling** - Responds to checkout events - **Summary creation** - Generates read-only summaries with edit functionality ### Fragment Management The `fragments.js` file is responsible for creating all DOM structure in the multi-step checkout. It provides a centralized system for generating HTML fragments, managing selectors, and creating reusable summary components. **Core responsibilities:** - **DOM Structure Creation** - Generates HTML fragments for each step and the main checkout layout - **Selector Management** - Centralizes all CSS selectors in a frozen object for consistency - **Summary Components** - Provides reusable functions for creating step summaries with edit functionality - **Utility Functions** - Helper functions for fragment creation and DOM querying **Fragment Creation Pattern:** ```javascript // Step-specific fragment creation function createShippingStepFragment() { return createFragment(` `); } // Main checkout structure export function createCheckoutFragment() { const checkoutFragment = createFragment(` `); // Append step fragments to main structure return checkoutFragment; } ``` **Centralized Selector System:** ```javascript // All selectors defined in one place export const selectors = Object.freeze({ checkout: { loginForm: '.checkout__login', shippingAddressForm: '.checkout__shipping-form', shippingStepContinueBtn: '.checkout__continue-to-shipping-methods', // ... more selectors } }); ``` **Summary Creation Functions:** ```javascript // Reusable summary components with edit functionality export const createLoginFormSummary = (email, onEditClick) => { const content = document.createElement('div'); content.textContent = email; return createSummary(content, onEditClick); }; export const createAddressSummary = (data, onEditClick) => { // Format address data into summary display return createSummary(formattedContent, onEditClick); }; ``` **Key benefits of fragment management:** - **Consistent DOM structure** - All HTML is generated through standardized functions - **CSS class coordination** - Selectors and fragments use the same class names - **Reusable components** - Summary functions can be used across different steps - **Maintainable markup** - All HTML structure defined in one centralized location ### Element Access Pattern Step modules access DOM elements using the centralized selector system from `fragments.js`. Here's how step modules import and use those selectors: ```javascript // steps/shipping.js - Element access in step modules const { checkout } = selectors; const elements = { $loginForm: getElement(checkout.loginForm), $loginFormSummary: getElement(checkout.loginFormSummary), $shippingAddressForm: getElement(checkout.shippingAddressForm), $shippingAddressFormSummary: getElement(checkout.shippingAddressFormSummary), $shippingStep: getElement(checkout.shippingStep), $shippingStepContinueBtn: getElement(checkout.shippingStepContinueBtn), }; ``` **Key benefits of this pattern:** - **Centralized selectors** - All CSS classes defined in one location - **Type safety** - Object structure prevents typos and missing selectors - **Maintainability** - Easy to update selectors across the entire system - **Consistency** - All step modules follow the same element access pattern - **Fragment coordination** - Selectors match the structure created by fragments This ensures that fragments create the DOM structure and steps access it through a consistent, maintainable selector system. ### Summary and Edit Pattern When users complete a step by clicking the continue button and validation succeeds, the step transitions to **summary mode**: ```javascript // Step completion flow async function continueFromStep() { // 1. Validate step data if (!validateStep()) return; // 2. Save data to cart await api.setStepData(formData); // 3. Hide step content, show summary await displayStepSummary(data); // 4. Move to next step await displayNextStep(); } ``` **Summary features:** - **Read-only display** - Shows completed step information in condensed format - **Edit functionality** - "Edit" link allows users to return and modify data - **Visual state** - Different styling indicates step completion - **Persistent data** - Summary reflects the actual saved cart data **Edit flow:** ```javascript // Edit button functionality const handleEdit = async () => { await displayStep(true); // Reactivate step // Previous data automatically pre-fills forms }; ``` This pattern ensures users can review their choices and make changes at any point without losing progress. ### Place Order Button Enablement The **Place Order** button is disabled by default and only becomes enabled when all required steps are completed: ```javascript // Place order button management async function updatePlaceOrderButton(data) { const allStepsComplete = steps.shipping.isComplete(data) && (!isVirtualCart(data) ? steps.shippingMethods.isComplete(data) : true) && steps.paymentMethods.isComplete(data) && steps.billingAddress.isComplete(data); if (allStepsComplete) { placeOrderButton.setProps({ disabled: false }); } else { placeOrderButton.setProps({ disabled: true }); } } ``` **Progressive enablement features:** - **Disabled by default** - Prevents incomplete order submissions - **Step validation** - Checks each step's completion status - **Virtual product logic** - Skips shipping validation for virtual carts - **Real-time updates** - Button state updates as users complete steps - **Visual feedback** - Users can see their progress toward completion ## Implementation Guide The following sections demonstrate how to build a **production-ready multi-step checkout** using Adobe's drop-in components. This implementation replaces the regular one-step checkout in the boilerplate template with a sophisticated, modular system. ### 1. Create the entry point and main structure Create the main block file `commerce-checkout.js` and set up the modular architecture: ```javascript // Initializers // Block-level utils // Fragments export default async function decorate(block) { setMetaTags('Checkout'); document.title = 'Checkout'; block.replaceChildren(createCheckoutFragment()); const stepsManager = createStepsManager(block); await stepsManager.init(); } ``` Create `fragments.js` for the main HTML structure: ```javascript export function createCheckoutFragment() { return document.createRange().createContextualFragment(` `); } ``` This modular approach separates concerns: the entry point coordinates everything, fragments handle HTML creation, and the steps manager handles step logic. ### 2. Step fragments and HTML structure Create the step-specific fragments in `fragments.js`. Each checkout step gets its own fragment with specific containers and CSS classes: ```javascript /** * Creates the shipping address fragment for the checkout. * Includes login form and address form containers. */ function createShippingStepFragment() { return document.createRange().createContextualFragment(` `); } /** * Creates the shipping methods fragment for the checkout. */ function createShippingMethodsStepFragment() { return document.createRange().createContextualFragment(` `); } ``` **Key fragment concepts:** - **CHECKOUT_STEP_CONTENT** - Shows containers when step is active (editable mode) - **CHECKOUT_STEP_SUMMARY** - Shows completed step information (read-only mode) - **CHECKOUT_STEP_BUTTON** - Continue buttons for step progression - **Multiple containers per step** - Each fragment can contain multiple containers with their own summary versions - **CSS-driven visibility** - No DOM manipulation, just class-based show/hide The shipping step includes **both login and address containers** because guests need both email capture (via LoginForm) and shipping address entry (via AddressForm). ### 3. Create step modules Create individual step modules that implement the universal step interface. Each step module follows the pattern described in the [Step Modules architecture section](#step-modules). **Required step files:** - **`steps/shipping.js`** - Email capture (LoginForm) and shipping address collection (AddressForm) - **`steps/shipping-methods.js`** - Delivery method selection and cost calculation - **`steps/payment-methods.js`** - Payment provider integration and method selection - **`steps/billing-address.js`** - Conditional billing address form rendering **Implementation reference:** For complete implementations of these step modules, see the sample files in the https://github.com/hlxsites/aem-boilerplate-commerce/tree/demos/blocks/commerce-checkout-multi-step/steps. Each file demonstrates the full step interface implementation with proper error handling, user flow logic, and integration with containers and APIs. ### 4. Implement the steps manager Create `steps.js` to coordinate all step logic and manage the checkout flow. Build this step by step: 1. **Set up the basic structure** with imports and function signature: ```javascript import { createShippingStep } from './steps/shipping.js'; import { createShippingMethodsStep } from './steps/shipping-methods.js'; import { createPaymentMethodsStep } from './steps/payment-methods.js'; import { createBillingAddressStep } from './steps/billing-address.js'; export default function createStepsManager(block) { // Implementation will go here } ``` 2. **Create step instances** by gathering dependencies and instantiating each step module: ```javascript export default function createStepsManager(block) { const elements = getElements(block); const dependencies = { elements, api, events, ui }; const steps = { shipping: createShippingStep(dependencies), shippingMethods: createShippingMethodsStep(dependencies), paymentMethods: createPaymentMethodsStep(dependencies), billingAddress: createBillingAddressStep(dependencies) }; } ``` 3. **Implement step coordination logic** that determines which step to show based on completion status: ```javascript async function handleCheckoutUpdated(data) { // Step 1: Shipping - always required if (!steps.shipping.isComplete(data)) { await steps.shipping.display(data); return; } await steps.shipping.displaySummary(data); // Step 2: Shipping Methods (skip for virtual products) if (!isVirtualCart(data)) { if (!steps.shippingMethods.isComplete(data)) { await steps.shippingMethods.display(data); return; } await steps.shippingMethods.displaySummary(data); } // Step 3: Payment Methods if (!steps.paymentMethods.isComplete(data)) { await steps.paymentMethods.display(data); return; } await steps.paymentMethods.displaySummary(data); // Step 4: Billing Address (if needed) if (!steps.billingAddress.isComplete(data)) { await steps.billingAddress.display(data); return; } await steps.billingAddress.displaySummary(data); } ``` 4. **Wire up event handling** to respond to checkout state changes: ```javascript return { async init() { events.on('checkout/initialized', handleCheckoutUpdated); events.on('checkout/updated', handleCheckoutUpdated); } }; ``` The steps manager uses the **early return pattern** - if a step is incomplete, it displays that step and exits. Only when all previous steps are complete does it move to the next step. This ensures proper linear progression through the checkout flow. ### 5. Add CSS styling for step controls Create the CSS that controls step visibility and progression. Add this to your `commerce-checkout-multi-step.css` file: 1. **Step visibility controls** - Define the core classes that show/hide step content: ```css /* Hide all step content by default */ .checkout-step-content { display: none; } /* Show content when step is active */ .checkout-step-active .checkout-step-content { display: block; } /* Hide summaries by default */ .checkout-step-summary { display: none; } /* Show summaries when step is completed (not active) */ .checkout-step:not(.checkout-step-active) .checkout-step-summary { display: block; } /* Hide continue buttons when step is completed */ .checkout-step:not(.checkout-step-active) .checkout-step-button { display: none; } ``` 2. **Step progression styling** - For complete visual styling (borders, colors, animations, etc.), see https://github.com/hlxsites/aem-boilerplate-commerce/tree/demos/blocks/commerce-checkout-multi-step/commerce-checkout-multi-step.css in the demo repository. These CSS rules create the core multi-step behavior: **content shows when active**, **summaries show when completed**, and **step progression controls** guide users through the checkout flow. ## Example See https://github.com/hlxsites/aem-boilerplate-commerce/tree/demos/blocks/commerce-checkout-multi-step in the `demos` branch of the boilerplate repository for complete JS and CSS code for the multi-step checkout flow. --- # Validate shipping address Use the `AddressValidation` container to present both the original and suggested addresses from your verification service, letting shoppers choose before placing their order. This tutorial shows how to integrate the container in the `commerce-checkout` block. ![Autocomplete shipping address](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/checkout/address-validation.png) *AddressValidation displayed in a modal* ## Overview At a high level: - Call your address verification service before placing the order. - If it returns a suggestion, open a modal and render `AddressValidation`. - If the shopper selects the suggestion, persist it as the shipping address; otherwise, use the original address. ## Integration ```javascript // in commerce-checkout.js block // Handler passed to the PlaceOrder container const handlePlaceOrder = async ({ cartId, code }) => { await displayOverlaySpinner(loaderRef, $loader); try { // Payment Services credit card if (code === PaymentMethodCode.CREDIT_CARD) { if (!creditCardFormRef.current) { console.error('Credit card form not rendered.'); return; } if (!creditCardFormRef.current.validate()) { // Credit card form invalid; abort order placement return; } // Submit Payment Services credit card form await creditCardFormRef.current.submit(); } // Address validation const suggestion = await validateAddress(); if (suggestion) { const container = document.createElement('div'); await showModal(container); await renderAddressValidation(container, { suggestedAddress: suggestion, handleSelectedAddress: async ({ selection, address }) => { if (selection === 'suggested') { // Update the shipping form using the suggested address sessionStorage.removeItem(SHIPPING_ADDRESS_DATA_KEY); shippingForm.setProps((prevProps) => ({ ...prevProps, inputsDefaultValueSet: address, })); } else { // Place order await orderApi.placeOrder(cartId); } removeModal(); }, }); } else { // Place order await orderApi.placeOrder(cartId); } } catch (error) { console.error(error); throw error; } finally { removeOverlaySpinner(loaderRef, $loader); } }; ``` ```javascript // in containers.js /** * Renders the AddressValidation container in its own host element * @param {HTMLElement} container - DOM element to render into */ export const renderAddressValidation = async ( container, { suggestedAddress, handleSelectedAddress } ) => CheckoutProvider.render(AddressValidation, { suggestedAddress, handleSelectedAddress, })(container); ``` ```javascript // in utils.js (example stub) export const validateAddress = async () => { // Here’s where your API call goes return { city: 'Bainbridge Island', countryCode: 'US', postcode: '98110-2450', region: 'CA', street: ['123 Winslow Way E'], }; }; ``` Finally, add some padding for better appearance: ```css /* commerce-checkout.css */ .modal-content .checkout-address-validation { padding: var(--spacing-big); } ``` ## Next steps - See the [`AddressValidation` container](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/containers/address-validation/) for props and behaviors. - Ensure your suggestion matches the `CartAddressInput` format. --- # Checkout utility functions This topic provides details and instructions for using the utility functions available in the checkout drop-in component. These functions were moved from the integration layer and are now publicly accessible within the checkout block from `@dropins/storefront-checkout/lib/utils.js`. ### Quick imports ```ts ``` ## API Functions ### setAddressOnCart The `setAddressOnCart` function creates a debounced handler for setting shipping or billing addresses on the cart, preventing excessive API calls when address data changes frequently. ```ts export function setAddressOnCart({ type = 'shipping', debounceMs = 0, placeOrderBtn, }: { type?: 'shipping' | 'billing'; debounceMs?: number; placeOrderBtn?: RenderAPI; }): (change: AddressFormChange) => void; ``` ```text [ ['Parameter', 'Type', 'Req?', 'Description'], ['type', 'string', 'No', 'Address type: "shipping" or "billing". Defaults to "shipping".'], ['debounceMs', 'number', 'No', 'Milliseconds to debounce API calls. Defaults to 0.'], ['placeOrderBtn', 'RenderAPI', 'No', 'Place order button API to manage disabled state.'], ] ``` ### Returns Returns a function that accepts address form changes and updates the cart accordingly. ### Usage ```ts // Set up debounced shipping address handler const handleShippingChange = setAddressOnCart({ type: 'shipping', debounceMs: 500, placeOrderBtn: placeOrderButtonAPI }); // Use with form change events shippingForm.addEventListener('input', (event) => { const formData = getFormValues(event.target.form); const isValid = validateForm(event.target.form); handleShippingChange({ data: formData, isDataValid: isValid }); }); ``` ### estimateShippingCost The `estimateShippingCost` function creates a debounced handler for estimating shipping costs based on address information. ```ts export function estimateShippingCost({ debounceMs = 0 }): (change: AddressFormChange) => void; ``` ```text [ ['Parameter', 'Type', 'Req?', 'Description'], ['debounceMs', 'number', 'No', 'Milliseconds to debounce API calls. Defaults to 0.'], ] ``` ### Returns Returns a function that estimates shipping costs when address data changes. ### Usage ```ts // Set up shipping cost estimation const handleEstimateShipping = estimateShippingCost({ debounceMs: 300 }); // Use with address form changes addressForm.addEventListener('input', (event) => { const formData = getFormValues(event.target.form); const isValid = validateForm(event.target.form); handleEstimateShipping({ data: formData, isDataValid: isValid }); }); ``` ## Cart Data Functions ### isVirtualCart The `isVirtualCart` function checks if a cart contains only virtual products (no shipping required). If no argument is provided, it reads the latest checkout data. ```ts export function isVirtualCart(data?: Cart | null): boolean; ``` ```text [ ['Parameter', 'Type', 'Req?', 'Description'], ['data', 'Cart | null', 'Yes', 'The cart data object to check.'], ] ``` ### Returns Returns `true` if the cart is virtual, `false` otherwise. ### Usage ```ts // Check if shipping is required using explicit data const cartData = await getCart(); const skipShipping = isVirtualCart(cartData); // Or check using the latest checkout data const skipShippingFromState = isVirtualCart(); if (skipShipping) { // Hide shipping-related UI document.querySelector('.shipping-section').style.display = 'none'; } ``` ### isEmptyCart The `isEmptyCart` function checks if a cart is empty or null. ```ts export function isEmptyCart(data: Cart | null): boolean; ``` ```text [ ['Parameter', 'Type', 'Req?', 'Description'], ['data', 'Cart | null', 'Yes', 'The cart data object to check.'], ] ``` ### Returns Returns `true` if the cart is empty or null, `false` otherwise. ### Usage ```ts const cartData = await getCart(); if (isEmptyCart(cartData)) { // Show empty cart message showEmptyCartMessage(); return; } // Proceed with checkout proceedToCheckout(cartData); ``` ### getCartShippingMethod The `getCartShippingMethod` function retrieves the selected shipping method from cart data. ```ts export function getCartShippingMethod(data: Cart | null): ShippingMethod | null; ``` ```text [ ['Parameter', 'Type', 'Req?', 'Description'], ['data', 'Cart | null', 'Yes', 'The cart data object.'], ] ``` ### Returns Returns the selected shipping method object or `null` if none is selected. ### Usage ```ts const cartData = await getCart(); const shippingMethod = getCartShippingMethod(cartData); if (shippingMethod) { console.log(`Shipping: ${shippingMethod.title} - $${shippingMethod.amount.value}`); } ``` ### getCartAddress The `getCartAddress` function retrieves shipping or billing address from cart data. ```ts export function getCartAddress( data: Cart | null, type: 'shipping' | 'billing' = 'shipping' ): Record | null; ``` ```text [ ['Parameter', 'Type', 'Req?', 'Description'], ['data', 'Cart | null', 'Yes', 'The cart data object.'], ['type', 'string', 'No', 'Address type: "shipping" or "billing". Defaults to "shipping".'], ] ``` ### Returns Returns the address object or `null` if no address is set. ### Usage ```ts const cartData = await getCart(); const shippingAddress = getCartAddress(cartData, 'shipping'); const billingAddress = getCartAddress(cartData, 'billing'); if (shippingAddress) { populateAddressForm(shippingAddress); } ``` ### getCartPaymentMethod The `getCartPaymentMethod` function retrieves the selected payment method from cart data. ```ts export function getCartPaymentMethod(data: Cart | null): PaymentMethod | null; ``` ```text [ ['Parameter', 'Type', 'Req?', 'Description'], ['data', 'Cart | null', 'Yes', 'The cart data object.'], ] ``` ### Returns Returns the selected payment method object or `null` if none is selected. ### Usage ```ts const cartData = await getCart(); const paymentMethod = getCartPaymentMethod(cartData); if (paymentMethod) { console.log(`Payment method: ${paymentMethod.code}`); } ``` ## DOM and Fragment Functions ### createFragment The `createFragment` function creates a `DocumentFragment` from an HTML string. ```ts export function createFragment(html: string): DocumentFragment; ``` ```text [ ['Parameter', 'Type', 'Req?', 'Description'], ['html', 'string', 'Yes', 'The HTML string to convert to a DocumentFragment.'], ] ``` ### Returns Returns a DocumentFragment containing the parsed HTML. ### Usage ```ts const html = ` ## Payment Information
`; const fragment = createFragment(html); document.querySelector('.checkout-container').appendChild(fragment); ``` ### createScopedSelector The `createScopedSelector` function creates a scoped `querySelector` function for a DocumentFragment. ```ts export function createScopedSelector( fragment: DocumentFragment ): (selector: string) => HTMLElement | null; ``` ```text [ ['Parameter', 'Type', 'Req?', 'Description'], ['fragment', 'DocumentFragment', 'Yes', 'The DocumentFragment to scope the selector to.'], ] ``` ### Returns Returns a function that queries elements within the given fragment. ### Usage ```ts const html = ` `; const fragment = createFragment(html); const $ = createScopedSelector(fragment); // Query within the fragment only const nextButton = $('.next-btn'); const prevButton = $('.prev-btn'); nextButton?.addEventListener('click', handleNext); ``` ## Form Functions ### validateForm The `validateForm` function validates a form by name using form references. ```ts export function validateForm( formName: string, formRef: RefObject ): boolean; ``` ```text [ ['Parameter', 'Type', 'Req?', 'Description'], ['formName', 'string', 'Yes', 'The name attribute of the form to validate.'], ['formRef', 'RefObject', 'Yes', 'Reference object to the form component.'], ] ``` ### Returns Returns `true` if the form is valid, `false` otherwise. ### Usage ```ts // Validate checkout form before submission const isShippingValid = validateForm('shipping-form', shippingFormRef); const isBillingValid = validateForm('billing-form', billingFormRef); if (isShippingValid && isBillingValid) { proceedToPayment(); } else { showValidationErrors(); } ``` ## Meta Functions ### createMetaTag The `createMetaTag` function creates or updates meta tags in the document head. ```ts export function createMetaTag(property: string, content: string, type: string): void; ``` ```text [ ['Parameter', 'Type', 'Req?', 'Description'], ['property', 'string', 'Yes', 'The property/name of the meta tag.'], ['content', 'string', 'Yes', 'The content value for the meta tag.'], ['type', 'string', 'Yes', 'The type of meta tag: "name" or "property".'], ] ``` ### Returns The function does not return a value; it modifies the document head. ### Usage ```ts // Set checkout-specific meta tags createMetaTag('description', 'Complete your purchase securely', 'name'); createMetaTag('og:title', 'Checkout - Your Store', 'property'); ``` ### setMetaTags The `setMetaTags` function sets standard meta tags for a drop-in component. ```ts export function setMetaTags(dropin: string): void; ``` ```text [ ['Parameter', 'Type', 'Req?', 'Description'], ['dropin', 'string', 'Yes', 'The name of the drop-in component.'], ] ``` ### Returns The function does not return a value; it sets multiple meta tags. ### Usage ```ts // Set meta tags for checkout page setMetaTags('Checkout'); ``` ## Utility Functions ### scrollToElement The `scrollToElement` function smoothly scrolls to and focuses on an HTML element. ```ts export function scrollToElement(element: HTMLElement): void; ``` ```text [ ['Parameter', 'Type', 'Req?', 'Description'], ['element', 'HTMLElement', 'Yes', 'The element to scroll to and focus.'], ] ``` ### Returns The function does not return a value; it performs scrolling and focusing. ### Usage ```ts // Scroll to error field const errorField = document.querySelector('.field-error'); if (errorField) { scrollToElement(errorField); } // Scroll to next checkout step const nextStep = document.querySelector('.checkout-step.active'); scrollToElement(nextStep); ``` ## Data Transformer Functions ### transformAddressFormValuesToCartAddressInput The `transformAddressFormValuesToCartAddressInput` function converts form data to cart address input format. ```ts export const transformAddressFormValuesToCartAddressInput = ( data: Record ): ShippingAddressInput | BillingAddressInput; ``` ```text [ ['Parameter', 'Type', 'Req?', 'Description'], ['data', 'Record', 'Yes', 'Form data object containing address information.'], ] ``` ### Returns Returns a formatted address input object for cart API calls. ### Usage ```ts // Transform form data for API const formData = getFormValues(addressForm); const addressInput = transformAddressFormValuesToCartAddressInput(formData); // Send to cart API await setShippingAddress(addressInput); ``` ### transformCartAddressToFormValues The `transformCartAddressToFormValues` function converts cart address data to the form values format. ```ts export const transformCartAddressToFormValues = ( address: CartAddress ): Record; ``` ```text [ ['Parameter', 'Type', 'Req?', 'Description'], ['address', 'CartAddress', 'Yes', 'Cart address object to transform.'], ] ``` ### Returns Returns a form-compatible object with address data. ### Usage ```ts // Pre-populate form with existing address const cartData = await getCart(); const shippingAddress = getCartAddress(cartData, 'shipping'); if (shippingAddress) { const formValues = transformCartAddressToFormValues(shippingAddress); populateForm(shippingForm, formValues); } ``` ## Common Usage Patterns These utility functions work together to create robust checkout experiences: ### Complete Address Handling ```ts // Set up address form handling const handleAddressChange = setAddressOnCart({ type: 'shipping', debounceMs: 500, placeOrderBtn: placeOrderAPI }); // Pre-populate form with existing data const cartData = await getCart(); const existingAddress = getCartAddress(cartData, 'shipping'); if (existingAddress) { const formValues = transformCartAddressToFormValues(existingAddress); populateAddressForm(formValues); } // Handle form changes addressForm.addEventListener('input', (event) => { const formData = getFormValues(event.target.form); const isValid = validateForm('shipping-address', formRef); handleAddressChange({ data: formData, isDataValid: isValid }); }); ``` ### Cart State Management ```ts function updateCheckoutUI(cartData) { // Handle empty cart if (isEmptyCart(cartData)) { showEmptyCartMessage(); return; } // Handle virtual cart (no shipping) if (isVirtualCart(cartData)) { hideShippingSection(); } else { const shippingMethod = getCartShippingMethod(cartData); updateShippingDisplay(shippingMethod); } // Update payment display const paymentMethod = getCartPaymentMethod(cartData); updatePaymentDisplay(paymentMethod); } ``` ### Dynamic Content Creation ```ts function createCheckoutStep(stepHtml, stepName) { const fragment = createFragment(stepHtml); const $ = createScopedSelector(fragment); // Set up step-specific interactions const nextButton = $('.next-step'); const prevButton = $('.prev-step'); nextButton?.addEventListener('click', () => { if (validateCurrentStep()) { proceedToNextStep(); } else { const errorField = $('.field-error'); if (errorField) scrollToElement(errorField); } }); return fragment; } ``` --- # Overview Drop-ins are pre-built, customizable UI components that provide complete commerce functionality for your storefront. Each drop-in handles a specific aspect of the shopping experience, from browsing products to completing checkout. | Item | Description | |------|-------------| | [Cart overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/) | Provides editable controls to help you view, update, and merge the products in your cart and mini-cart, including image thumbnails, pricing. | | [Checkout overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/) | Provides customizable controls to help complete a purchase. | | [Order overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/) | Provides tools to manage and display order-related data across various pages and scenarios. | | [Payment Services overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/payment-services/) | Renders the credit card form and the Apple Pay button. | | [Personalization overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/personalization/) | Provides tools to display content conditionally, based on Adobe Commerce customer groups, segments, and cart price rules. | | [Product details page overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/) | Renders detailed information about your products, including descriptions, specifications, options, pricing, and images. | | [Product Discovery overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-discovery/) | Enables you to display and customize product search results, category listings, and faceted navigation. | | [Product Recommendations overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/recommendations/) | Enables you to suggest products to customers based on their browsing patterns and behaviors. | | [User account overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/user-account/) | Provides account management features. | | [User auth overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/user-auth/) | Provides user authentication to allow customers to sign up, log in, and log out. | | [Wishlist overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/wishlist/) | Lets customers store products they are interested in purchasing later. | --- # OrderStatus container The `OrderStatus` container displays the current order status and a service message about the order’s condition. It supports three actions: Return, Cancel, and Reorder. The availability of these actions is defined by the backend, based on the order status, individual items, and global configurations. To display all information, you must enable the following features: - https://experienceleague.adobe.com/en/docs/commerce-admin/stores-sales/order-management/returns/rma-configure - https://experienceleague.adobe.com/en/docs/commerce-admin/stores-sales/shopper-tools/reorders-allow - https://experienceleague.adobe.com/en/docs/commerce-admin/stores-sales/shopper-tools/cancel-allow ![OrderStatus container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/order-status.png) *OrderStatus container* ## Configurations The `OrderStatus` container provides the following configuration options: ```text [ ['Options', 'Type', 'Req?', 'Description'], ['slots.OrderActions', 'slot', 'No', 'Provides the ability to customize / extend available order actions.'], ['orderData', 'OrderDataModel', 'No', 'Contains order information, including the order ID and a list of items.'], ['className', 'string', 'No', 'Allows custom CSS classes to be applied to the form for styling.'], ['statusTitle', 'string', 'No', 'Provides the ability to manually input a custom title for the status section.'], ['status', 'StatusEnumProps', 'No', 'Displays one of the predefined statuses, such as Pending, Shipping, Complete, Processing, On Hold, Canceled, Suspected Fraud, or Payment Review.'], ['routeCreateReturn', 'function', 'No', 'A function that returns the URL to redirect the user to create return page.'], ['onError', 'function', 'No', 'A function executed when an error occurs. It receives an errorInformation object with details about the error.'], ] ``` ## Example The following example demonstrates how to render the `OrderStatus` container: ```javascript export default async function decorate(block) { await orderRenderer.render(OrderStatus, { routeCreateReturn: ({ token, number: orderNumber }) => { const isAuthenticated = checkIsAuthenticated(); const { searchParams } = new URL(window.location.href); const orderRefFromUrl = searchParams.get('orderRef'); const newOrderRef = isAuthenticated ? orderNumber : token; const encodedOrderRef = encodeURIComponent(orderRefFromUrl || newOrderRef); return checkIsAuthenticated() ? `${CUSTOMER_CREATE_RETURN_PATH}?orderRef=${encodedOrderRef}` : `${CREATE_RETURN_PATH}?orderRef=${encodedOrderRef}`; }, routeOnSuccess: () => '/cart', })(block); } ``` --- # CreateReturn container The `CreateReturn` container manages the creation of return requests. It supports custom return attributes and configurable validation through the Adobe Commerce Admin. This container consists of three sequential screens that guide users through the item return process: 1. **Select Items.** The first screen displays a list of items eligible for return. The user can select items by checking the boxes next to them and specifying the quantity of each item they wish to return. Once the selection is complete, the user can click the **Continue** button to proceed to the next step. ![CreateReturn container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/create-return1.png) *CreateReturn container* 1. **Return Reasons.** The second screen shows the selected items. Below each item, an additional form is displayed that allows the customer to specify the reason for the return. This step helps gather information on why each item is being returned, which can be valuable for analytics and improving customer experience. ![CreateReturn container, return reasons screen](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/create-return2.png) *CreateReturn container — Return reasons* 1. **Success Screen.** The final screen displays a success message confirming the return process. It also includes a customizable button that allows redirection to any specified page on the website. ![CreateReturn container, success screen](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/create-return3.png) *CreateReturn container — Success* ## Prerequisites - https://experienceleague.adobe.com/en/docs/commerce-admin/stores-sales/order-management/returns/rma-configure. The **Stores** > Configuration > **Sales** > **Sales** > **RMA Settings** in the Adobe Commerce Admin. - If you need to add custom return attributes, add them at **Stores** > **Attributes** > **Returns**. You can optionally use the Input Validation field to define custom validation rules. ## Configurations The `CreateReturn` container provides the following configuration options: ```text [ ['Options', 'Type', 'Req?', 'Description'], ['className', 'string', 'No', 'Allows custom CSS classes to be applied to the container for styling purposes.'], ['orderData', 'OrderDataModel', 'No', 'A structured object containing order-related data. It can be used as an initial value if data is not fetched from the backend, serving as a fallback.'], ['slots.ReturnOrderItem', 'function', 'No', 'Enables integration of additional elements or functionality, allowing customization to meet specific requirements. This can include adding new components, modifying existing ones, or inserting custom content.'], ['slots.ReturnFormActions', 'function', 'No', 'Provides the ability to add custom events or replace existing actions with tailored functionality. Examples include adding unique buttons, setting up custom redirects, or modifying interface elements.'], ['onSuccess', 'function', 'No', 'A callback function executed after the form is successfully submitted.'], ['onError', 'function', 'No', 'A callback function executed when an error occurs during submission. The error is passed as a parameter for handling.'], ['routeReturnSuccess', 'function', '', 'Defines a custom URL to redirect users upon successful return submission.'], ['showConfigurableOptions', 'function', 'No', 'Allows rendering additional product parameters during container integration by defining key-value pairs for further customization.'], ] ``` ## Example The following example demonstrates how to render the `CreateReturn` container: ```javascript export default async function decorate(block) { await orderRenderer.render(CreateReturn, { routeReturnSuccess: (orderData) => checkIsAuthenticated() ? `${CUSTOMER_ORDER_DETAILS_PATH}?orderRef=${orderData.number}` : `${ORDER_DETAILS_PATH}?orderRef=${orderData.token}`, })(block); } ``` --- # CustomerDetails container The `CustomerDetails` container organizes customer and order information into the following sections: - Contact details - Shipping address - Billing address - Shipping method - Payment method - Return details: The return details section is available exclusively on return pages. It provides information about the return. ![CustomerDetails container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/customer-details.png) *CustomerDetails container* ## Configurations The `CustomerDetails` container provides the following configuration options: ```text [ ['Options', 'Type', 'Req?', 'Description'], ['paymentIconsMap', 'Record', 'No', 'Configures the icon list by specifying key-value pairs to set custom icons where value can be either the name of SDK icon or custom SVG icon.'], ['orderData', 'OrderDataModel', 'No', 'A structured object containing transformed order data. It can be used as an initial value if data is not fetched from the backend, serving as a fallback.'], ['title', 'string', 'No', 'Enables setting a custom title to replace the default one during container interaction.'], ['className', 'string', 'No', 'Allows custom CSS classes to be applied to the form.'], ['slots.OrderReturnInformation', 'SlotProps', 'No', 'Allows adding or expanding the return information details section by including additional data or attributes.'], ] ``` ## Example The following example demonstrates how to integrate the `CustomerDetails` container: ```javascript export default async function decorate(block) { await orderRenderer.render(CustomerDetails, {})(block); } ``` --- # Order Containers The **Order** drop-in provides pre-built container components for integrating into your storefront. Version: 4.0.0 ## What are Containers? Containers are pre-built UI components that combine functionality, state management, and presentation. They provide a complete solution for specific features and can be customized through props, slots, and CSS. ## Available Containers | Container | Description | | --------- | ----------- | | [CreateReturn](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/containers/create-return/) | Learn about the `CreateReturn` container. | | [CustomerDetails](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/containers/customer-details/) | Learn about the `CustomerDetails` container. | | [OrderCancelForm](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/containers/order-cancel-form/) | Learn about the `OrderCancelForm` container. | | [OrderComments](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/containers/order-comments/) | Learn about the `OrderComments` container. | | [OrderCostSummary](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/containers/order-cost-summary/) | Learn about the `OrderCostSummary` container. | | [OrderHeader](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/containers/order-header/) | *Enrichment needed - add description to `_dropin-enrichments/order/containers.json`* | | [OrderProductList](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/containers/order-product-list/) | Learn about the `OrderProductList` container. | | [OrderReturns](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/containers/order-returns/) | Learn about the `OrderReturns` container. | | [OrderSearch](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/containers/order-search/) | Learn about the `OrderSearch` container. | | [OrderStatus](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/containers/order-status/) | *Enrichment needed - add description to `_dropin-enrichments/order/containers.json`* | | [ReturnsList](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/containers/returns-list/) | Learn about the `ReturnsList` container. | | [ShippingStatus](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/containers/shipping-status/) | Learn about the `ShippingStatus` container. | > Each container is designed to work independently but can be composed together to create comprehensive user experiences. --- # OrderCancelForm container The `OrderCancelForm` container provides a cancellation form that allows users to select reasons for canceling an order and perform the cancellation operation. ![OrderCancelForm container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/order-cancel-form.png) * OrderCancelForm container* ## Configurations The `OrderCancelForm container provides the following configuration options: ```text [ ['Options', 'Type', 'Req?', 'Description'], ['orderRef', 'string', 'Yes', 'ID of the order to be canceled.'], ['pickerProps', 'PickerProps', 'No', 'Configuration for the picker used to display and select the reason for order cancellation.'], ['submitButtonProps', 'ButtonProps', 'No', 'Configuration for the button used to submit the order cancellation.'], ['cancelReasons', 'PickerOption[]', 'Yes', 'An array of reasons available for order cancellation.'], ] ``` ## Example The `OrderCancelForm` container is not directly integrated within the boilerplate, but it is delivered as part of the `OrderStatus` container. However, the `OrderCancelForm` container can also be used independently to create custom implementations. Here’s an integration example from the drop-in component development environment: ```javascript provider.render(OrderCancelForm, { orderRef: "", pickerProps: {} , submitButtonProps: {} , cancelReasons: [] , })(containerWrapper); ``` --- # OrderComments container The `OrderComments` container displays order-level comments on the Order Details page. It renders a read-only list of comments associated with the order, each showing a timestamp and message. - **Comment list**: Displays all comments from the `CustomerOrder.comments` GraphQL field in a chronological list, with each entry showing a formatted date and time alongside the comment message. - **Empty state**: When there are no comments for the order, the container displays an empty state message ("No order comments."). - **Loading state**: While order data is being fetched, the container displays a skeleton loader. The container listens to the `order/data` event to receive order data. When order data becomes available, it extracts the `comments` array and renders the comment list. ## Configurations The `OrderComments` container provides the following configuration options: ```text [ ['Options', 'Type', 'Req?', 'Description'], ['orderData', 'OrderDataModel', 'No', 'A structured object containing transformed order data. It can be used as an initial value if data is not fetched from the backend, serving as a fallback.'], ['className', 'string', 'No', 'Allows custom CSS classes to be applied to the container.'], ] ``` ## Example The following example demonstrates how to render the `OrderComments` container: ```javascript export default async function decorate(block) { await orderRenderer.render(OrderComments, {})(block); } ``` --- # OrderCostSummary container The `OrderCostSummary` container displays detailed order costs on the Order Details and Return Details pages. It includes the following sections: - **Subtotal**: Displays the total cost of all items in the order before applying discounts, taxes, or additional charges. - **Shipping**: Displays the shipping cost, which depends on the shipping method, location, and weight of the order. - **Discount**: Displays any applicable discounts, such as promotional or volume-based offers, subtracted from the subtotal. - **Coupon**: Displays the value of any applied coupons and their impact on the final cost. - **Tax**: Displays the tax amount added to the order, calculated based on jurisdiction and item type. - **Total**: Displays the final payable amount, including all adjustments such as discounts, shipping, and taxes. If a value is not provided for any section (such as no discount or coupons are applied), the corresponding line is hidden. This ensures the container only displays relevant information. The settings for displaying tax amounts can be configured at **Stores** > Configuration > **Sales** > **Tax** > **Order, Invoices, Credit Memos Display Settings**. ![OrderCostSummary container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/order-cost-summary.png) *OrderCostSummary container* ## Configurations The `OrderCostSummary` container provides the following configuration options: ```text [ ['Options', 'Type', 'Req?', 'Description'], ['withHeader', 'boolean', 'Yes', 'Enables showing or hiding the container header.'], ['orderData', 'OrderDataModel', 'No', 'A structured object containing transformed order data. It can be used as an initial value if data is not fetched from the backend, serving as a fallback.'], ['className', 'string', 'No', 'Allows custom CSS classes to be applied to the form.'], ] ``` ## Example The following example demonstrates how to render the `OrderCostSummary` container: ```javascript export default async function decorate(block) { await orderRenderer.render(OrderCostSummary, {})(block); } ``` --- # OrderHeader Container Version: 4.0.0 ## Configuration The `OrderHeader` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `handleEmailAvailability` | `function` | No | Callback to check whether a given email is available (not yet registered). Used to conditionally offer sign-up during guest order lookup. | | `handleSignUpClick` | `function` | No | Callback invoked when the sign-up action is triggered from the order header. | | `orderData` | `OrderDataModel` | No | A structured object containing order data used to pre-populate the header. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `OrderHeader` container: ```js await provider.render(OrderHeader, { handleEmailAvailability: handleEmailAvailability, handleSignUpClick: handleSignUpClick, orderData: orderData, })(block); ``` --- # OrderProductList container The `OrderProductList` container displays a list of products associated with a specific order or return. Each item in the list is represented by a product card containing details such as the price, applied discounts, tax information, final amount, and product attributes. The settings for displaying tax amounts can be configured at **Stores** > Configuration > **Sales** > **Tax** > **Order, Invoices, Credit Memos Display Settings**. ![OrderProductList container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/order-product-list.png) *OrderProductList container* ## Configurations The `OrderProductList` container provides the following configuration options: ```text [ ['Options', 'Type', 'Req?', 'Description'], ['className', 'string', 'No', 'Allows custom CSS classes to be applied to the form.'], ['orderData', 'OrderDataModel', 'No', 'A structured object containing transformed order data. It can be passed as an initial value and used as a fallback if data is not received from the backend.'], ['withHeader', 'boolean', 'No', 'Controls the visibility of the container header, allowing it to be shown or hidden.'], ['showConfigurableOptions', 'function', 'No', 'Allows rendering additional product parameters during container integration by defining key-value pairs for further customization.'], ['routeProductDetails', 'function', 'No', 'A function that returns the URL for the product details page. Receives the product data as an argument.'], ] ``` ## Example The following example demonstrates how to render the `OrderProductList` container: ```javascript export default async function decorate(block) { await orderRenderer.render(OrderProductList, { routeProductDetails: (product) => `/products/${product.productUrlKey}/${product.product.sku}`, })(block); } ``` --- # OrderReturns container The `OrderReturns` container displays the list of returns associated with a specific order. Each return is presented with relevant details, such as return status and associated items. If no returns have been created for the order, the container is not rendered, ensuring that the interface remains clean and free of unnecessary placeholders. ![OrderReturns container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/order-returns.png) *OrderReturns container* ## Configurations The `OrderReturns` container provides the following configuration options: ```text [ ['Options', 'Type', 'Req?', 'Description'], ['slot.ReturnItemsDetails', 'slot', 'No', 'Provides the ability to expand information for a specific card. Allows adding additional data or attributes to make the card more detailed and customizable, adapting it to specific product or interface requirements.'], ['slot.DetailsActionParams', 'slot', 'No', 'Enables customization of actions by adding elements, buttons, or links to replace the default setup. This allows for tailored functionality to meet specific user tasks or requirements.'], ['className', 'string', 'No', 'Allows custom CSS classes to be applied to the form for styling.'], ['orderData', 'OrderDataModel', 'No', 'A structured object containing transformed order data. It can be passed as an initial value and used as a fallback if data is not received from the backend.'], ['withHeader', 'boolean', 'No', 'Controls the visibility of the container header, allowing it to be shown or hidden.'], ['withThumbnails', 'boolean', 'No', 'Enables or disables the display of product thumbnails on order cards.'], ['routeReturnDetails', 'function', 'No', 'Specifies the URL where the return number link redirects the customer.'], ['routeProductDetails', 'function', 'No', 'A function that returns the URL for the product details page.'], ['routeTracking', 'function', 'No', 'Specifies the URL where the tracking number link redirects the customer.'], ] ``` ## Example The following example demonstrates how to render the `OrderReturns` container: ```javascript export default async function decorate(block) { const isAuthenticated = checkIsAuthenticated(); const returnDetailsPath = isAuthenticated ? CUSTOMER_RETURN_DETAILS_PATH : RETURN_DETAILS_PATH; await orderRenderer.render(OrderReturns, { routeTracking: ({ carrier, number }) => { if (carrier?.toLowerCase() === 'ups') { return `${UPS_TRACKING_URL}?tracknum=${number}`; } return ''; }, routeReturnDetails: ({ orderNumber, returnNumber, token }) => { const { searchParams } = new URL(window.location.href); const orderRefFromUrl = searchParams.get('orderRef'); const newOrderRef = isAuthenticated ? orderNumber : token; const encodedOrderRef = encodeURIComponent(orderRefFromUrl || newOrderRef); return `${returnDetailsPath}?orderRef=${encodedOrderRef}&returnRef=${returnNumber}`; }, routeProductDetails: (productData) => (productData ? `/products/${productData.product.urlKey}/${productData.product.sku}` : '#'), })(block); } ``` --- # OrderSearch container The `OrderSearch` container enables order searches using email, last name, and order number. It is available to both guest and registered users for quick access to order details. ![OrderSearch container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/order-search.png) *OrderSearch container* ## Configurations The `OrderSearch` container provides the following configuration options: ```text [ ['Options', 'Type', 'Req?', 'Description'], ['className', 'string', 'No', 'Allows custom CSS classes to be applied to the form for styling.'], ['isAuth', 'boolean', 'No', 'Indicates whether the user is authenticated.'], ['renderSignIn', 'function', 'No', 'A function responsible for rendering the sign-in form for the user.'], ['routeGuestOrder', 'function', 'No', 'A function that returns the URL for the guest (unauthenticated user) order route.'], ['routeCustomerOrder', 'function', 'No', 'A function that returns the URL for an authenticated customer order details page.'], ['onError', 'function', 'No', 'A function executed when an error occurs. It receives an errorInformation object containing details about the error.'], ] ``` ## Example The following example demonstrates how to render the `OrderSearch` container: ```javascript const renderSignIn = async (element, email, orderNumber) => authRenderer.render(SignIn, { initialEmailValue: email, renderSignUpLink: false, labels: { formTitleText: email ? 'Enter your password to view order details' : 'Sign in to view order details', primaryButtonText: 'View order', }, routeForgotPassword: () => 'reset-password.html', routeRedirectOnSignIn: () => `${CUSTOMER_ORDER_DETAILS_PATH}?orderRef=${orderNumber}`, })(element); export default async function decorate(block) { block.innerHTML = ''; events.on('order/data', async (order) => { if (!order) return; block.innerHTML = ''; await orderRenderer.render(OrderSearch, { isAuth: checkIsAuthenticated(), renderSignIn: async ({ render, formValues }) => { if (render) { renderSignIn( block, formValues?.email ?? '', formValues?.number ?? '', ); return false; } return true; }, routeCustomerOrder: () => CUSTOMER_ORDER_DETAILS_PATH, routeGuestOrder: () => ORDER_DETAILS_PATH, onError: async (errorInformation) => { console.info('errorInformation', errorInformation); }, })(block); }); await orderRenderer.render(OrderSearch, { isAuth: checkIsAuthenticated(), renderSignIn: async ({ render, formValues }) => { if (render) { renderSignIn(block, formValues?.email ?? '', formValues?.number ?? ''); return false; } return true; }, routeCustomerOrder: () => CUSTOMER_ORDER_DETAILS_PATH, routeGuestOrder: () => ORDER_DETAILS_PATH, onError: async (errorInformation) => { console.info('errorInformation', errorInformation); }, })(block); } ``` --- # OrderStatus Container Version: 4.0.0 ## Configuration The `OrderStatus` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `className` | `string` | No | Additional CSS classes to apply to the container | | `orderData` | `OrderDataModel` | No | A structured object containing order data. Used as an initial value or fallback if data is not fetched from the backend. | | `statusTitle` | `string` | No | Custom title text to display above the order status indicator. | | `status` | `StatusEnumProps` | No | The current order status value used to display the status indicator. | | `routeCreateReturn` | `function` | No | Function that returns the URL for the create return page. | | `routeOnSuccess` | `function` | No | Function that returns the URL to redirect to after a successful action. | | `onError` | `function` | No | Callback function triggered when error | ## Slots This container exposes the following slots for customization: | Slot | Type | Required | Description | |------|------|----------|-------------| | `OrderActions` | `SlotProps` | Yes | | ## Usage The following example demonstrates how to use the `OrderStatus` container: ```js await provider.render(OrderStatus, { className: "Example Name", orderData: orderData, statusTitle: "Example Title", slots: { // Add custom slot implementations here } })(block); ``` ## Administrator-assisted orders When the seller-assisted buying feature is in use, the `OrderStatus` container shows an "Order placed by an administrator" label when `adminAssistedOrder` is set on the order. The label renders with the CSS class `.order-order-status-content__admin-assisted` and appears automatically. No configuration is required. ![Order status page showing the administrator label](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/SAB-OrderStatus.png) *Order status page showing the administrator label* To customize the label text, override the `Order.OrderStatusContent.adminAssistedLabel` dictionary key in the order initializer. See the [Order dictionary](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/dictionary/) for the key and [Seller-assisted buying](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/user-account/containers/seller-assisted-buying/) for the end-to-end flow. {/* Verified against adobe-commerce/storefront-order src/components/OrderStatusContent/OrderStatusContent.tsx: adminAssistedOrder (boolean), label key Order.OrderStatusContent.adminAssistedLabel, CSS class .order-order-status-content__admin-assisted; src/i18n/en_US.json (Order.OrderStatusContent.adminAssistedLabel = "Order placed by an administrator"). */} --- # ReturnsList container The `ReturnsList` container displays a complete list of all created returns available to the user. Each return card follows the same structure as the `OrderReturns` container, allowing consistent presentation of return details. It provides an overview of all return requests, enabling users to manage and track their status in one place. ![ReturnsList container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/returns-list.png) *ReturnsList container* ## Configurations The `ReturnsList` container provides the following configuration options: ```text [ ['Options', 'Type', 'Req?', 'Description'], ['slot.ReturnItemsDetails', 'slot', 'No', 'Provides the ability to expand information for a specific return card. Allows adding additional data or attributes to make the card more detailed and customizableto specific requirements.'], ['slot.DetailsActionParams', 'slot', 'No', 'Enables customization of actions by adding elements, buttons, or links to replace the default setup. This allows for tailored functionality to meet specific user tasks or requirements.'], ['withReturnsListButton', 'boolean', 'No', 'Determines whether the button at the bottom of the container is visible (applies only in minified view).'], ['className', 'string', 'No', 'Allows custom CSS classes to be applied to the form for styling.'], ['minifiedView', 'boolean', 'No', 'Enables or disables the minified view of the container.'], ['withHeader', 'boolean', 'No', 'Controls the visibility of the container header.'], ['withThumbnails', 'boolean', 'No', 'Enables or disables the display of product thumbnails on order cards.'], ['returnPageSize', 'number', 'No', 'Specifies the number of items displayed on a single page of the returns list.'], ['returnsInMinifiedView', 'number', 'No', 'Defines the number of returns visible in the minified view (default is 1).'], ['routeReturnDetails', 'function', 'No', 'Specifies the URL where the return number link redirects the customer.'], ['routeOrderDetails', 'function', 'No', 'Specifies the URL where the customer should be redirected when clicking the order number link (number and token).'], ['routeTracking', 'function', 'No', 'Specifies the URL where the tracking number link redirects the customer.'], ['routeReturnsList', 'function', 'No', 'Defines the URL for the button click at the bottom of the container.'], ['routeProductDetails', 'function', 'No', 'A function that returns the URL for the product details page.'], ] ``` ## Example The following example demonstrates how to render the `ReturnsList` container: ```javascript export default async function decorate(block) { const { 'minified-view': minifiedViewConfig = 'false', } = readBlockConfig(block); if (!checkIsAuthenticated()) { window.location.href = CUSTOMER_LOGIN_PATH; } else { await orderRenderer.render(ReturnsList, { minifiedView: minifiedViewConfig === 'true', routeTracking: ({ carrier, number }) => { if (carrier?.toLowerCase() === 'ups') { return `${UPS_TRACKING_URL}?tracknum=${number}`; } return ''; }, routeReturnDetails: ({ orderNumber, returnNumber }) => `${CUSTOMER_RETURN_DETAILS_PATH}?orderRef=${orderNumber}&returnRef=${returnNumber}`, routeOrderDetails: ({ orderNumber }) => `${CUSTOMER_ORDER_DETAILS_PATH}?orderRef=${orderNumber}`, routeReturnsList: () => CUSTOMER_RETURNS_PATH, routeProductDetails: (productData) => (productData ? `/products/${productData.product.urlKey}/${productData.product.sku}` : '#'), })(block); } } ``` --- # ShippingStatus container The `ShippingStatus` container displays information about shipments, including product images, the delivery service used, and tracking numbers. A separate block is rendered for each shipment created for the order. It also lists products that have not yet been shipped, providing a clear overview of the shipping status for all items. ![ShippingStatus container](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/shipping-status.png) *ShippingStatus container* ## Configurations The `ShippingStatus` container provides the following configuration options: ```text [ ['Options', 'Type', 'Req?', 'Description'], ['slots.DeliveryTimeLine', 'slot', 'No', 'Allows integration of the delivery process in a timeline format. Displays key events from dispatch to arrival, making it easier to track delivery progress and view the current stage in real-time.'], ['slots.DeliveryTrackActions', 'slot', 'No', 'Enables integration of custom actions related to order and delivery tracking. Allows customization with parameters such as delivery type, status updates, and timestamps, providing flexibility and control over user interactions and tracking.'], ['slots.ReturnItemsDetails', 'slot', 'No', 'Supports adding or customizing additional data for return items, enabling tailored content to meet specific requirements.'], ['className', 'string', 'No', 'CSS class for additional styling customization of the container.'], ['collapseThreshold', 'number', 'No', 'Sets the minimum number of elements required for images to be displayed in an accordion view.'], ['orderData', 'OrderDataModel', 'No', 'Contains order data, including the order ID and a list of items.'], ['routeOrderDetails', 'function', 'No', 'A function that returns the URL for the product details route.'], ['routeTracking', 'function', 'No', 'Specifies the URL where the customer should be redirected when clicking the tracking number link.'], ['routeProductDetails', 'function', 'No', 'A function that returns the URL for the product details page.'], ] ``` ## Example The following example demonstrates how to render the `ShippingStatus` container: ```javascript export default async function decorate(block) { await orderRenderer.render(ShippingStatus, { routeTracking: ({ carrier, number }) => { if (carrier?.toLowerCase() === 'ups') { return `${UPS_TRACKING_URL}?tracknum=${number}`; } return ''; }, routeProductDetails: (data) => { if (data?.orderItem) { return `/products/${data?.orderItem?.productUrlKey}/${data?.orderItem?.product?.sku}`; } if (data?.product) { return `/products/${data?.product?.urlKey}/${data?.product?.sku}`; } return '#'; }, })(block); } ``` --- # Order Dictionary The **Order dictionary** contains all user-facing text, labels, and messages displayed by this drop-in. Customize the dictionary to: - **Localize** the drop-in for different languages and regions - **Customize** labels and messages to match your brand voice - **Override** default text without modifying source code for the drop-in Dictionaries use the **i18n (internationalization)** pattern, where each text string is identified by a unique key path. Version: 4.0.0 ## How to customize Override dictionary values during drop-in initialization. The drop-in deep-merges your custom values with the defaults. ```javascript await initialize({ langDefinitions: { en_US: { "Order": { "CreateReturn": { "headerText": "Your custom message here", "downloadableCount": "Custom value" } } } } }); ``` You only need to include the keys you want to change. For multi-language support and advanced patterns, see the [Dictionary customization guide](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/dictionaries/). ## Default keys and values Below are the default English (`en_US`) strings provided by the **Order** drop-in: ```json title="en_US.json" { "Order": { "CreateReturn": { "headerText": "Return items", "downloadableCount": "Files", "returnedItems": "Returned items:", "configurationsList": { "quantity": "Quantity" }, "stockStatus": { "inStock": "In stock", "outOfStock": "Out of stock" }, "giftCard": { "sender": "Sender", "recipient": "Recipient", "message": "Note" }, "success": { "title": "Return submitted", "message": "Your return request has been successfully submitted." }, "buttons": { "nextStep": "Continue", "backStep": "Back", "submit": "Submit return", "backStore": "Back to order" } }, "OrderComments": { "emptyState": "No order comments.", "title": "Order comments" }, "OrderCostSummary": { "headerText": "Order summary", "headerReturnText": "Return summary", "totalFree": "Free", "subtotal": { "title": "Subtotal" }, "shipping": { "title": "Shipping", "freeShipping": "Free shipping" }, "appliedGiftCards": { "label": { "singular": "Gift card", "plural": "Gift cards" } }, "giftOptionsTax": { "printedCard": { "title": "Printer card", "inclTax": "Including taxes", "exclTax": "Excluding taxes" }, "itemGiftWrapping": { "title": "Item gift wrapping", "inclTax": "Including taxes", "exclTax": "Excluding taxes" }, "orderGiftWrapping": { "title": "Order gift wrapping", "inclTax": "Including taxes", "exclTax": "Excluding taxes" } }, "tax": { "accordionTitle": "Taxes", "accordionTotalTax": "Tax Total", "totalExcludingTaxes": "Total excluding taxes", "title": "Tax", "incl": "Including taxes", "excl": "Excluding taxes" }, "discount": { "title": "Discount", "subtitle": "discounted" }, "total": { "title": "Total" } }, "Returns": { "minifiedView": { "returnsList": { "viewAllOrdersButton": "View all returns", "ariaLabelLink": "Redirect to full order information", "emptyOrdersListMessage": "No returns", "minifiedViewTitle": "Recent returns", "orderNumber": "Order number:", "returnNumber": "Return number:", "carrier": "Carrier:", "itemText": { "none": "", "one": "item", "many": "items" }, "returnStatus": { "pending": "Pending", "authorized": "Authorized", "partiallyAuthorized": "Partially authorized", "received": "Received", "partiallyReceived": "Partially received", "approved": "Approved", "partiallyApproved": "Partially approved", "rejected": "Rejected", "partiallyRejected": "Partially rejected", "denied": "Denied", "processedAndClosed": "Processed and closed", "closed": "Closed" } } }, "fullSizeView": { "returnsList": { "viewAllOrdersButton": "View all orders", "ariaLabelLink": "Redirect to full order information", "emptyOrdersListMessage": "No returns", "minifiedViewTitle": "Returns", "orderNumber": "Order number:", "returnNumber": "Return number:", "carrier": "Carrier:", "itemText": { "none": "", "one": "item", "many": "items" }, "returnStatus": { "pending": "Pending", "authorized": "Authorized", "partiallyAuthorized": "Partially authorized", "received": "Received", "partiallyReceived": "Partially received", "approved": "Approved", "partiallyApproved": "Partially approved", "rejected": "Rejected", "partiallyRejected": "Partially rejected", "denied": "Denied", "processedAndClosed": "Processed and closed", "closed": "Closed" } } } }, "OrderProductListContent": { "cancelledTitle": "Cancelled", "allOrdersTitle": "Your order", "returnedTitle": "Returned", "refundedTitle": "Your refunded", "downloadableCount": "Files", "stockStatus": { "inStock": "In stock", "outOfStock": "Out of stock" }, "GiftCard": { "sender": "Sender", "recipient": "Recipient", "message": "Note" } }, "OrderSearchForm": { "title": "Enter your information to view order details", "description": "You can find your order number in the receipt you received via email.", "button": "View Order", "email": "Email", "lastname": "Last Name", "orderNumber": "Order Number" }, "Form": { "notifications": { "requiredFieldError": "This is a required field." } }, "ShippingStatusCard": { "orderNumber": "Order number:", "returnNumber": "Return number:", "itemText": { "none": "", "one": "Package contents ({{count}} item)", "many": "Package contents ({{count}} items)" }, "trackButton": "Track package", "carrier": "Carrier:", "prepositionOf": "of", "returnOrderCardTitle": "Package details", "shippingCardTitle": "Package details", "shippingInfoTitle": "Shipping information", "notYetShippedTitle": "Not yet shipped", "notYetShippedImagesTitle": { "singular": "Package contents ({{count}} item)", "plural": "Package contents ({{count}} items)" } }, "OrderStatusContent": { "noInfoTitle": "Check back later for more details.", "adminAssistedLabel": "Order placed by an administrator", "returnMessage": "The order was placed on {ORDER_CREATE_DATE} and your return process started on {RETURN_CREATE_DATE}", "returnStatus": { "pending": "Pending", "authorized": "Authorized", "partiallyAuthorized": "Partially authorized", "received": "Received", "partiallyReceived": "Partially received", "approved": "Approved", "partiallyApproved": "Partially approved", "rejected": "Rejected", "partiallyRejected": "Partially rejected", "denied": "Denied", "processedAndClosed": "Processed and closed", "closed": "Closed" }, "actions": { "cancel": "Cancel order", "confirmGuestReturn": "Return request confirmed", "confirmGuestReturnMessage": "Your return request has been successfully confirmed.", "createReturn": "Return or replace", "createAnotherReturn": "Start another return", "reorder": "Reorder" }, "orderPlaceholder": { "title": "", "message": "Your order has been in its current status since {DATE}.", "messageWithoutDate": "Your order has been in its current status for some time." }, "orderPending": { "title": "Pending", "message": "The order was successfully placed on {DATE} and your order is processing. Check back for more details when your order ships.", "messageWithoutDate": "Your order is processing. Check back for more details when your order ships." }, "orderProcessing": { "title": "Processing", "message": "The order was successfully placed on {DATE} and your order is processing. Check back for more details when your order ships.", "messageWithoutDate": "Your order is processing. Check back for more details when your order ships." }, "orderOnHold": { "title": "On hold", "message": "We’ve run into an issue while processing your order on {DATE}. Please check back later or contact us at support@adobe.com for more information.", "messageWithoutDate": "We’ve run into an issue while processing your order. Please check back later or contact us at support@adobe.com for more information." }, "orderReceived": { "title": "Order received", "message": "The order was successfully placed on {DATE} and your order is processing. Check back for more details when your order ships.", "messageWithoutDate": "Your order is processing. Check back for more details when your order ships." }, "orderComplete": { "title": "Complete", "message": "Your order is complete. Need help with your order? Contact us at support@adobe.com" }, "orderCanceled": { "title": "Canceled", "message": "This order was cancelled by you. You should see a refund to your original payment method with 5-7 business days.", "messageWithoutDate": "This order was cancelled by you. You should see a refund to your original payment method with 5-7 business days." }, "orderSuspectedFraud": { "title": "Suspected fraud", "message": "We’ve run into an issue while processing your order on {DATE}. Please check back later or contact us at support@adobe.com for more information.", "messageWithoutDate": "We’ve run into an issue while processing your order. Please check back later or contact us at support@adobe.com for more information." }, "orderPaymentReview": { "title": "Payment Review", "message": "The order was successfully placed on {DATE} and your order is processing. Check back for more details when your order ships.", "messageWithoutDate": "Your order is processing. Check back for more details when your order ships." }, "guestOrderCancellationRequested": { "title": "Cancellation requested", "message": "The cancellation has been requested on {DATE}. Check your email for further instructions.", "messageWithoutDate": "The cancellation has been requested. Check your email for further instructions." }, "orderPendingPayment": { "title": "Pending Payment", "message": "The order was successfully placed on {DATE}, but it is awaiting payment. Please complete the payment so we can start processing your order.", "messageWithoutDate": "Your order is awaiting payment. Please complete the payment so we can start processing your order." }, "orderRejected": { "title": "Rejected", "message": "Your order was rejected on {DATE}. Please contact us for more information.", "messageWithoutDate": "Your order was rejected. Please contact us for more information." }, "orderAuthorized": { "title": "Authorized", "message": "Your order was successfully authorized on {DATE}. We will begin processing your order shortly.", "messageWithoutDate": "Your order was successfully authorized. We will begin processing your order shortly." }, "orderPaypalCanceledReversal": { "title": "PayPal Canceled Reversal", "message": "The PayPal transaction reversal was canceled on {DATE}. Please check your order details for more information.", "messageWithoutDate": "The PayPal transaction reversal was canceled. Please check your order details for more information." }, "orderPendingPaypal": { "title": "Pending PayPal", "message": "Your order is awaiting PayPal payment confirmation since {DATE}. Please check your PayPal account for the payment status.", "messageWithoutDate": "Your order is awaiting PayPal payment confirmation. Please check your PayPal account for the payment status." }, "orderPaypalReversed": { "title": "PayPal Reversed", "message": "The PayPal payment was reversed on {DATE}. Please contact us for further details.", "messageWithoutDate": "The PayPal payment was reversed. Please contact us for further details." }, "orderClosed": { "title": "Closed", "message": "The order placed on {DATE} has been closed. For any further assistance, please contact support.", "messageWithoutDate": "Your order has been closed. For any further assistance, please contact support." } }, "CustomerDetails": { "headerText": "Customer information", "freeShipping": "Free shipping", "orderReturnLabels": { "createdReturnAt": "Return requested on: ", "returnStatusLabel": "Return status: ", "orderNumberLabel": "Order number: " }, "returnStatus": { "pending": "Pending", "authorized": "Authorized", "partiallyAuthorized": "Partially authorized", "received": "Received", "partiallyReceived": "Partially received", "approved": "Approved", "partiallyApproved": "Partially approved", "rejected": "Rejected", "partiallyRejected": "Partially rejected", "denied": "Denied", "processedAndClosed": "Processed and closed", "closed": "Closed" }, "email": { "title": "Contact details" }, "shippingAddress": { "title": "Shipping address" }, "shippingMethods": { "title": "Shipping method" }, "billingAddress": { "title": "Billing address" }, "paymentMethods": { "title": "Payment method" }, "returnInformation": { "title": "Return details" } }, "Errors": { "invalidOrder": "Invalid order. Please try again.", "invalidSearch": "No order found with these order details." }, "OrderCancel": { "buttonText": "Cancel Order" }, "OrderCancelForm": { "title": "Cancel order", "description": "Select a reason for canceling the order", "label": "Reason for cancel", "button": "Submit Cancellation", "errorHeading": "Error", "errorDescription": "There was an error processing your order cancellation." }, "OrderHeader": { "title": "{{name}}, thank you for your order!", "defaultTitle": "Thank you for your order!", "order": "ORDER #{{order}}", "CreateAccount": { "message": "Save your information for faster checkout next time.", "button": "Create an account" } } } } ``` --- # Order Data & Events The **Order** drop-in uses the [event bus](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/events/) to emit and listen to events for communication between drop-ins and external integrations. Version: 4.0.0 ## Events reference {/* EVENTS_TABLE_START */} | Event | Direction | Description | |-------|-----------|-------------| | [cart/reset](#cartreset-emits) | Emits | Emitted when the component state is reset. | | [order/placed](#orderplaced-emits) | Emits | Emitted when an order is placed. | | [companyContext/changed](#companycontextchanged-listens) | Listens | Fired by Company Context (`companyContext`) when a change occurs. | | [order/data](#orderdata-emits-and-listens) | Emits and listens | Triggered when data is available or changes. | | [order/error](#ordererror-emits-and-listens) | Emits and listens | Triggered when an error occurs. | {/* EVENTS_TABLE_END */} ## Event details The following sections provide detailed information about each event, including its direction, event payload, and usage examples. ### `cart/reset` (emits) Emitted when the component state is reset. #### Event payload #### Example ```js events.on('cart/reset', (payload) => { console.log('cart/reset event received:', payload); // Add your custom logic here }); ``` ### `companyContext/changed` (listens) Fired by Company Context (`companyContext`) when a change occurs. #### Event payload ```typescript string | null | undefined ``` #### Example ```js events.on('companyContext/changed', (payload) => { console.log('companyContext/changed event received:', payload); // Add your custom logic here }); ``` ### `order/data` (emits and listens) Emitted when order data is loaded or updated. This includes order details, items, shipping information, and payment status. #### Event payload ```typescript OrderDataModel ``` See [`OrderDataModel`](#orderdatamodel) for full type definition. #### Example ```js events.on('order/data', (payload) => { console.log('order/data event received:', payload); // Add your custom logic here }); ``` ### `order/error` (emits and listens) Emitted when an error occurs during order operations such as fetching order details, reordering items, or processing returns. #### Event payload ```typescript { source: string; type: string; error: Error | string } ``` #### Example ```js events.on('order/error', (payload) => { console.log('order/error event received:', payload); // Add your custom logic here }); ``` ### `order/placed` (emits) Emitted when an order is placed. #### Event payload ```typescript OrderDataModel ``` See [`OrderDataModel`](#orderdatamodel) for full type definition. #### Example ```js events.on('order/placed', (payload) => { console.log('order/placed event received:', payload); // Add your custom logic here }); ``` ## Data Models The following data models are used in event payloads for this drop-in. ### OrderDataModel Used in: [`order/data`](#orderdata-emits-and-listens), [`order/placed`](#orderplaced-emits). ```ts type OrderDataModel = { giftReceiptIncluded: boolean; printedCardIncluded: boolean; giftWrappingOrder: { price: MoneyProps; uid: string; }; placeholderImage?: string; returnNumber?: string; id: string; orderStatusChangeDate?: string; number: string; email: string; token?: string; status: string; isVirtual: boolean; totalQuantity: number; shippingMethod?: string; carrier?: string; orderDate: string; returns: OrdersReturnPropsModel[]; discounts: { amount: MoneyProps; label: string }[]; coupons: { code: string; }[]; payments: { code: string; name: string; }[]; shipping?: { code: string; amount: number; currency: string }; shipments: ShipmentsModel[]; items: OrderItemModel[]; totalGiftCard: MoneyProps; grandTotal: MoneyProps; grandTotalExclTax: MoneyProps; totalShipping?: MoneyProps; subtotalExclTax: MoneyProps; subtotalInclTax: MoneyProps; totalTax: MoneyProps; shippingAddress: OrderAddressModel; totalGiftOptions: { giftWrappingForItems: MoneyProps; giftWrappingForItemsInclTax: MoneyProps; giftWrappingForOrder: MoneyProps; giftWrappingForOrderInclTax: MoneyProps; printedCard: MoneyProps; printedCardInclTax: MoneyProps; }; billingAddress: OrderAddressModel; availableActions: AvailableActionsProps[]; taxes: { amount: MoneyProps; rate: number; title: string }[]; appliedGiftCards: { code: string; appliedBalance: MoneyProps; }[]; }; ``` ### OrderItemProductModel ```ts type OrderItemProductModel = { onlyXLeftInStock?: number; priceRange?: { maximumPrice?: { regularPrice?: MoneyProps; }; }; uid: string; __typename: string; stockStatus?: string; canonicalUrl?: string; urlKey?: string; id: string; image?: string; imageAlt?: string; name: string; productType: string; sku: string; thumbnail: { url: string; label: string; }; giftWrappingAvailable?: boolean; }; ``` ### OrderItemModel ```ts type OrderItemModel = { giftMessage: { senderName: string; recipientName: string; message: string; }; giftWrappingPrice: MoneyProps; productGiftWrapping: { uid: string; design: string; selected: boolean; image: { url: string; label: string; }; price: MoneyProps; }[]; taxCalculations: { includeAndExcludeTax: { originalPrice: MoneyProps; baseOriginalPrice: MoneyProps; baseDiscountedPrice: MoneyProps; baseExcludingTax: MoneyProps; }; excludeTax: { originalPrice: MoneyProps; baseOriginalPrice: MoneyProps; baseDiscountedPrice: MoneyProps; baseExcludingTax: MoneyProps; }; includeTax: { singleItemPrice: MoneyProps; baseOriginalPrice: MoneyProps; baseDiscountedPrice: MoneyProps; }; }; productSalePrice: MoneyProps; status?: string; currentReturnOrderQuantity?: number; eligibleForReturn: boolean; productSku?: string; type?: string; discounted?: boolean; id: string; productName?: string; productUrlKey?: string; regularPrice?: MoneyProps; price: MoneyProps; product?: OrderItemProductModel; selectedOptions?: Array<{ label: string; value: any; }>; thumbnail?: { label: string; url: string; }; downloadableLinks: { count: number; result: string; } | null; prices: { priceIncludingTax: MoneyProps; originalPrice: MoneyProps; originalPriceIncludingTax: MoneyProps; price: MoneyProps; discounts: { label: string; amount: { value: number }; }[]; }; itemPrices: { priceIncludingTax: MoneyProps; originalPrice: MoneyProps; originalPriceIncludingTax: MoneyProps; price: MoneyProps; discounts: { label: string; amount: { value: number }; }[]; }; bundleOptions: Record | null; totalInclTax: MoneyProps; priceInclTax: MoneyProps; total: MoneyProps; configurableOptions: Record | undefined; giftCard?: { senderName: string; senderEmail: string; recipientEmail: string; recipientName: string; message: string; }; quantityCanceled: number; quantityInvoiced: number; quantityOrdered: number; quantityRefunded: number; quantityReturned: number; quantityShipped: number; requestQuantity?: number; totalQuantity: number; returnableQuantity?: number; quantityReturnRequested: number; }; ``` --- # Order Functions The Order drop-in provides API functions that enable you to programmatically control behavior, fetch data, and integrate with Adobe Commerce backend services. Version: 4.0.0 | Function | Description | | --- | --- | | [`cancelOrder`](#cancelorder) | Calls the `cancelOrder` mutation. | | [`confirmCancelOrder`](#confirmcancelorder) | Confirms the cancellation of an order using the provided order ID and confirmation key. | | [`confirmGuestReturn`](#confirmguestreturn) | Confirms a return request for a guest order using an order ID and confirmation key. | | [`getAttributesForm`](#getattributesform) | Calls the `attributesForm` query. | | [`getAttributesList`](#getattributeslist) | Is a wrapper for the `attributesList` query. | | [`getCustomer`](#getcustomer) | Is a wrapper for the customer query. | | [`getCustomerOrdersReturn`](#getcustomerordersreturn) | Returns details about the returns a customer has requested. | | [`getGuestOrder`](#getguestorder) | Is a wrapper for the `guestOrder` query. | | [`getOrderDetailsById`](#getorderdetailsbyid) | Fetches detailed order data by order ID from the Commerce backend. | | [`getStoreConfig`](#getstoreconfig) | Returns information about the storefront configuration. | | [`guestOrderByToken`](#guestorderbytoken) | Retrieves a guest order using a token generated by Adobe Commerce. | | [`placeNegotiableQuoteOrder`](#placenegotiablequoteorder) | Places an order for a negotiable quote. | | [`placeOrder`](#placeorder) | API function for the drop-in. | | [`reorderItems`](#reorderitems) | Allows a logged-in customer to add all the products from a previous order into their cart. | | [`requestGuestOrderCancel`](#requestguestordercancel) | Is similar to the `cancelOrder` function, but it is used for guest orders. | | [`requestGuestReturn`](#requestguestreturn) | Initiates a return request for a guest order. | | [`requestReturn`](#requestreturn) | Takes the `RequestReturnProps` form as an argument and initiates the process of returning items from an order. | | [`setPaymentMethodAndPlaceOrder`](#setpaymentmethodandplaceorder) | Sets the payment method on a cart and immediately places the order. | ## cancelOrder The `cancelOrder` function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/orders/mutations/cancel-order/ mutation. You must pass an order ID and reason. ```ts const cancelOrder = async ( orderId: string, reason: string, onSuccess: Function, onError: Function ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `orderId` | `string` | Yes | The ID of the order to cancel. | | `reason` | `string` | Yes | The reason for canceling the order. | | `onSuccess` | `Function` | Yes | The callback function to execute when the order is successfully canceled. | | `onError` | `Function` | Yes | The callback function to execute when an error occurs. | ### Events Does not emit any drop-in events. ### Returns Returns `void | null | undefined`. ## confirmCancelOrder The `confirmCancelOrder` function confirms the cancellation of an order using the provided order ID and confirmation key. The function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/orders/mutations/confirm-cancel-order/ mutation. ```ts const confirmCancelOrder = async ( orderId: string, confirmationKey: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `orderId` | `string` | Yes | The ID of the order to cancel. | | `confirmationKey` | `string` | Yes | A key generated when a guest requests to cancel an order. | ### Events Emits the [`order/data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/events/#orderdata-emits-and-listens) event with the updated order information after confirming the cancellation. ### Returns Returns `void`. ## confirmGuestReturn The `confirmGuestReturn` function confirms a return request for a guest order using an order ID and confirmation key. The function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/orders/mutations/confirm-return/ mutation. ```ts const confirmGuestReturn = async ( orderId: string, confirmationKey: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `orderId` | `string` | Yes | The ID of the order for which the return is being confirmed. | | `confirmationKey` | `string` | Yes | The confirmation key sent to the guest's email address to authorize the return. | ### Events Emits the [`order/data`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/events/#orderdata-emits-and-listens) event. ### Returns Returns [`OrderDataModel`](#orderdatamodel) or `null`. ## getAttributesForm The `getAttributesForm` function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/attributes/queries/attributes-form/ query. ```ts const getAttributesForm = async ( formCode: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `formCode` | `string` | Yes | One of "customer_account_create", "customer_account_edit", "customer_address_create", "customer_address_edit". | ### Events Does not emit any drop-in events. ### Returns Returns `AttributesFormModel[]`. ## getAttributesList The `getAttributesList` function is a wrapper for the https://developer.adobe.com/commerce/webapi/graphql/schema/attributes/queries/attributes-list/ query. You must pass an attribute code to retrieve the list. The system default values are `CUSTOMER`, `CUSTOMER_ADDRESS`, `CATALOG_PRODUCT` and `RMA_ITEM`. ```ts const getAttributesList = async ( entityType: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `entityType` | `string` | Yes | The entity type for which to retrieve the list of attributes. | ### Events Does not emit any drop-in events. ### Returns Returns `AttributesFormModel[] | []`. ## getCustomer The `getCustomer` function is a wrapper for the https://developer.adobe.com/commerce/webapi/graphql/schema/customer/queries/customer/ query. You must pass a customer ID to retrieve the customer data. ```ts const getCustomer = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns [`CustomerDataModelShort`](#customerdatamodelshort). ## getCustomerOrdersReturn The `getCustomerOrdersReturn` function returns details about the returns a customer has requested. It is a wrapper for the https://developer.adobe.com/commerce/webapi/graphql/schema/customer/queries/customer/ query. ```ts const getCustomerOrdersReturn = async ( pageSize = 10, currentPage = 1 ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `pageSize` | `number` | No | The number of orders to return at a time. | | `currentPage` | `number` | No | See function signature above | ### Events Does not emit any drop-in events. ### Returns Returns [`CustomerOrdersReturnModel`](#customerordersreturnmodel) or `null`. ## getGuestOrder The `getGuestOrder` function is a wrapper for the https://developer.adobe.com/commerce/webapi/graphql/schema/orders/queries/guest-order/ query. ```ts const getGuestOrder = async ( form: { number: string; email: string; lastname: string; } ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `number` | `string` | Yes | The order number. | | `email` | `string` | Yes | The email address associated with the order. | | `lastname` | `string` | Yes | The last name associated with the order. | ### Events Does not emit any drop-in events. ### Returns Returns [`OrderDataModel`](#orderdatamodel) or `null`. ## getStoreConfig The `getStoreConfig` function returns information about the storefront configuration. It is a wrapper for the https://developer.adobe.com/commerce/webapi/graphql/schema/store/queries/store-config/ query. ```ts const getStoreConfig = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns [`StoreConfigModel`](#storeconfigmodel) or `null`. ## guestOrderByToken The `guestOrderByToken` function retrieves a guest order using a token generated by Adobe Commerce. It is a wrapper for the `guestOrderByToken` query. The function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/orders/queries/guest-order-by-token/ query. ```ts const guestOrderByToken = async ( token?: string, returnRef?: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `token` | `string` | No | A token for the order assigned by Adobe Commerce. | | `returnRef` | `string` | No | The reference to return. | ### Events Does not emit any drop-in events. ### Returns Returns [`OrderDataModel`](#orderdatamodel) or `null`. ## placeNegotiableQuoteOrder The `placeNegotiableQuoteOrder` function places an order for a negotiable quote. It is a wrapper for the `placeNegotiableQuoteOrder` mutation. The function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/b2b/negotiable-quote/mutations/place-order/ mutation. ```ts const placeNegotiableQuoteOrder = async ( quoteUid: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `quoteUid` | `string` | Yes | The unique identifier (UID) of the negotiable quote to place as an order. | ### Events Emits the [`order/placed`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/events/#orderplaced-emits) event. ### Returns Returns `OrderDataModel | null | undefined`. See [`OrderDataModel`](#orderdatamodel). ## placeOrder ```ts const placeOrder = async ( cartId: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `cartId` | `string` | Yes | The unique identifier for the shopping cart. This ID is used to track and persist cart data across sessions. | ### Events Emits the following events: [`cart/reset`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/events/#cartreset-emits), [`order/placed`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/events/#orderplaced-emits). ### Returns Returns `OrderDataModel | null | undefined`. See [`OrderDataModel`](#orderdatamodel). ## reorderItems The `reorderItems` function allows a logged-in customer to add all the products from a previous order into their cart. It is a wrapper for the https://developer.adobe.com/commerce/webapi/graphql/schema/orders/mutations/reorder-items/ mutation. ```ts const reorderItems = async ( orderNumber: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `orderNumber` | `string` | Yes | The order number to reorder. | ### Events Does not emit any drop-in events. ### Returns Returns [`ReorderItemsProps`](#reorderitemsprops). ## requestGuestOrderCancel The `requestGuestOrderCancel` function is similar to the `cancelOrder` function, but it is used for guest orders. The token is a unique value generated using guest's email, order number and postcode The function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/orders/mutations/request-guest-order-cancel/ mutation. ```ts const requestGuestOrderCancel = async ( token: string, reason: string, onSuccess: Function, onError: Function ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `token` | `string` | Yes | The token for the order assigned by Adobe Commerce. | | `reason` | `string` | Yes | The reason for canceling the order. | | `onSuccess` | `Function` | Yes | The callback function to execute when the order is successfully canceled. | | `onError` | `Function` | Yes | The callback function to execute when an error occurs. | ### Events Does not emit any drop-in events. ### Returns Returns `void`. ## requestGuestReturn The `requestGuestReturn` function initiates a return request for a guest order. The function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/orders/mutations/request-guest-return/ mutation. ```ts const requestGuestReturn = async ( form: RequestGuestReturnProps ): Promise<{ uid: string; number: string; status: string; createdAt: string; }> ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `form` | `RequestGuestReturnProps` | Yes | The form data for the guest return request, including order details and items to return. | ### Events Does not emit any drop-in events. ### Returns ```ts Promise<{ uid: string; number: string; status: string; createdAt: string; }> ``` ## requestReturn The `requestReturn` function takes the `RequestReturnProps` form as an argument and initiates the process of returning items from an order. It is a wrapper for the https://developer.adobe.com/commerce/webapi/graphql/schema/orders/mutations/request-return/ mutation. ```ts const requestReturn = async ( form: RequestReturnProps ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `form` | `RequestReturnProps` | Yes | The form data for the return request. | ### Events Does not emit any drop-in events. ### Returns Returns `RequestReturnModel | {}`. See [`RequestReturnModel`](#requestreturnmodel). ## setPaymentMethodAndPlaceOrder The `setPaymentMethodAndPlaceOrder` function sets the payment method on a cart and immediately places the order. The function calls the https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/set-payment-method/ mutation. ```ts const setPaymentMethodAndPlaceOrder = async ( cartId: string, paymentMethod: any ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `cartId` | `string` | Yes | The ID of the cart to place as an order. | | `paymentMethod` | `any` | Yes | The payment method information to apply to the cart before placing the order. | ### Events Emits the following events: [`cart/reset`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/events/#cartreset-emits), [`order/placed`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/events/#orderplaced-emits). ### Returns Returns `OrderDataModel | null | undefined`. See [`OrderDataModel`](#orderdatamodel). ## getOrderDetailsById The `getOrderDetailsById` function fetches detailed order data by order number from the Adobe Commerce backend. It supports optional return details and is used internally by the order initialization helpers. ```ts const getOrderDetailsById = async ({ orderId, returnRef, queryType, returnsPageSize, }: GetOrderDetailsByIdProps): Promise> ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `orderId` | `string` | No | The order number to fetch. | | `returnRef` | `string` | No | An optional return reference used to filter return details. | | `queryType` | `QueryType` | Yes | The type of data to query. Use `'orderData'` for standard order details. | | `returnsPageSize` | `number` | No | Number of return records to fetch. Defaults to `50`. | ### Events Does not emit any drop-in events. ### Returns Returns [`OrderDataModel`](#orderdatamodel) or `null`. ## Data Models The following data models are used by functions in this drop-in. ### CustomerDataModelShort The `CustomerDataModelShort` object is returned by the following functions: [`getCustomer`](#getcustomer). ```ts interface CustomerDataModelShort { firstname: string; lastname: string; email: string; } ``` ### CustomerOrdersReturnModel The `CustomerOrdersReturnModel` object is returned by the following functions: [`getCustomerOrdersReturn`](#getcustomerordersreturn). ```ts interface CustomerOrdersReturnModel { ordersReturn: OrdersReturnPropsModel[]; pageInfo?: PageInfoProps; } ``` ### OrderDataModel The `OrderDataModel` object is returned by the following functions: [`confirmGuestReturn`](#confirmguestreturn), [`getGuestOrder`](#getguestorder), [`guestOrderByToken`](#guestorderbytoken), [`placeNegotiableQuoteOrder`](#placenegotiablequoteorder), [`placeOrder`](#placeorder), [`setPaymentMethodAndPlaceOrder`](#setpaymentmethodandplaceorder). ```ts type OrderDataModel = { giftReceiptIncluded: boolean; printedCardIncluded: boolean; giftWrappingOrder: { price: MoneyProps; uid: string; }; placeholderImage?: string; returnNumber?: string; id: string; orderStatusChangeDate?: string; number: string; email: string; token?: string; status: string; isVirtual: boolean; totalQuantity: number; shippingMethod?: string; carrier?: string; orderDate: string; returns: OrdersReturnPropsModel[]; discounts: { amount: MoneyProps; label: string }[]; coupons: { code: string; }[]; payments: { code: string; name: string; }[]; shipping?: { code: string; amount: number; currency: string }; shipments: ShipmentsModel[]; items: OrderItemModel[]; totalGiftCard: MoneyProps; grandTotal: MoneyProps; grandTotalExclTax: MoneyProps; totalShipping?: MoneyProps; subtotalExclTax: MoneyProps; subtotalInclTax: MoneyProps; totalTax: MoneyProps; shippingAddress: OrderAddressModel; totalGiftOptions: { giftWrappingForItems: MoneyProps; giftWrappingForItemsInclTax: MoneyProps; giftWrappingForOrder: MoneyProps; giftWrappingForOrderInclTax: MoneyProps; printedCard: MoneyProps; printedCardInclTax: MoneyProps; }; billingAddress: OrderAddressModel; availableActions: AvailableActionsProps[]; taxes: { amount: MoneyProps; rate: number; title: string }[]; appliedGiftCards: { code: string; appliedBalance: MoneyProps; }[]; }; ``` ### OrderItemProductModel ```ts type OrderItemProductModel = { onlyXLeftInStock?: number; priceRange?: { maximumPrice?: { regularPrice?: MoneyProps; }; }; uid: string; __typename: string; stockStatus?: string; canonicalUrl?: string; urlKey?: string; id: string; image?: string; imageAlt?: string; name: string; productType: string; sku: string; thumbnail: { url: string; label: string; }; giftWrappingAvailable?: boolean; }; ``` ### OrderItemModel ```ts type OrderItemModel = { giftMessage: { senderName: string; recipientName: string; message: string; }; giftWrappingPrice: MoneyProps; productGiftWrapping: { uid: string; design: string; selected: boolean; image: { url: string; label: string; }; price: MoneyProps; }[]; taxCalculations: { includeAndExcludeTax: { originalPrice: MoneyProps; baseOriginalPrice: MoneyProps; baseDiscountedPrice: MoneyProps; baseExcludingTax: MoneyProps; }; excludeTax: { originalPrice: MoneyProps; baseOriginalPrice: MoneyProps; baseDiscountedPrice: MoneyProps; baseExcludingTax: MoneyProps; }; includeTax: { singleItemPrice: MoneyProps; baseOriginalPrice: MoneyProps; baseDiscountedPrice: MoneyProps; }; }; productSalePrice: MoneyProps; status?: string; currentReturnOrderQuantity?: number; eligibleForReturn: boolean; productSku?: string; type?: string; discounted?: boolean; id: string; productName?: string; productUrlKey?: string; regularPrice?: MoneyProps; price: MoneyProps; product?: OrderItemProductModel; selectedOptions?: Array<{ label: string; value: any; }>; thumbnail?: { label: string; url: string; }; downloadableLinks: { count: number; result: string; } | null; prices: { priceIncludingTax: MoneyProps; originalPrice: MoneyProps; originalPriceIncludingTax: MoneyProps; price: MoneyProps; discounts: { label: string; amount: { value: number }; }[]; }; itemPrices: { priceIncludingTax: MoneyProps; originalPrice: MoneyProps; originalPriceIncludingTax: MoneyProps; price: MoneyProps; discounts: { label: string; amount: { value: number }; }[]; }; bundleOptions: Record | null; totalInclTax: MoneyProps; priceInclTax: MoneyProps; total: MoneyProps; configurableOptions: Record | undefined; giftCard?: { senderName: string; senderEmail: string; recipientEmail: string; recipientName: string; message: string; }; quantityCanceled: number; quantityInvoiced: number; quantityOrdered: number; quantityRefunded: number; quantityReturned: number; quantityShipped: number; requestQuantity?: number; totalQuantity: number; returnableQuantity?: number; quantityReturnRequested: number; }; ``` ### ReorderItemsProps The `ReorderItemsProps` object is returned by the following functions: [`reorderItems`](#reorderitems). ```ts interface ReorderItemsProps { success: boolean; userInputErrors: UserInputErrorProps[]; } ``` ### RequestReturnModel The `RequestReturnModel` object is returned by the following functions: [`requestReturn`](#requestreturn). ```ts interface RequestReturnModel { uid: string; number: string; status: string; createdAt: string; } ``` ### StoreConfigModel The `StoreConfigModel` object is returned by the following functions: [`getStoreConfig`](#getstoreconfig). ```ts interface StoreConfigModel { baseMediaUrl: string; orderCancellationEnabled: boolean; orderCancellationReasons: OrderCancellationReason[]; shoppingOrderDisplayPrice: OrderDisplayPriceProps; shoppingOrdersDisplayShipping: OrderDisplayPriceProps; shoppingOrdersDisplaySubtotal: OrderDisplayPriceProps; shoppingOrdersDisplayFullSummary: boolean; shoppingOrdersDisplayGrandTotal: boolean; shoppingOrdersDisplayZeroTax: boolean; salesPrintedCard: number; salesGiftWrapping: number; } ``` {/* This documentation is auto-generated from the drop-in source repository: REPO_URL */} --- # Order overview The order drop-in component provides a comprehensive set of tools and containers designed to manage and display order-related data across various pages and scenarios. It simplifies the implementation of order management functionality and supports seamless integration with both customer accounts and guest user workflows. > **Part of seller-assisted buying** The `OrderStatus` container labels orders placed by an administrator during an assisted session. This label belongs to the cross-drop-in [seller-assisted buying](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/user-account/containers/seller-assisted-buying/) feature. Read that overview first for the end-to-end flow and Admin setup. ## Architecture The order drop-in component consists of multiple containers that display order details on different pages, such as: - Order data containers display order details within the customer account, guest user areas, and on the order confirmation page. - Returned merchandise authorization (RMA) containers guide users through the return process and display a list of created return requests. - The `OrderSearch` container enables guest users to locate their orders using a combination of email, last name, and order number. This ensures easy access to order details even for users without an account. (Logged-in customers can also use this form.) The component's initialization process helps manage data retrieval and event emission, ensuring that containers receive the necessary data without individual fetching. This modular architecture allows for efficient, reusable, and highly customizable implementations of order and return workflows, making it ideal for both standard and advanced e-commerce use cases. In addition, the implementation of order and return details pages includes elements such as headers, which are implemented at the boilerplate level rather than provided directly by the drop-in containers. These elements, including `commerce-order-header` and `commerce-return-header` blocks, are covered as part of the overall framework for order and return details layouts, but are distinct from the drop-in container set. The following diagrams provide a visual composition of the order details and return details pages: ![Order details containers](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/order-details-containers.png) *Order details containers* ![Return details containers](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/return-details-containers.png) *Return details containers* ## Supported Commerce features The following table provides an overview of the Adobe Commerce order and return features that the order component supports: | Feature | Status | | ---------------------------------------------------------------- | ------------------------------------------ | | Cancel order (with email confirmation) | Supported | | Create a return | Supported | | Filter orders by time or purchase date | Supported | | Look up a specific order by email, last name, and order number (`OrderSearch`) | Supported | | Reorder | Supported | | Search orders (for example keyword search across order history) | Roadmap | | View list of orders on the account | Supported | | View order comments marked visible on storefront in Commerce Admin | Supported | | View order status | Supported | | View return status | Supported | > **Order lookup versus order search** The supported row for `OrderSearch` covers locating a known order when the shopper already has email, last name, and order number. The roadmap row covers richer search (for example, keyword search across the full order list in the account area). ## See also - [Seller-assisted buying](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/user-account/containers/seller-assisted-buying/) — extends this drop-in so the `OrderStatus` container labels orders placed by an administrator during an assisted session. --- # Order initialization The **Order initializer** configures how order data is managed and displayed, including order history, status tracking, and order details. Use initialization to customize order data structures and integrate order management features. Version: 4.0.1 ## Configuration options The following table describes the configuration options available for the **Order** initializer: | Parameter | Type | Req? | Description | |---|---|---|---| | `langDefinitions` | [`LangDefinitions`](#langdefinitions) | No | Language definitions for internationalization (i18n). Override dictionary keys for localization or branding. | | `models` | [`Record`](#models) | No | Custom data models for type transformations. Extend or modify default models with custom fields and transformers. | | `orderRef` | `string` | No | Pre-loads a specific order by its reference ID or order number. Useful for direct access to order details from email links or confirmation pages. | | `returnRef` | `string` | No | Pre-loads a specific return request by its reference ID. Enables direct navigation to return request details and status tracking. | | `orderData` | [`OrderDataModel`](#orderdatamodel) \| null | No | Injects initial order data on page load. Useful for server-side rendering or hydrating order details without an additional \`GraphQL\` request. | | `routeOrdersList` | `() => string` | No | | ## Default configuration The initializer runs with these defaults when no configuration is provided: ```javascript title="scripts/initializers/order.js" // All configuration options are optional await initializers.mountImmediately(initialize, { langDefinitions: {}, // Uses built-in English strings models: {}, // Uses default data models // Drop-in-specific defaults: // orderRef: undefined // See configuration options below // returnRef: undefined // See configuration options below // orderData: undefined // See configuration options below // routeOrdersList: undefined // See configuration options below }); ``` ## Language definitions Override dictionary keys for localization or branding. The `langDefinitions` object maps locale keys to custom strings that override default text for the drop-in. ```javascript title="scripts/initializers/order.js" const customStrings = { 'AddToCart': 'Add to Bag', 'Checkout': 'Complete Purchase', 'Price': 'Cost', }; const langDefinitions = { default: customStrings, }; await initializers.mountImmediately(initialize, { langDefinitions }); ``` > For complete dictionary customization including all available keys and multi-language support, see the [Order Dictionary](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/dictionary/) page. ## Customizing data models Extend or transform data models by providing custom transformer functions. Use the `models` option to add custom fields or modify existing data structures returned from the backend. ### Available models The following models can be customized through the `models` configuration option: | Model | Description | |---|---| | [`OrderDataModel`](#orderdatamodel) | Transforms order data from `GraphQL` including order details, items, shipping, billing, payment, and tracking information. Use this to add custom fields specific to your order management workflow. | | [`CustomerOrdersReturnModel`](#customerordersreturnmodel) | Transforms `CustomerOrdersReturnModel` data from `GraphQL`. | | [`RequestReturnModel`](#requestreturnmodel) | Transforms `RequestReturnModel` data from `GraphQL`. | The following example shows how to customize the `OrderDataModel` model for the **Order** drop-in: ```javascript title="scripts/initializers/order.js" const models = { OrderDataModel: { transformer: (data) => ({ // Add custom fields from backend data customField: data?.custom_field, promotionBadge: data?.promotion?.label, // Transform existing fields displayPrice: data?.price?.value ? `${data.price.value}` : 'N/A', }), }, }; await initializers.mountImmediately(initialize, { models }); ``` ## Drop-in configuration The **Order initializer** configures how order data is managed and displayed, including order history, status tracking, and order details. Use initialization to customize order data structures and integrate order management features. ```javascript title="scripts/initializers/order.js" await initializers.mountImmediately(initialize, { langDefinitions: {}, models: {}, orderRef: 'abc123', returnRef: 'abc123', orderData: {}, routeOrdersList: 'value', }); ``` > Refer to the [Configuration options](#configuration-options) table for detailed descriptions of each option. ## Configuration types The following TypeScript definitions show the structure of each configuration object: ### langDefinitions Maps locale identifiers to dictionaries of key-value pairs. The `default` locale is used as the fallback when no specific locale matches. Each dictionary key corresponds to a text string used in the drop-in UI. ```typescript langDefinitions?: { [locale: string]: { [key: string]: string; }; }; ``` ### models Maps model names to transformer functions. Each transformer receives data from GraphQL and returns a modified or extended version. Use the `Model` type from `@dropins/tools` to create type-safe transformers. ```typescript models?: { [modelName: string]: Model; }; ``` ## Model definitions The following TypeScript definitions show the structure of each customizable model: ### OrderDataModel ```typescript export type OrderDataModel = { giftReceiptIncluded: boolean; printedCardIncluded: boolean; giftWrappingOrder: { price: MoneyProps; uid: string; }; placeholderImage?: string; returnNumber?: string; id: string; orderStatusChangeDate?: string; number: string; email: string; token?: string; status: string; isVirtual: boolean; totalQuantity: number; shippingMethod?: string; carrier?: string; orderDate: string; returns: OrdersReturnPropsModel[]; discounts: { amount: MoneyProps; label: string }[]; coupons: { code: string; }[]; payments: { code: string; name: string; }[]; shipping?: { code: string; amount: number; currency: string }; shipments: ShipmentsModel[]; items: OrderItemModel[]; totalGiftCard: MoneyProps; grandTotal: MoneyProps; grandTotalExclTax: MoneyProps; totalShipping?: MoneyProps; subtotalExclTax: MoneyProps; subtotalInclTax: MoneyProps; totalTax: MoneyProps; shippingAddress: OrderAddressModel; totalGiftOptions: { giftWrappingForItems: MoneyProps; giftWrappingForItemsInclTax: MoneyProps; giftWrappingForOrder: MoneyProps; giftWrappingForOrderInclTax: MoneyProps; printedCard: MoneyProps; printedCardInclTax: MoneyProps; }; billingAddress: OrderAddressModel; availableActions: AvailableActionsProps[]; taxes: { amount: MoneyProps; rate: number; title: string }[]; appliedGiftCards: { code: string; appliedBalance: MoneyProps; }[]; }; ``` ### OrderItemProductModel ```typescript export type OrderItemProductModel = { onlyXLeftInStock?: number; priceRange?: { maximumPrice?: { regularPrice?: MoneyProps; }; }; uid: string; __typename: string; stockStatus?: string; canonicalUrl?: string; urlKey?: string; id: string; image?: string; imageAlt?: string; name: string; productType: string; sku: string; thumbnail: { url: string; label: string; }; giftWrappingAvailable?: boolean; }; ``` ### OrderItemModel ```typescript export type OrderItemModel = { giftMessage: { senderName: string; recipientName: string; message: string; }; giftWrappingPrice: MoneyProps; productGiftWrapping: { uid: string; design: string; selected: boolean; image: { url: string; label: string; }; price: MoneyProps; }[]; taxCalculations: { includeAndExcludeTax: { originalPrice: MoneyProps; baseOriginalPrice: MoneyProps; baseDiscountedPrice: MoneyProps; baseExcludingTax: MoneyProps; }; excludeTax: { originalPrice: MoneyProps; baseOriginalPrice: MoneyProps; baseDiscountedPrice: MoneyProps; baseExcludingTax: MoneyProps; }; includeTax: { singleItemPrice: MoneyProps; baseOriginalPrice: MoneyProps; baseDiscountedPrice: MoneyProps; }; }; productSalePrice: MoneyProps; status?: string; currentReturnOrderQuantity?: number; eligibleForReturn: boolean; productSku?: string; type?: string; discounted?: boolean; id: string; productName?: string; productUrlKey?: string; regularPrice?: MoneyProps; price: MoneyProps; product?: OrderItemProductModel; selectedOptions?: Array<{ label: string; value: any; }>; thumbnail?: { label: string; url: string; }; downloadableLinks: { count: number; result: string; } | null; prices: { priceIncludingTax: MoneyProps; originalPrice: MoneyProps; originalPriceIncludingTax: MoneyProps; price: MoneyProps; discounts: { label: string; amount: { value: number }; }[]; }; itemPrices: { priceIncludingTax: MoneyProps; originalPrice: MoneyProps; originalPriceIncludingTax: MoneyProps; price: MoneyProps; discounts: { label: string; amount: { value: number }; }[]; }; bundleOptions: Record | null; totalInclTax: MoneyProps; priceInclTax: MoneyProps; total: MoneyProps; configurableOptions: Record | undefined; giftCard?: { senderName: string; senderEmail: string; recipientEmail: string; recipientName: string; message: string; }; quantityCanceled: number; quantityInvoiced: number; quantityOrdered: number; quantityRefunded: number; quantityReturned: number; quantityShipped: number; requestQuantity?: number; totalQuantity: number; returnableQuantity?: number; quantityReturnRequested: number; }; ``` ### CustomerOrdersReturnModel ```typescript export interface CustomerOrdersReturnModel { ordersReturn: OrdersReturnPropsModel[]; pageInfo?: PageInfoProps; } ``` ### RequestReturnModel ```typescript export interface RequestReturnModel { uid: string; number: string; status: string; createdAt: string; } ``` --- # Order Quick Start The Order drop-in provides a complete order management experience for customers. It includes containers for viewing order details, tracking shipments, managing returns, and searching order history. Version: 4.0.0 ## Quick example The Order drop-in is included in the https://github.com/hlxsites/aem-boilerplate-commerce. This example shows the basic pattern: ```js // 1. Import initializer (handles all setup) // 2. Import the container you need // 3. Import the provider // 4. Render in your block export default async function decorate(block) { await provider.render(CreateReturn, { // Configuration options - see Containers page })(block); } ``` **New to drop-ins?** See the [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/) guide for complete step-by-step instructions. ## Quick reference **Import paths:** - Initializer: `import '../../scripts/initializers/order.js'` - Containers: `import ContainerName from '@dropins/storefront-order/containers/ContainerName.js'` - Provider: `import { render } from '@dropins/storefront-order/render.js'` **Package:** `@dropins/storefront-order` **Version:** 4.0.0 (verify compatibility with your Commerce instance) **Example container:** `CreateReturn` ## Learn more - [Containers](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/containers/) - Available UI components and configuration options - [Initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/initialization/) - Customize initializer settings and data models - [Functions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/functions/) - Control drop-in behavior programmatically - [Events](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/events/) - Listen to and respond to drop-in state changes - [Slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/order/slots/) - Extend containers with custom content --- # Order Slots The Order drop-in exposes slots for customizing specific UI sections. Use slots to replace or extend container components. For default properties available to all slots, see [Extending drop-in components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/extending/). Version: 4.0.0 | Container | Slots | |-----------|-------| | [`CreateReturn`](#createreturn-slots) | `Footer`, `ReturnOrderItem`, `ReturnFormActions`, `ReturnReasonFormImage`, `CartSummaryItemImage` | | [`CustomerDetails`](#customerdetails-slots) | `OrderReturnInformation`, `PaymentMethodIcon` | | [`OrderProductList`](#orderproductlist-slots) | `Footer`, `CartSummaryItemImage` | | [`OrderReturns`](#orderreturns-slots) | `ReturnItemsDetails`, `DetailsActionParams`, `ReturnListImage` | | [`OrderStatus`](#orderstatus-slots) | `OrderActions` | | [`ReturnsList`](#returnslist-slots) | `ReturnItemsDetails`, `DetailsActionParams`, `ReturnListImage` | | [`ShippingStatus`](#shippingstatus-slots) | `DeliveryTimeLine`, `DeliveryTrackActions`, `ReturnItemsDetails`, `ShippingStatusCardImage`, `NotYetShippedProductImage`, `ShippingStatusReturnCardImage` | > **Slot usage best practice** Do not use context methods inside other context methods (for example, `appendChild()` inside `onChange()`). See [Slots best practices](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/slots/#best-practice-for-dynamic-slot-content) for details and examples. ## CreateReturn slots The slots for the `CreateReturn` container allow you to customize its appearance and behavior. ```typescript interface CreateReturnProps { slots?: { Footer: SlotProps; ReturnOrderItem: SlotProps; ReturnFormActions: SlotProps<{ handleChangeStep: (value: StepsTypes) => void; }>; ReturnReasonFormImage?: SlotProps<{ data: OrderItemModel; defaultImageProps: ImageProps; }>; CartSummaryItemImage?: SlotProps<{ data: OrderItemModel; defaultImageProps: ImageProps; }>; }; } ``` ### Footer slot The Footer slot allows you to customize the footer section of the `CreateReturn` container. #### Example ```js await provider.render(CreateReturn, { slots: { Footer: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Footer'; ctx.appendChild(element); } } })(block); ``` ### ReturnOrderItem slot The `ReturnOrderItem` slot allows you to customize the return order item section of the `CreateReturn` container. #### Example ```js await provider.render(CreateReturn, { slots: { ReturnOrderItem: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ReturnOrderItem'; ctx.appendChild(element); } } })(block); ``` ### ReturnReasonFormImage slot The `ReturnReasonFormImage` slot allows you to customize the return reason form image section of the `CreateReturn` container. #### Example ```js await provider.render(CreateReturn, { slots: { ReturnReasonFormImage: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ReturnReasonFormImage'; ctx.appendChild(element); } } })(block); ``` ### CartSummaryItemImage slot The `CartSummaryItemImage` slot allows you to customize the cart summary item image section of the `CreateReturn` container. #### Example ```js await provider.render(CreateReturn, { slots: { CartSummaryItemImage: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom CartSummaryItemImage'; ctx.appendChild(element); } } })(block); ``` ## CustomerDetails slots The slots for the `CustomerDetails` container allow you to customize its appearance and behavior. ```typescript interface CustomerDetailsProps { slots?: { OrderReturnInformation: SlotProps; PaymentMethodIcon: SlotProps>; }; } ``` ### OrderReturnInformation slot The `OrderReturnInformation` slot allows you to customize the order return information section of the `CustomerDetails` container. #### Example ```js await provider.render(CustomerDetails, { slots: { OrderReturnInformation: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom OrderReturnInformation'; ctx.appendChild(element); } } })(block); ``` ## OrderProductList slots The slots for the `OrderProductList` container allow you to customize its appearance and behavior. ```typescript interface OrderProductListProps { slots?: { Footer: SlotProps; CartSummaryItemImage?: SlotProps<{ data: OrderItemModel; defaultImageProps: ImageProps; }>; }; } ``` ### Footer slot The Footer slot allows you to customize the footer section of the `OrderProductList` container. #### Example ```js await provider.render(OrderProductList, { slots: { Footer: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom Footer'; ctx.appendChild(element); } } })(block); ``` ### CartSummaryItemImage slot The `CartSummaryItemImage` slot allows you to customize the cart summary item image section of the `OrderProductList` container. #### Example ```js await provider.render(OrderProductList, { slots: { CartSummaryItemImage: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom CartSummaryItemImage'; ctx.appendChild(element); } } })(block); ``` ## OrderReturns slots The slots for the `OrderReturns` container allow you to customize its appearance and behavior. ```typescript interface OrderReturnsProps { slots?: { ReturnItemsDetails?: SlotProps<{ items: OrdersReturnItemsPropsModel[]; }>; DetailsActionParams?: SlotProps<{ returnOrderItem: OrdersReturnPropsModel; }>; ReturnListImage?: SlotProps<{ data: OrdersReturnItemsPropsModel; defaultImageProps: ImageProps; }>; }; } ``` ### ReturnItemsDetails slot The `ReturnItemsDetails` slot allows you to customize the return items details section of the `OrderReturns` container. #### Example ```js await provider.render(OrderReturns, { slots: { ReturnItemsDetails: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ReturnItemsDetails'; ctx.appendChild(element); } } })(block); ``` ### DetailsActionParams slot The `DetailsActionParams` slot allows you to customize the return action parameters section of the `OrderReturns` container. #### Example ```js await provider.render(OrderReturns, { slots: { DetailsActionParams: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom DetailsActionParams'; ctx.appendChild(element); } } })(block); ``` ### ReturnListImage slot The `ReturnListImage` slot allows you to customize the return list image section of the `OrderReturns` container. #### Example ```js await provider.render(OrderReturns, { slots: { ReturnListImage: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ReturnListImage'; ctx.appendChild(element); } } })(block); ``` ## OrderStatus slots The slots for the `OrderStatus` container allow you to customize its appearance and behavior. ```typescript interface OrderStatusProps { slots?: { OrderActions: SlotProps; }; } ``` ### OrderActions slot The `OrderActions` slot allows you to customize the order actions section of the `OrderStatus` container. #### Example ```js await provider.render(OrderStatus, { slots: { OrderActions: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom OrderActions'; ctx.appendChild(element); } } })(block); ``` ## ReturnsList slots The slots for the `ReturnsList` container allow you to customize its appearance and behavior. ```typescript interface ReturnsListProps { slots?: { ReturnItemsDetails?: SlotProps<{ items: OrdersReturnItemsPropsModel[]; }>; DetailsActionParams?: SlotProps<{ returnOrderItem: OrdersReturnPropsModel; }>; ReturnListImage?: SlotProps<{ data: OrdersReturnItemsPropsModel; defaultImageProps: ImageProps; }>; }; } ``` ### ReturnItemsDetails slot The `ReturnItemsDetails` slot allows you to customize the return items details section of the `ReturnsList` container. #### Example ```js await provider.render(ReturnsList, { slots: { ReturnItemsDetails: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ReturnItemsDetails'; ctx.appendChild(element); } } })(block); ``` ### DetailsActionParams slot The `DetailsActionParams` slot allows you to customize the details action params section of the `ReturnsList` container. #### Example ```js await provider.render(ReturnsList, { slots: { DetailsActionParams: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom DetailsActionParams'; ctx.appendChild(element); } } })(block); ``` ### ReturnListImage slot The `ReturnListImage` slot allows you to customize the return list image section of the `ReturnsList` container. #### Example ```js await provider.render(ReturnsList, { slots: { ReturnListImage: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ReturnListImage'; ctx.appendChild(element); } } })(block); ``` ## ShippingStatus slots The slots for the `ShippingStatus` container allow you to customize its appearance and behavior. ```typescript interface ShippingStatusProps { slots?: { DeliveryTimeLine?: SlotProps; DeliveryTrackActions?: SlotProps; ReturnItemsDetails?: SlotProps; ShippingStatusCardImage?: SlotProps<{ data: ShipmentItemsModel; defaultImageProps: ImageProps; }>; NotYetShippedProductImage?: SlotProps<{ data: OrderItemModel; defaultImageProps: ImageProps; }>; ShippingStatusReturnCardImage?: SlotProps<{ data: OrdersReturnItemsPropsModel; defaultImageProps: ImageProps; }>; }; } ``` ### DeliveryTimeLine slot The `DeliveryTimeLine` slot allows you to customize the delivery time line section of the `ShippingStatus` container. #### Example ```js await provider.render(ShippingStatus, { slots: { DeliveryTimeLine: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom DeliveryTimeLine'; ctx.appendChild(element); } } })(block); ``` ### DeliveryTrackActions slot The `DeliveryTrackActions` slot allows you to customize the delivery track actions section of the `ShippingStatus` container. #### Example ```js await provider.render(ShippingStatus, { slots: { DeliveryTrackActions: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom DeliveryTrackActions'; ctx.appendChild(element); } } })(block); ``` ### ReturnItemsDetails slot The `ReturnItemsDetails` slot allows you to customize the return items details section of the `ShippingStatus` container. #### Example ```js await provider.render(ShippingStatus, { slots: { ReturnItemsDetails: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ReturnItemsDetails'; ctx.appendChild(element); } } })(block); ``` ### ShippingStatusCardImage slot The `ShippingStatusCardImage` slot allows you to customize the shipping status card image section of the `ShippingStatus` container. #### Example ```js await provider.render(ShippingStatus, { slots: { ShippingStatusCardImage: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ShippingStatusCardImage'; ctx.appendChild(element); } } })(block); ``` ### NotYetShippedProductImage slot The `NotYetShippedProductImage` slot allows you to customize the not yet shipped product image section of the `ShippingStatus` container. #### Example ```js await provider.render(ShippingStatus, { slots: { NotYetShippedProductImage: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom NotYetShippedProductImage'; ctx.appendChild(element); } } })(block); ``` ### ShippingStatusReturnCardImage slot The `ShippingStatusReturnCardImage` slot allows you to customize the shipping status return card image section of the `ShippingStatus` container. #### Example ```js await provider.render(ShippingStatus, { slots: { ShippingStatusReturnCardImage: (ctx) => { // Your custom implementation const element = document.createElement('div'); element.innerText = 'Custom ShippingStatusReturnCardImage'; ctx.appendChild(element); } } })(block); ``` --- # Order styles Customize the Order drop-in using CSS classes and design tokens. This page covers the Order-specific container classes and customization examples. For comprehensive information about design tokens, responsive breakpoints, and styling best practices, see [Styling Drop-In Components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/). Version: 4.0.0 ## Customization example Add this to the CSS file of the specific https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/ where you're using the Order drop-in. For a complete list of available design tokens (colors, spacing, typography, and more), see the [Design tokens reference](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/#design-tokens-reference). ```css title="styles/styles.css" del={2-3} ins={4-5} .order-order-actions__wrapper { gap: 0 var(--spacing-small); margin-bottom: var(--spacing-small); gap: 0 var(--spacing-medium); margin-bottom: var(--spacing-medium); } ``` ## Container classes The Order drop-in uses BEM-style class naming. Use the browser DevTools to inspect elements and find specific class names. ```css /* CustomerDetailsContent */ .dropin-card__content {} .order-customer-details-content {} .order-customer-details-content__container {} .order-customer-details-content__container--no-margin {} .order-customer-details-content__container-billing_address {} .order-customer-details-content__container-billing_address--fullwidth {} .order-customer-details-content__container-description {} .order-customer-details-content__container-email {} .order-customer-details-content__container-payment_methods {} .order-customer-details-content__container-payment_methods--fullwidth {} .order-customer-details-content__container-payment_methods--icon {} .order-customer-details-content__container-return-information {} .order-customer-details-content__container-shipping_address {} .order-customer-details-content__container-shipping_methods {} .order-customer-details-content__container-title {} /* EmptyList */ .dropin-card {} .dropin-card__content {} .order-empty-list {} .order-empty-list--empty-box {} .order-empty-list--minified {} /* OrderActions */ .order-order-actions__wrapper {} .order-order-actions__wrapper--empty {} /* OrderCancel */ .dropin-modal__body--medium {} .dropin-modal__header {} .dropin-modal__header-close-button {} .dropin-modal__header-title {} .order-order-cancel__button-container {} .order-order-cancel__modal {} .order-order-cancel__text {} .order-order-cancel__title {} /* OrderComments */ .order-order-comments-container {} .order-order-comments-card .dropin-card__content {} .order-order-comments-card__container {} .order-order-comments {} .order-order-comments--empty {} .order-order-comments__empty-state {} .order-order-comments__item {} .order-order-comments__header {} .order-order-comments__date {} .order-order-comments__text {} /* OrderCostSummaryContent */ .dropin-accordion-section {} .dropin-accordion-section__content-container {} .dropin-card__content {} .dropin-price {} .order-cost-summary-content {} .order-cost-summary-content__accordion {} .order-cost-summary-content__accordion-row {} .order-cost-summary-content__accordion-total {} .order-cost-summary-content__description {} .order-cost-summary-content__description--discount {} .order-cost-summary-content__description--gift-wrapping {} .order-cost-summary-content__description--header {} .order-cost-summary-content__description--printed-card {} .order-cost-summary-content__description--shipping {} .order-cost-summary-content__description--subheader {} .order-cost-summary-content__description--subtotal {} .order-cost-summary-content__description--total {} .order-cost-summary-content__description--total-free {} /* OrderHeader */ .order-header {} .order-header-create-account {} .order-header-create-account__button {} .order-header-create-account__message {} .order-header__icon {} .order-header__order {} .order-header__title {} .success-icon {} /* OrderLoaders */ .order-order-loaders--card-loader {} /* OrderProductListContent */ .cart-summary-item__title--strikethrough {} .dropin-card__content {} .dropin-cart-item__alert {} .order-confirmation-cart-summary-item {} .order-order-product-list-content {} .order-order-product-list-content__items {} /* OrderSearchForm */ .dropin-card__content {} .order-order-search-form {} .order-order-search-form__button-container {} .order-order-search-form__title {} .order-order-search-form__wrapper {} .order-order-search-form__wrapper__item--email {} .order-order-search-form__wrapper__item--lastname {} .order-order-search-form__wrapper__item--number {} /* OrderStatusContent */ .dropin-card__content {} .order-order-status-content {} .order-order-status-content__wrapper {} .order-order-status-content__wrapper-description {} .order-order-status-content__wrapper-description--actions-slot {} /* ReturnOrderMessage */ .order-return-order-message {} .order-return-order-message__subtitle {} .order-return-order-message__title {} /* ReturnOrderProductList */ .cart-summary-item__title--strikethrough {} .dropin-cart-item__alert {} .dropin-cart-item__footer {} .dropin-incrementer {} .dropin-incrementer--medium {} .dropin-incrementer__button-container {} .order-create-return {} .order-create-return_notification {} .order-return-order-product-list {} .order-return-order-product-list__item {} .order-return-order-product-list__item--blur {} /* ReturnReasonForm */ .dropin-cart-item {} .dropin-field {} .order-return-reason-form {} .order-return-reason-form__actions {} /* ReturnsListContent */ .dropin-accordion-section__content-container {} .dropin-card__content {} .dropin-content-grid {} .dropin-content-grid__content {} .dropin-divider {} .dropin-divider--secondary {} .order-returns-list-content {} .order-returns-list-content__actions {} .order-returns-list-content__card {} .order-returns-list-content__card-wrapper {} .order-returns-list-content__cards-grid {} .order-returns-list-content__cards-list {} .order-returns-list-content__descriptions {} .order-returns-list-content__images {} .order-returns-list-content__images-3 {} .order-returns-list-content__return-status {} .order-returns__header--full-size {} .order-returns__header--minified {} /* ShippingStatusCard */ .dropin-accordion-section__content-container {} .dropin-card__content {} .dropin-content-grid {} .dropin-content-grid__content {} .dropin-divider {} .dropin-divider--secondary {} .order-shipping-status-card {} .order-shipping-status-card--count-stepper {} .order-shipping-status-card--return-order {} .order-shipping-status-card__header {} .order-shipping-status-card__header--content {} .order-shipping-status-card__images {} /* OrderCancelForm */ .order-order-cancel-reasons-form__button-container {} .order-order-cancel-reasons-form__text {} ``` --- # Order Cancellation The order drop-in component enables both logged-in users and guest users to cancel an order. ## Big picture The order cancellation workflow is as follows: 1. The shopper selects an order to cancel. Guest users must use the order search form to locate the order. Logged-in customers can select an order from their order history or use the search form. 1. The shopper submits the cancellation form after selecting a cancellation reason. 1. If the shopper is a logged-in customer, the order is `Canceled` immediately. Otherwise, the order status remains `Pending` until the guest clicks a link in a confirmation email. ## Prerequisites Adobe Commerce must be configured to allow order cancellations. In the Admin, go to **Stores** > Configuration > **Sales** > **Sales** > **Order Cancellation** and set the following options: ```text [ ['Configuration', 'Description'], ['Order cancellation through GraphQL', 'Set to Yes to enable order cancellations.'], [ 'Order cancellation reasons', 'A list of reasons that the shopper can choose from to explain why they want to cancel the order. You can customize the default options to provide a list that is applicable to your business.', ], ] ``` ![Order cancellation configuration options](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/cancellation-prerequisites.png) *Order cancellation configuration options* ## Step-by-step The following steps describe how to implement the order cancellation workflow for both logged in customers and guests. ### 1. Display the order history (logged-in customers only) The workflow for logged-in customers is straightforward. An order can be canceled only if it has a status of `Received`, `Pending`, or `Processing`. > The logged-in customer can also use the order search form to locate the order. See [Search for the order](#search-for-the-order) for this use case. The customer selects an active order from their order history to cancel. ![Order history](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/cancellation-order-history.png) *Order history* The order history page uses the `OrderProductListContent` component to render the list of orders that the customer previously placed. It iterates over the list of orders and uses the `CartSummaryItem` component to render each order item. The order item is an instance of the `OrderItemModel`, which contains all the necessary properties of an order item. The following example shows an implementation of the `OrderProductListContent` component: ```jsx ...
    {item.list?.map((product: OrderItemModel) => (
  • ))} ... ``` ### 2. Search for the order A guest user does not have access to the order history page. Therefore, the only way to access an order is by using the Order Search form. The search order form retrieves the order that matches the specified email address, last name, and order name, as shown below: ![Order cancellation form](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/cancellation-form.png) *Order cancellation form* The `OrderSearchForm` component is responsible for rendering the form to search for an order. It receives the following parameters: ```text [ ['Configuration', 'Description'], ['fieldsConfig', 'An array of fields to be rendered in the form.'], ['loading', 'A boolean to indicate whether the form is loading.'], [ 'inLineAlert', 'After submitting the form, an inline alert is shown with the result of the operation (success, warning, error) with its respective message.', ], [ 'onSubmit', 'A function to be called when the form is submitted and a flag to indicate if the form is valid.', ], ] ``` See the following code for an example of the implementation: ```jsx ... {inLineAlert.text ? ( } /> ) : null}
    ... ``` ### 3. Render and submit the cancellation form The cancellation form allows the customer to select a cancellation reason and submit the form. ![Order cancellation form](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/cancellation-form.png) *Order cancellation form* The `OrderCancel` component renders the `OrderCancelForm` container inside a modal. This modal receives two parameters: ```text [ ['Parameter', 'Description'], ['orderRef', 'Identifies the order.'], ['cancelReasons', 'Contains all the configured cancellation reasons.'], ] ``` For example: ```jsx ... } data-testid="order-cancellation-reasons-modal" > ... ``` The `OrderCancelForm` component is responsible for: - Rendering the form with multiple cancellation reasons and a submission button. - Handling the form submission. - Showing the appropriate error messages if a failure occurs after submitting the form. ```jsx ... {isErrorVisible && ( )} ... ``` ### 4. Display the confirmation notice When a logged-in customer submits the cancellation form, Commerce immediately sets the status of the order to `Canceled` and the drop-in displays a confirmation notice. ![Confirmation for logged-in users](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/cancellation-performed.png) *Confirmation for logged-in users.* If the shopper is a guest user, the drop-in displays the following dialog: ![Cancellation has been requested for a guest user](https://experienceleague.adobe.com/developer/commerce/storefront/images/dropins/order/cancellation-requested.png) *Guest user is notified that cancellation has been requested.* The order status remains `Pending` until the shopper clicks the link in an email similar to the following to confirm the cancellation. > **Confirm Your Main Website Store Order Cancellation** It seems that you'd like to cancel your order #000000001. If this is correct, please _click here_ to confirm your cancellation request. If you have questions about your order, you can email us at _support@example.com_. Once clicked, the order becomes `Canceled` and the order status page is updated accordingly. The order status page uses the `OrderStatusContent` component to render the order with all its properties and the available actions. See the following code for an example of the `OrderStatusContent` implementation below: ```jsx ...
    {isReturnPage ? returnMessage : orderMessage} ... ``` After performing any action, the order status page is re-rendered, so that it reflects the new status and the available actions. The appropriate message is shown to the user, depending on the action performed. --- # ApplePay Container The `ApplePay` container renders a checkout button that enables macOS and iOS users to pay using https://www.apple.com/apple-pay/. Version: 4.1.0 ## Configuration The `ApplePay` container provides the following configuration options: | Option | Type | Req? | Description | |--------|------|------|-------------| | `location` | string | Yes | Location where Apple Pay is rendered. Must be either `CHECKOUT` or `PRODUCT_DETAIL`. | | `getCartId` | function | Maybe | Required if `createCart` is not provided. Returns a promise that resolves to the shopper's cart ID. | | `createCart` | object | Maybe | Required if `getCartId` is not provided. Provides cart items when `getCartId` is not used. Must be an object with a `getCartItems` function that returns at least one cart item. Each item must include at least a `sku` (string) and `quantity` (number). See "Cart item object" for details. | | `onButtonClick` | function | No | Called when the shopper clicks the Apple Pay button. Receives a `showPaymentSheet` function, which must be called synchronously to start the Apple Pay session and display the payment sheet. If not provided, clicking the button automatically triggers the payment sheet. | | `onSuccess` | function | No | Called when the payment completes successfully. Receives `{ cartId: string }`. If the function returns a promise, it is awaited before marking the payment as successful in the Apple Pay sheet. If the promise rejects, the payment is marked as failed and the error is passed to `onError` (if provided). | | `onError` | function | No | Called when the payment flow fails or is aborted. Receives `{ name: string, message: string }`, containing localized, user-facing error details. These values can be translated using `PaymentServices.ApplePay.errors` [language definitions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/payment-services/dictionary/). | | `hidden` | boolean | No | Whether the button is hidden. Set to `true` to hide the Apple Pay button. Default: `false`. | | `disabled` | boolean | No | Whether the button is disabled. Set to `true` to disable the Apple Pay button. Default: `false`. | ### Cart item object The cart items that `getCartItems` returns should be objects with the following properties: | Property | Type | Req? | Description | |----------|------|------|-------------| | `sku` | string | Yes | The product SKU. | | `quantity` | number | Yes | The quantity of the product. | | `parentSku` | string | No | The parent product SKU. | | `selectedOptions` | `(string \| number)[]` | No | Selected product options. | | `enteredOptions` | `{ uid: string \| number; value: string }[]` | No | Entered product options. | ## Slots This container does not expose any customizable slots. ## Usage ### Checkout page example The following example demonstrates how to use the `ApplePay` container on the checkout page. ```js const cart = events.lastPayload('checkout/initialized'); if (cart) { const $content = document.createElement('div'); PaymentServices.render(ApplePay, { location: PaymentLocation.CHECKOUT, getCartId: async () => cart.id, onSuccess: () => orderApi.placeOrder(cart.id), onError: (error) => { console.error(error) }, })($content); } ``` ### Product details page example The following example demonstrates how to use the `ApplePay` container on a product details page. ```js const product = events.lastPayload('pdp/values'); if (product) { const $content = document.createElement('div'); PaymentServices.render(ApplePay, { location: PaymentLocation.PRODUCT_DETAIL, createCart: { getCartItems: () => [{ sku: product.sku, quantity: 1, }], }, onSuccess: ({cartId}) => orderApi.placeOrder(cartId), onError: (error) => { console.error(error) }, })($content); } ``` --- # CreditCard Container The `CreditCard` container renders a form for entering credit card details during checkout. Version: 4.1.0 ## Configuration The `CreditCard` container provides the following configuration options: | Option | Type | Req? | Description | |--------|------|------|-------------| | `getCartId` | function | Yes | Returns a promise that resolves to the shopper’s cart ID. | | `creditCardFormRef` | object | Yes | Reference to the credit card form. Initially, `{ current: null }` should be passed. After rendering, the container sets `current` to the `validate: () => boolean` and `submit: () => Promise` object, which parent containers can use to programmatically validate and submit the form. Any error during the payment flow propagates to `onError` and causes the promise returned by `submit()` to be rejected.| | `onSuccess` | function | No | Called when the payment completes successfully. Receives `{ cartId: string }`. | | `onError` | function | No | Called when the payment flow fails or is aborted. Receives `{ name: string, message: string }`, containing localized, user-facing error details. These values can be translated using `PaymentServices.CreditCard.errors` [language definitions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/payment-services/dictionary/). | ## Slots This container exposes no customizable slots. ## Usage The following example demonstrates how to render and submit a credit card form: ```js const $content = document.createElement('div'); const creditCardFormRef = { current: null }; const placeOrderButton = document.getElementById('place-order'); PaymentServices.render(CreditCard, { getCartId: async () => 'ozGi7uLI74etDYyMijoI2cla5CmGIBch', creditCardFormRef: creditCardFormRef, })($content); placeOrderButton.onclick = () => { if (creditCardFormRef.current) { if (creditCardFormRef.current.validate()) { const future = creditCardFormRef.current.submit() future.catch(console.error) } } } ``` --- # GooglePay Container The `GooglePay` container renders a checkout button that enables shoppers to pay using https://pay.google.com/about/. Version: 4.1.0 ## Configuration The `GooglePay` container provides the following configuration options: | Option | Type | Req? | Description | |--------|------|------|-------------| | `onButtonClick` | function | No | Called when the shopper clicks the Google Pay button. Receives a `showPaymentSheet` function, which must be called synchronously to begin the Google Pay checkout and show the payment sheet. If not provided, clicking the button automatically triggers the payment sheet. | | `onSuccess` | function | No | Called when the payment completes successfully. Receives `{ cartId: string }`. If the function returns a promise, it is awaited before marking the payment as successful. If the promise rejects, the payment is marked as failed and the error is passed to `onError` (if provided). | | `onError` | function | No | Called when the payment flow fails or is aborted. Receives `{ name: string, message: string }`, containing localized, user-facing error details. These values can be translated using `PaymentServices.GooglePay.errors` [language definitions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/payment-services/dictionary/). | | `hidden` | boolean | No | Whether the button is hidden. Set to `true` to hide the Google Pay button. Default: `false`. | | `disabled` | boolean | No | Whether the button is disabled. Set to `true` to disable the Google Pay button. Default: `false`. | ## Slots This container does not expose any customizable slots. ## Usage ### Checkout page example The following example demonstrates how to use the `GooglePay` container on the checkout page. ```js const $content = document.createElement('div'); PaymentServices.render(GooglePay, { onSuccess: ({ cartId }) => orderApi.placeOrder(cartId), onError: (error) => { console.error(error) }, })($content); ``` --- # Payment Services Containers The **Payment Services** drop-in provides pre-built container components for integrating into your storefront. Version: 4.1.0 ## What are Containers? Containers are pre-built UI components that combine functionality, state management, and presentation. They provide a complete solution for specific features and can be customized through props, slots, and CSS. ## Available Containers | Container | Description | | --------- | ----------- | | [ApplePay](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/payment-services/containers/apple-pay/) | The `ApplePay` container renders a checkout button that enables macOS and iOS users to pay using Apple Pay. | | [CreditCard](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/payment-services/containers/credit-card/) | The `CreditCard` container renders a form for shoppers to enter credit card details during checkout. | | [GooglePay](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/payment-services/containers/google-pay/) | The `GooglePay` container renders a checkout button that enables shoppers to pay using Google Pay. | | [PayPalButtons](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/payment-services/containers/paypal-buttons/) | The `PayPalButtons` container renders a set of checkout buttons that enables shoppers to pay using PayPal, Venmo, Pay Later, or a debit/credit card. | > Each container is designed to work independently but can be composed together to create comprehensive user experiences. --- # PayPalButtons Container The `PayPalButtons` container renders a set of checkout buttons that enable shoppers to pay using https://www.paypal.com/ — including PayPal, Venmo, Pay Later, and debit/credit card, depending on which funding sources are enabled for the merchant. Version: 4.1.0 ## Configuration The `PayPalButtons` container provides the following configuration options: | Option | Type | Req? | Description | |--------|------|------|-------------| | `onButtonClick` | function | No | Called when the shopper clicks a PayPal button. Receives a `showPaymentSheet` function, which must be called synchronously to begin the PayPal checkout and show the payment sheet. If not provided, clicking the button automatically triggers the payment sheet. | | `onSuccess` | function | No | Called when the payment completes successfully. Receives `{ cartId: string }`. If the function returns a promise, it is awaited before marking the payment as successful. If the promise rejects, the payment is marked as failed and the error is passed to `onError` (if provided). | | `onError` | function | No | Called when the payment flow fails or is aborted. Receives `{ name: string, message: string }`, containing localized, user-facing error details. These values can be translated using `PaymentServices.PayPalButtons.errors` [language definitions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/payment-services/dictionary/). | | `hidden` | boolean | No | Whether the buttons are hidden. Set to `true` to hide the PayPal buttons. Default: `false`. | | `disabled` | boolean | No | Whether the buttons are disabled. Set to `true` to disable the PayPal buttons. Default: `false`. | ## Slots This container does not expose any customizable slots. ## Usage ### Checkout page example The following example demonstrates how to use the `PayPalButtons` container on the checkout page. ```js const $content = document.createElement('div'); PaymentServices.render(PayPalButtons, { onSuccess: ({ cartId }) => orderApi.placeOrder(cartId), onError: (error) => { console.error(error) }, })($content); ``` --- # Payment Services Dictionary The **Payment Services dictionary** contains all user-facing text, labels, and messages displayed by this drop-in. Customize the dictionary to: - **Localize** the drop-in for different languages and regions - **Customize** labels and messages to match your brand voice - **Override** default text without modifying source code for the drop-in Dictionaries use the **i18n (internationalization)** pattern, where each text string is identified by a unique key path. Version: 4.1.0 ## How to customize Override dictionary values during drop-in initialization. The drop-in deep-merges your custom values with the defaults. ```javascript await initialize({ langDefinitions: { en_US: { "PaymentServices": { "ApplePay": { "errors": { "default": { "name": "Custom value", "message": "Your custom message here" } } } } } } }); ``` You only need to include the keys you want to change. For multi-language support and advanced patterns, see the [Dictionary customization guide](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/dictionaries/). ## Default keys and values Below are the default English (`en_US`) strings provided by the **Payment Services** drop-in: ```json title="en_US.json" { "PaymentServices": { "ApplePay": { "errors": { "default": { "name": "Apple Pay error", "message": "An unexpected error occurred. Please try again or contact support." } } }, "CreditCard": { "errors": { "default": { "name": "Credit Card error", "message": "An unexpected error occurred. Please try again or contact support." } }, "formFields": { "cvv": { "invalidError": "Enter valid cvv.", "label": "", "missingError": "This field is required.", "placeholder": "CVV*" }, "expirationDate": { "invalidError": "Enter valid expiration date.", "label": "", "missingError": "This field is required.", "placeholder": "MM/YY*" }, "number": { "invalidError": "Enter valid card number.", "label": "", "missingError": "This field is required.", "placeholder": "Card Number*" } } }, "GooglePay": { "errors": { "default": { "name": "Google Pay error", "message": "An unexpected error occurred. Please try again or contact support." } } }, "PayPalButtons": { "errors": { "default": { "name": "PayPal error", "message": "An unexpected error occurred. Please try again or contact support." } } }, "messages": { "methodNotAvailable": "Payment method not available. Please contact support.", "methodNotLoaded": "Failed to load payment method. Please try again later.", "methodLoading": "Loading payment method..." } } } ``` --- # Payment Services Data & Events The Payment Services drop-in uses the [event bus](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/events/) to emit and listen for events, enabling communication between drop-ins and external integrations. Version: 4.1.0 ## Events reference | Event | Direction | Description | |---------------|-----------|------------------------------------------------------------| | [authenticated](#authenticated-listens) | Listens | Fired by the [User Auth](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/user-auth/events/) drop-in when the user's authentication state changes. Used to determine whether the shopper is a guest customer. | | [cart/data](#cartdata-listens) | Listens | Fired by the [Cart](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/) drop-in when cart data is available or changes. Read by the `GooglePay` container at click time to supply cart items and totals to the Google Pay payment sheet, and by the `PayPalButtons` container at click time to supply the cart ID to PayPal checkout. | | [checkout/initialized](#checkoutinitialized-listens) | Listens | Fired by the [Checkout](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/events/) drop-in when the checkout state is first loaded. Used as a fallback by the `GooglePay` container when `checkout/updated` has not yet been received. | | [checkout/updated](#checkoutupdated-listens) | Listens | Fired by the [Checkout](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/events/) drop-in when the checkout state is updated. Read by the `GooglePay` container at click time to supply billing address and shipping method to the Google Pay payment sheet, and by the `PayPalButtons` container at click time to supply the cart ID to PayPal checkout. | | [payment-services/initialized/checkout](#payment-servicesinitializedcheckout-emits) | Emits | Emitted when the drop-in finishes initializing for the `CHECKOUT` location. | | [payment-services/initialized/product-detail](#payment-servicesinitializedproduct-detail-emits) | Emits | Emitted when the drop-in finishes initializing for the `PRODUCT_DETAIL` location. | ## Event details The following sections provide detailed information about each event, including its direction, event payload, and usage examples. ### `authenticated` (listens) Fired by the [User Auth](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/user-auth/events/#authenticated-emits) drop-in when the user's authentication state changes. Used to determine whether the shopper is a guest customer. #### Event payload ```typescript boolean ``` `true` when the shopper is authenticated, `false` when signed out. #### Example ```js events.on('authenticated', (isAuthenticated) => { console.log('authenticated event received:', isAuthenticated); }); ``` --- ### `cart/data` (listens) Fired by the [Cart](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartdata-emits-and-listens) drop-in when cart data is available or changes. The `GooglePay` container reads this event using `events.lastPayload('cart/data')` at the moment the shopper clicks the Google Pay button — not at render time — to populate cart items and price totals in the Google Pay payment sheet. The `PayPalButtons` container reads the same event, at the same moment, to supply the cart ID to PayPal checkout — PayPal's own hosted checkout flow collects billing and shipping details itself, so `PayPalButtons` only needs the cart ID, not the full cart contents. The `cart/data` payload must therefore be present on the event bus before the shopper clicks the button. For the full `CartModel` type definition, see the [Cart events reference](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartmodel). #### Event payload ```typescript CartModel | null ``` See [`CartModel`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/cart/events/#cartmodel) for the full type definition. #### Example ```js events.on('cart/data', (payload) => { console.log('cart/data event received:', payload); }); ``` --- ### `checkout/initialized` (listens) Fired by the [Checkout](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/events/) drop-in when the checkout state is first loaded. The `GooglePay` container uses this event as a fallback when `checkout/updated` has not yet been received — for example, when the shopper opens the Google Pay payment sheet without having interacted with the checkout form. #### Event payload ```typescript CheckoutData | null ``` #### Example ```js events.on('checkout/initialized', (payload) => { console.log('checkout/initialized event received:', payload); }); ``` --- ### `checkout/updated` (listens) Fired by the [Checkout](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/events/#checkoutupdated-emits-and-listens) drop-in when the checkout state is updated. The `GooglePay` container reads this event using `events.lastPayload('checkout/updated')` at the moment the shopper clicks the Google Pay button to populate billing address and shipping method in the Google Pay payment sheet. If `checkout/updated` has not yet been received, the container falls back to `checkout/initialized`. Like `cart/data`, one of these payloads must be present on the event bus before the shopper clicks the button. For the full `Cart` and `NegotiableQuote` type definitions, see the [Checkout events reference](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/events/#cart). #### Event payload ```typescript Cart | NegotiableQuote | null ``` See [`Cart`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/events/#cart) and [`NegotiableQuote`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/checkout/events/#negotiablequote) for the full type definitions. #### Example ```js events.on('checkout/updated', (payload) => { console.log('checkout/updated event received:', payload); }); ``` --- ### `payment-services/initialized/checkout` (emits) See [Payment method availability](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/payment-services/initialization/#payment-method-availability). --- ### `payment-services/initialized/product-detail` (emits) See [Payment method availability](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/payment-services/initialization/#payment-method-availability). --- # Payment Services Functions {/* ⚠️ TEMPLATE USAGE GUIDE ⚠️ This template is used by scripts/@generate-function-docs.js to generate API function documentation. Placeholders used in this template: - DROPIN_NAME → Display name (e.g., "Cart", "Checkout") - DROPIN_DISPLAY_NAME → Display name for use in text (e.g., "Cart", "Checkout") - DROPIN_VERSION → Version number (e.g., "1.5.1") - FUNCTIONS_TABLE → Table listing all functions with brief descriptions - FUNCTIONS_CONTENT → All function documentation (generated from source .mdx files) The script handles: - Reading function .mdx files from src/api directories in source repositories - Cleaning Storybook imports and metadata - Combining all functions into a single documentation page The template and script must be kept in sync. HEADING HIERARCHY: H1: "DROPIN_NAME functions" (from title) H2: Individual function names H3: Examples, Events, Returns (subsection headings) Content flow: [Signature code block] → [Parameters table] → Examples → Events → Returns */} The Payment Services drop-in currently has no functions defined. Version: 4.1.0 {/* AUTO-GENERATED CONTENT - Do not edit below this line */} {/* This documentation is auto-generated from the drop-in source repository: git@github.com:adobe-commerce/storefront-payment-services */} --- # Payment Services overview The Payment Services drop-in component renders Payment Services checkout methods (for example credit card, Apple Pay, Google Pay, and PayPal buttons). Shoppers can also see Local Payment Methods (LPM) when your Adobe Commerce configuration exposes them. > **Test payment on every page that takes money** A surface is simply a place in the storefront where a shopper can pay, for example, full checkout, the cart page, the mini-cart, the product page, or a one-click express checkout you added. Digital wallets and express pay buttons are not guaranteed to look or behave the same in each of those places, even when the table below marks a method as supported for this drop-in. Use the table below as the list of methods this drop-in can integrate. Then, in your own project, click through each place you take payment and confirm which methods actually appear. Your theme, blocks, and Adobe Commerce configuration decide what shoppers see. Hosting also matters: Adobe Commerce on Cloud Services and other Adobe Commerce hosting setups do not always return the same methods everywhere, so run your checks in the environment that matches production. ## Supported payment methods The following table provides an overview of the payment methods that the Payment Services drop-in supports. When a row lists a `PaymentMethodCode` value, it matches the enum used in code. Local Payment Methods share the single `APM` enum value; which specific methods (for example, Bancontact or iDEAL) appear depends on what your Adobe Commerce configuration returns for your store. | Payment method | Payment method code | Status | |-------------------|---------------------|--------------------------------------------| | Apple Pay | `APPLE_PAY` | Supported | | Credit/debit card | `CREDIT_CARD` | Supported | | Local Payment Methods (LPM) | `APM` | Supported | | Google Pay | `GOOGLE_PAY` | Supported | | PayPal buttons | `PAYPAL_BUTTONS` | Supported | | PayPal Fastlane | `FASTLANE` | Roadmap | Use the `PaymentMethodCode` enum from the API import: ```ts ``` ## Available containers The Payment Services drop-in component provides four containers: - **Apple Pay container:** The `ApplePay` container renders an Apple Pay button that shoppers can use to place an order. - **Credit card container:** The `CreditCard` container renders a form where shoppers enter their card details to place an order with a credit or debit card. - **Google Pay container:** The `GooglePay` container renders a Google Pay button that shoppers can use to place an order. - **PayPal buttons container:** The `PayPalButtons` container renders PayPal, Venmo, Pay Later, and debit/credit card buttons that shoppers can use to place an order. ## Additional resources - https://developer.adobe.com/commerce/webapi/graphql/payment-services-extension/ - https://developer.adobe.com/commerce/webapi/graphql/payment-services-extension/workflows/ - https://experienceleague.adobe.com/en/docs/commerce/payment-services/guide-overview#support For more information, refer to the specific service documentation linked above. --- # Payment Services initialization The Payment Services initializer provides configuration options for the backend endpoint and language definitions. Version: 4.1.0 ## Configuration options The following table describes the configuration options available for the **Payment Services** initializer: | Option | Type | Req? | Description | |--------------------|----------|------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `apiUrl` | string | Yes | Adobe Commerce GraphQL endpoint URL (for example, `https://example.com/graphql`). | | `getCustomerToken` | function | Yes | Provides authorization for GraphQL requests made on behalf of the shopper. For token-based auth, it must return a customer token (string) or `null` for guests. For session-based auth, it must be set to `null`. | | `storeViewCode` | string | No | Adobe Commerce store view code used for GraphQL requests. If not set, the `Store` HTTP header is not included. | | `langDefinitions` | object | No | Language definitions used for internationalization (i18n). | ## Default configuration The initializer runs with these defaults when no configuration is provided: ```javascript title="scripts/initializers/payment-services.js" // All configuration options are optional await initializers.mountImmediately(initialize, { langDefinitions: {}, // Uses built-in English strings // Drop-in-specific defaults: // apiUrl: undefined // See configuration options above // getCustomerToken: undefined // See configuration options above // storeViewCode: undefined // See configuration options above }); ``` ## Language definitions Override dictionary keys for localization or branding. The `langDefinitions` object maps locale keys to custom strings that override default text for the drop-in. ```javascript title="scripts/initializers/payment-services.js" const customStrings = { "CreditCard": { "formFields": { "cvv": { "placeholder": "CVV*" }, "expirationDate": { "placeholder": "MM/YY*" }, "number": { "placeholder": "Card number*" } } }, }; const langDefinitions = { default: customStrings, }; await initializers.mountImmediately(initialize, { langDefinitions }); ``` > For complete dictionary customization including all available keys and multi-language support, see the [Payment Services Dictionary](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/payment-services/dictionary/) page. ## Customizing data models No customizable models are available for this drop-in. ## Drop-in configuration The **Payment Services initializer** provides options to configure the backend endpoint and language definitions. ```javascript title="scripts/initializers/payment-services.js" const initializeDropin = async () => { const labels = await fetchPlaceholders('placeholders/payment-services.json'); const langDefinitions = { default: { ...labels, }, }; const coreEndpoint = await getConfigValue('commerce-core-endpoint'); const getUserTokenCookie = () => getCookie('auth_dropin_user_token'); return initializers.mountImmediately(initialize, { apiUrl: coreEndpoint, getCustomerToken: getUserTokenCookie, langDefinitions, }); }; await initializeDropin(); ``` > Refer to the [Configuration options](#configuration-options) table for detailed descriptions of each option. ### Initialization is asynchronous The Payment Services drop-in initializer is fully asynchronous. Even if mounted immediately, it initializes in the background and emits initialization updates via custom events. ### Payment method availability During asynchronous initialization, the Payment Services drop-in emits a series of events to indicate readiness across different locations. Each event includes a payload of `{ availablePaymentMethods: string[] }`, listing the available payment method codes. The following table contains the available event names and their descriptions: | Event name | Description | |-----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| | `payment-services/initialized/checkout` | Indicates the drop-in is initialized for the `CHECKOUT` location. The `event.availablePaymentMethods` property lists the payment methods available on the checkout page. | | `payment-services/initialized/product-detail` | Indicates the drop-in is initialized for the `PRODUCT_DETAIL` location. The `event.availablePaymentMethods` property lists the payment methods available on the product detail page. | The following example demonstrates how to implement payment method availability event handlers for the checkout page. ```javascript events.on('payment-services/initialized/checkout', ({ availablePaymentMethods }) => { if (availablePaymentMethods.contains(PaymentMethodCode.APPLE_PAY)) { console.log("Apple Pay available for checkout page."); } if (availablePaymentMethods.contains(PaymentMethodCode.CREDIT_CARD)) { console.log("Credit Card available for checkout page."); } if (availablePaymentMethods.contains(PaymentMethodCode.GOOGLE_PAY)) { console.log("Google Pay available for checkout page."); } if (availablePaymentMethods.contains(PaymentMethodCode.PAYPAL_BUTTONS)) { console.log("PayPal Buttons available for checkout page."); } }); ``` --- # Payment Services installation This guide explains how to install and configure the Payment Services drop-in component in your storefront. ## Onboard to Payment Services Before you can use the Payment Services component in your storefront, you must https://experienceleague.adobe.com/en/docs/commerce/payment-services/get-started/onboard to Payment Services in the Adobe Commerce Admin. > **Payment Services extension** The Payment Services drop-in requires the Payment Services extension version https://experienceleague.adobe.com/en/docs/commerce/payment-services/release-notes#v2150 or higher. PaaS only (Applies to Adobe Commerce on Cloud projects (Adobe-managed PaaS infrastructure) and on-premises projects only.) ## Register your storefront domain with Apple The following steps explain how to register your storefront domain with Apple, which is required to use Apple Pay. ### 1. Download the file SaaS only (Applies to Adobe Commerce as a Cloud Service and Adobe Commerce Optimizer projects only (Adobe-managed SaaS infrastructure).) Download the Apple Pay domain verification file. https://paypalobjects.com/devdoc/apple-pay/well-known/apple-developer-merchantid-domain-association. ### 2. Serve as binary content SaaS only (Applies to Adobe Commerce as a Cloud Service and Adobe Commerce Optimizer projects only (Adobe-managed SaaS infrastructure).) Add the file to the following path in your storefront repository. Note the added `.bin` extension, which is currently the only way available in EDS to serve binary content. ```txt showLineNumbers=false /.well-known/apple-developer-merchantid-domain-association.bin ``` ### 3. Set up URL redirect SaaS only (Applies to Adobe Commerce as a Cloud Service and Adobe Commerce Optimizer projects only (Adobe-managed SaaS infrastructure).) Add a https://www.aem.live/docs/redirects to make the domain verification file accessible at `https://your-sandbox-domain.com/.well-known/apple-developer-merchantid-domain-association`. | Source | Destination | |-----------------------------------------------------------|----------------------------------------------------------------| | .well-known/apple-developer-merchantid-domain-association | /.well-known/apple-developer-merchantid-domain-association.bin | > **Sandbox only** Serving the file via a redirect works for sandbox domain registration, but not for production. In production, the file [must be served directly](#set-up-production-url-rewrite), without a redirect. ### 4. Register sandbox domain Contact your sales representative to register the sandbox domain with Apple. ### 5. Set up production URL rewrite SaaS only (Applies to Adobe Commerce as a Cloud Service and Adobe Commerce Optimizer projects only (Adobe-managed SaaS infrastructure).) Set up a /setup/configuration/content-delivery-network/#url-rewrites at the CDN level to make the domain verification file directly accessible at https://your-production-domain.com/.well-known/apple-developer-merchantid-domain-association, without redirects. ### 6. Register production domain Contact your sales representative to register the production domain with Apple. --- # Payment Services Slots The Payment Services drop-in does not expose any slots for customization. ## Why no slots? This drop-in wraps the Adobe Payment Services SDK (`@adobe-commerce/payment-services-sdk`), which renders secure payment forms directly into specified DOM elements. The SDK controls all UI rendering to maintain PCI (Payment Card Industry) compliance and security standards. You customize the payment forms through SDK configuration options (field placeholders, card type settings, callback handlers) passed to `sdk.Payment.CreditCard.render()`, not through the slot-based pattern other drop-ins use. Version: 4.1.0 --- # Payment Services styles Customize the Payment Services drop-in using CSS classes and design tokens. This page covers the Payment Services-specific container classes and customization examples. For comprehensive information about design tokens, responsive breakpoints, and styling best practices, see [Styling Drop-In Components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/). Version: 4.1.0 ## Customization example Add this to the CSS file of the specific https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/ where you're using the Payment Services drop-in. For a complete list of available design tokens (colors, spacing, typography, and more), see the [Design tokens reference](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/#design-tokens-reference). ```css title="styles/styles.css" del={2-2} ins={3-3} .credit-card-error { padding: var(--spacing-small); padding: var(--spacing-medium); } ``` ## Container classes The Payment Services drop-in uses BEM-style class naming. Use the browser DevTools to inspect elements and find specific class names. ```css /* CheckoutPaymentMethods */ .payment-services-checkout-payment-methods {} /* CreditCardForm */ .credit-card-error {} .credit-card-error__icon {} /* CreditCardForm */ .credit-card-field {} .credit-card-field__container {} .credit-card-field__container--error {} .credit-card-field__error {} .credit-card-field__label {} /* CreditCardForm */ .hidden {} .payment-services-credit-card-form {} .payment-services-credit-card-form__card-number {} .payment-services-credit-card-form__eligible-cards {} .payment-services-credit-card-form__eligible-cards-icon {} .payment-services-credit-card-form__eligible-cards-selected {} .payment-services-credit-card-form__eligible-cards-unselected {} .payment-services-credit-card-form__loading {} ``` --- # Personalization Containers The **Personalization** drop-in provides pre-built container components for integrating into your storefront. Version: 3.2.0 ## What are Containers? Containers are pre-built UI components that combine functionality, state management, and presentation. They provide a complete solution for specific features and can be customized through props, slots, and CSS. ## Available Containers | Container | Description | | --------- | ----------- | | [TargetedBlock](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/personalization/containers/targeted-block/) | Learn about the `TargetedBlock` container in the personalization drop-in component. | > Each container is designed to work independently but can be composed together to create comprehensive user experiences. --- # TargetedBlock container The `TargetedBlock` container wraps the conditional content. ## Configurations The `TargetedBlock` container provides the following configuration options: ```text [ ['Options', 'Type', 'Req?', 'Description'], ['slots', '{Content: SlotProps}', 'Yes', 'The slot that provides the content to be displayed conditionally.'], ['personalizationData', 'PersonalizationData', 'Yes', 'The customer groups, segments, and cart price rules that must be active for the block to render.'], ['type', 'string', 'No', 'Specify this value when you want only the first Targeted Block of this type that matches the conditions to be displayed on the page.'] ] ``` ## Example The following example demonstrates how to integrate the `TargetedBlock` container: ```javascript function prepareIds(providedIds) { return providedIds.split(',').map((num) => btoa(num.trim())); } export default async function decorate(block) { const blockConfig = readBlockConfig(block); const { fragment, type, 'customer-segments': customerSegments, 'customer-groups': customerGroups, 'cart-rules': rules, } = blockConfig; const content = (blockConfig.fragment !== undefined) ? await loadFragment(fragment) : block.children[block.children.length - 1]; const segments = customerSegments !== undefined ? prepareIds(customerSegments) : []; const groups = customerGroups !== undefined ? prepareIds(customerGroups) : []; const cartRules = rules !== undefined ? prepareIds(rules) : []; render.render(TargetedBlock, { type, personalizationData: { segments, groups, cartRules, }, slots: { Content: (ctx) => { const container = document.createElement('div'); container.append(content); ctx.replaceWith(container); }, }, })(block); } ``` --- # Personalization Dictionary The **Personalization dictionary** contains all user-facing text, labels, and messages displayed by this drop-in. Customize the dictionary to: - **Localize** the drop-in for different languages and regions - **Customize** labels and messages to match your brand voice - **Override** default text without modifying source code for the drop-in Dictionaries use the **i18n (internationalization)** pattern, where each text string is identified by a unique key path. Version: 3.2.0 ## How to customize Override dictionary values during drop-in initialization. The drop-in deep-merges your custom values with the defaults. ```javascript await initialize({ langDefinitions: { en_US: { "Personalization": { "Component": { "heading": "My Custom Heading", "buttonText": "Click Me" } } } } }); ``` You only need to include the keys you want to change. For multi-language support and advanced patterns, see the [Dictionary customization guide](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/dictionaries/). ## Default keys and values Below are the default English (`en_US`) strings provided by the **Personalization** drop-in: ```json title="en_US.json" { "": {} } ``` --- # Personalization Data & Events The **Personalization** drop-in uses the [event bus](https://experienceleague.adobe.com/developer/commerce/storefront/sdk/reference/events/) to emit and listen to events for communication between drop-ins and external integrations. Version: 3.2.0 ## Events reference {/* EVENTS_TABLE_START */} | Event | Direction | Description | |-------|-----------|-------------| | [cart/initialized](#cartinitialized-listens) | Listens | Fired by Cart (`cart`) when the component completes initialization. | | [cart/updated](#cartupdated-listens) | Listens | Fired by Cart (`cart`) when the component state is updated. | | [order/placed](#orderplaced-listens) | Listens | Fired by Order (`order`) when an order is placed. | | [personalization/updated](#personalizationupdated-emits-and-listens) | Emits and listens | Triggered when the component state is updated. | {/* EVENTS_TABLE_END */} ## Event details The following sections provide detailed information about each event, including its direction, event payload, and usage examples. ### `cart/initialized` (listens) Fired by Cart (`cart`) when the component completes initialization. #### Event payload ```typescript CartModel | null ``` See [`CartModel`](#cartmodel) for full type definition. #### Example ```js events.on('cart/initialized', (payload) => { console.log('cart/initialized event received:', payload); // Add your custom logic here }); ``` ### `cart/updated` (listens) Fired by Cart (`cart`) when the component state is updated. #### Event payload ```typescript CartModel | null ``` See [`CartModel`](#cartmodel) for full type definition. #### Example ```js events.on('cart/updated', (payload) => { console.log('cart/updated event received:', payload); // Add your custom logic here }); ``` ### `order/placed` (listens) Fired by Order (`order`) when an order is placed. #### Event payload ```typescript OrderDataModel ``` See [`OrderDataModel`](#orderdatamodel) for full type definition. #### Example ```js events.on('order/placed', (payload) => { console.log('order/placed event received:', payload); // Add your custom logic here }); ``` ### `personalization/updated` (emits and listens) Triggered when the component state is updated. #### Event payload ```typescript PersonalizationData, 'personalization/type-matched': string, 'cart/initialized': CartModel | null ``` See [`PersonalizationData`](#personalizationdata), [`CartModel`](#cartmodel) for full type definitions. #### Example ```js events.on('personalization/updated', (payload) => { console.log('personalization/updated event received:', payload); // Add your custom logic here }); ``` ## Data Models The following data models are used in event payloads for this drop-in. ### CartModel Used in: [`cart/initialized`](#cartinitialized-listens), [`cart/updated`](#cartupdated-listens), [`personalization/updated`](#personalizationupdated-emits-and-listens). ```ts interface CartModel { id: string; } ``` ### OrderDataModel Used in: [`order/placed`](#orderplaced-listens). ```ts interface OrderDataModel { id: string; } ``` ### PersonalizationData Used in: [`personalization/updated`](#personalizationupdated-emits-and-listens). ```ts interface PersonalizationData { segments: string[], groups: string[], cartRules: string[] } ``` --- # Personalization Functions The Personalization drop-in provides API functions that enable you to programmatically control behavior, fetch data, and integrate with Adobe Commerce backend services. Version: 3.2.0 | Function | Description | | --- | --- | | [`fetchPersonalizationData`](#fetchpersonalizationdata) | Request the customer groups, applied segments, and cart price rules from Adobe Commerce based on the cart ID. | | [`getPersonalizationData`](#getpersonalizationdata) | Retrieves the saved personalization data from a cookie. | | [`getStoreConfig`](#getstoreconfig) | Returns information about the store configuration related to personalization. | | [`savePersonalizationData`](#savepersonalizationdata) | Saves the personalization data to a cookie for later retrieval. | ## fetchPersonalizationData The `fetchPersonalizationData` function can be used to request the customer groups, applied segments, and cart price rules from Adobe Commerce based on the cart ID. ```ts const fetchPersonalizationData = async ( cartId: string ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `cartId` | `string` | Yes | The ID of the shopping cart. | ### Events Does not emit any drop-in events. ### Returns Returns [`PersonalizationData`](#personalizationdata) or `null`. ## getPersonalizationData The `getPersonalizationData` function retrieves the saved personalization data from a cookie. ```ts const getPersonalizationData = async (): PersonalizationData ``` ### Events Does not emit any drop-in events. ### Returns Returns [`PersonalizationData`](#personalizationdata). ## getStoreConfig The `getStoreConfig` function returns information about the store configuration related to personalization. ```ts const getStoreConfig = async (): Promise ``` ### Events Does not emit any drop-in events. ### Returns Returns [`StoreConfigModel`](#storeconfigmodel) or `null`. ## savePersonalizationData The `savePersonalizationData` function saves the personalization data to a cookie for later retrieval. ```ts const savePersonalizationData = async ( data: PersonalizationData ): Promise ``` | Parameter | Type | Req? | Description | |---|---|---|---| | `data` | `PersonalizationData` | Yes | Personalization data containing groups, segments, and cart price rules. | ### Events Emits the `personalization/updated` event. Emits the **personalization-updated** event with the saved personalization data, including customer segments, groups, and cart price rules. ### Returns Returns `void`. ## Data Models The following data models are used by functions in this drop-in. ### PersonalizationData The `PersonalizationData` object is returned by the following functions: [`fetchPersonalizationData`](#fetchpersonalizationdata), [`getPersonalizationData`](#getpersonalizationdata). ```ts interface PersonalizationData { segments: string[], groups: string[], cartRules: string[] } ``` ### StoreConfigModel The `StoreConfigModel` object is returned by the following functions: [`getStoreConfig`](#getstoreconfig). ```ts interface StoreConfigModel { shareActiveSegments: boolean; shareCustomerGroup: boolean; shareAppliedCartRule: boolean; customerAccessTokenLifetime: number; } ``` {/* This documentation is auto-generated from the drop-in source repository: REPO_URL */} --- # Personalization overview The personalization drop-in component provides a set of tools and containers designed to display content conditionally, based on Adobe Commerce customer groups, segments, and cart price rules. ## Overview The personalization drop-in component provides the [`TargetedBlock`](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/personalization/containers/targeted-block/) container, which requires you to specify the content (or a path to a fragment containing the content), and optionally specify the block type, Adobe Commerce customer groups, segments, and cart price rules that determine which customers can view the content. The component's initialization sets up event listeners that respond to changes in authentication state and cart state. These listeners request the currently applied customer groups, segments, and cart price rules from Adobe Commerce and save them to a cookie. When you add a `TargetedBlock` container to a page, it displays only when the customer groups, segments, and cart price rules specified in the block configuration match the groups, segments, and rules stored in the cookie. When you specify a block type for a `TargetedBlock`, only the first targeted block of that type is rendered on the page. This behavior enables you to create a fallback chain of targeted blocks. ## Supported Commerce features The following table provides an overview of the Adobe Commerce targeting inputs the personalization drop-in uses: | Feature | Status | | ------- | ------ | | Customer group targeting | Supported | | Customer segment targeting | Supported | | Cart price rule targeting | Supported | | Fallback chain by block type | Supported | --- # Personalization initialization The **Personalization initializer** configures personalization features including user preferences, behavioral tracking, and content customization. Use initialization to customize personalization data models and enhance user experience. Version: 3.2.1 ## Configuration options The following table describes the configuration options available for the **Personalization** initializer: | Parameter | Type | Req? | Description | |---|---|---|---| | `langDefinitions` | [`LangDefinitions`](#langdefinitions) | No | Language definitions for internationalization (i18n). Override dictionary keys for localization or branding. | ## Default configuration The initializer runs with these defaults when no configuration is provided: ```javascript title="scripts/initializers/personalization.js" // All configuration options are optional await initializers.mountImmediately(initialize, { langDefinitions: {}, // Uses built-in English strings models: {}, // Uses default data models }); ``` ## Language definitions Override dictionary keys for localization or branding. The `langDefinitions` object maps locale keys to custom strings that override default text for the drop-in. ```javascript title="scripts/initializers/personalization.js" const customStrings = { 'AddToCart': 'Add to Bag', 'Checkout': 'Complete Purchase', 'Price': 'Cost', }; const langDefinitions = { default: customStrings, }; await initializers.mountImmediately(initialize, { langDefinitions }); ``` > For complete dictionary customization including all available keys and multi-language support, see the [Personalization Dictionary](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/personalization/dictionary/) page. ## Customizing data models Extend or transform data models by providing custom transformer functions. Use the `models` option to add custom fields or modify existing data structures returned from the backend. ### Available models The following models can be customized through the `models` configuration option: > No customizable models are available for this drop-in. The following example shows how to customize the `CustomModel` model for the **Personalization** drop-in: ```javascript title="scripts/initializers/personalization.js" const models = { CustomModel: { transformer: (data) => ({ // Add custom fields from backend data customField: data?.custom_field, promotionBadge: data?.promotion?.label, // Transform existing fields displayPrice: data?.price?.value ? `${data.price.value}` : 'N/A', }), }, }; await initializers.mountImmediately(initialize, { models }); ``` ## Configuration types The following TypeScript definitions show the structure of each configuration object: ### langDefinitions Maps locale identifiers to dictionaries of key-value pairs. The `default` locale is used as the fallback when no specific locale matches. Each dictionary key corresponds to a text string used in the drop-in UI. ```typescript langDefinitions?: { [locale: string]: { [key: string]: string; }; }; ``` --- # Personalization Quick Start The Personalization drop-in enables dynamic, AI-powered content recommendations based on real-time customer behavior and Adobe Experience Platform data. Version: 3.2.0 ## Quick example The Personalization drop-in is included in the https://github.com/hlxsites/aem-boilerplate-commerce. This example shows the basic pattern: ```js // 1. Import initializer (handles all setup) // 2. Import the container you need // 3. Import the provider // 4. Render in your block export default async function decorate(block) { await provider.render(TargetedBlock, { // Configuration options - see Containers page })(block); } ``` **New to drop-ins?** See the [Using drop-ins](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/quick-start/) guide for complete step-by-step instructions. ## Quick reference **Import paths:** - Initializer: `import '../../scripts/initializers/personalization.js'` - Containers: `import ContainerName from '@dropins/storefront-personalization/containers/ContainerName.js'` - Provider: `import { render } from '@dropins/storefront-personalization/render.js'` **Package:** `@dropins/storefront-personalization` **Version:** 3.2.0 (verify compatibility with your Commerce instance) **Example container:** `TargetedBlock` ## Learn more - [Containers](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/personalization/containers/) - Available UI components and configuration options - [Initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/personalization/initialization/) - Customize initializer settings and data models - [Functions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/personalization/functions/) - Control drop-in behavior programmatically - [Events](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/personalization/events/) - Listen to and respond to drop-in state changes - [Slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/personalization/slots/) - Extend containers with custom content --- # Personalization Slots The Personalization drop-in exposes slots for customizing specific UI sections. Use slots to replace or extend container components. For default properties available to all slots, see [Extending drop-in components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/extending/). Version: 3.2.0 | Container | Slots | |-----------|-------| | [`TargetedBlock`](#targetedblock-slots) | `Content` | > **Slot usage best practice** Do not use context methods inside other context methods (for example, `appendChild()` inside `onChange()`). See [Slots best practices](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/slots/#best-practice-for-dynamic-slot-content) for details and examples. ## TargetedBlock slots The slots for the `TargetedBlock` container allow you to customize its appearance and behavior. ```typescript interface TargetedBlockProps { slots?: { Content: SlotProps }; } ``` --- # Personalization styles Customize the Personalization drop-in using CSS classes and design tokens. This page covers the Personalization-specific container classes and customization examples. For comprehensive information about design tokens, responsive breakpoints, and styling best practices, see [Styling Drop-In Components](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/). Version: 3.2.0 ## Customization example Add this to the CSS file of the specific https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/ where you're using the Personalization drop-in. For a complete list of available design tokens (colors, spacing, typography, and more), see the [Design tokens reference](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/all/styling/#design-tokens-reference). ```css title="styles/styles.css" /* Target Personalization containers */ .personalization-container { /* Use the browser DevTools to find the specific classes you need */ } ``` ## Container classes The Personalization drop-in uses BEM-style class naming. Use the browser DevTools to inspect elements and find specific class names. --- # Product Details Containers The Product Details drop-in provides pre-built container components for integrating into your storefront. Version: 3.2.0 ## What are Containers? Containers are pre-built UI components that combine functionality, state management, and presentation. They provide a complete solution for specific features and can be customized through props, slots, and CSS. ## Available Containers > The monolithic `ProductDetails` container is deprecated. It is not used in the Commerce boilerplate. Do not use it for new work. Compose the smaller containers in this table instead (for example, [ProductHeader](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/containers/product-header/), [ProductGallery](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/containers/product-gallery/), [ProductPrice](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/containers/product-price/), and [ProductOptions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/containers/product-options/)), then use [Slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/slots/) on each container when you need slot-based customization. Add to Cart, Add to Wishlist, and similar primary actions are not provided through the Product Details slots in the [Slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/slots/) table. The storefront composes them in the product-details block using separate mount points and the Wishlist or cart patterns described in [Add to Cart and Add to Wishlist](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/slots/#add-to-cart-and-add-to-wishlist) on that page. | Container | Description | | --------- | ----------- | | [ProductAttributes](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/containers/product-attributes/) | Configure the `ProductAttributes` container for the product details page drop-in component. | | [ProductDownloadableOptions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/containers/product-downloadable-options/) | Renders the downloadable link selection UI for downloadable product types. | | [ProductDescription](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/containers/product-description/) | Configure the `ProductDescription` container for the product details page drop-in component. | | [ProductDetails container (deprecated)](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/containers/product-details/) | Deprecated monolithic container. See the caution above and the linked topic for configuration and slots. | | [ProductGallery](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/containers/product-gallery/) | Configure the `ProductGallery` container for the product details page drop-in component. | | [ProductGiftCardOptions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/containers/product-gift-card-options/) | *Enrichment needed - add description to `_dropin-enrichments/product-details/containers.json`* | | [ProductHeader](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/containers/product-header/) | Configure the `ProductHeader` container for the product details page drop-in component. | | [ProductOptions](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/containers/product-options/) | Configure the `ProductOptions` container for the product details page drop-in component. | | [ProductPrice](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/containers/product-price/) | Configure the `ProductPrice` container for the product details page drop-in component. | | [ProductQuantity](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/containers/product-quantity/) | Configure the `ProductQuantity` container for the product details page drop-in component. | | [ProductShortDescription](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/containers/product-short-description/) | Configure the `ProductShortDescription` container for the product details page drop-in component. | > Each container is designed to work independently but can be composed together to create comprehensive user experiences. --- # ProductAttributes The `ProductAttributes` container displays a list of attributes for a product on the product details page. The container receives initial product data during [initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/initialization/) to preload the component and, being event-driven, updates with data emitted to `pdp/data` within the event scope. ## ProductAttributes configurations The `ProductAttributes` container provides the following configuration options: ```text [ ['Option', 'Type', 'Req?', 'Description'], ['formatValue', 'function', 'No', 'Custom formatter for attribute values. Receives the formatted value, attribute id, and label; returns the string to display. Use this to override the default rendering of any attribute value.'], ['scope', 'string', 'No', 'Unique identifier for the PDP context. Only containers rendered with this scope will respond to product events.'], ] ``` ## Example The following example demonstrates how to configure the `ProductAttributes` container: ```js return productRenderer.render(ProductAttributes, { scope: 'modal', // optional }); ``` --- # ProductDescription The `ProductDescription` container displays the detailed description of a product on the product details page. The container receives initial product data during [initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/initialization/) to preload the component and, being event-driven, updates with data emitted to `pdp/data` within the event scope. ## ProductDescription configurations The `ProductDescription` container provides the following configuration options: ```text [ ['Option', 'Type', 'Req?', 'Description'], ['scope', 'string', 'No', 'Unique identifier for the PDP context. Only containers rendered with this scope will respond to product events.'], ] ``` ## Example The following example demonstrates how to configure the `ProductDescription` container: ```js return productRenderer.render(ProductDescription, { scope: 'modal', // optional }); ``` --- # ProductDetails container (deprecated) > **Deprecated** The monolithic `ProductDetails` container is deprecated. It is not used in the Commerce boilerplate. Do not use it for new work. Compose the smaller containers from the [containers overview](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/containers/) instead. For slot-by-slot examples, see [ProductDetails (deprecated) slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/slots/#productdetails-deprecated-slots). For migration context, see [Product details page architectural evolution](https://experienceleague.adobe.com/developer/commerce/storefront/boilerplate/updates/#product-details-page-architectural-evolution). The legacy `ProductDetails` container (deprecated) renders a full product details experience from one component. This topic lists configuration parameters and slot names for older integrations. Version: 3.2.0 ## Configuration The `ProductDetails` container (deprecated) provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `sku` | `string` | Yes | SKU of the product to load. | | `productData` | `ProductModel` | No | Optional pre-fetched product data. | | `hideSku` | `boolean` | No | Hides the SKU when set to `true`. | | `hideQuantity` | `boolean` | No | Hides the quantity selector when set to `true`. | | `hideShortDescription` | `boolean` | No | Hides the short description when set to `true`. | | `hideDescription` | `boolean` | No | Hides the long description when set to `true`. | | `hideAttributes` | `boolean` | No | Hides the attributes block when set to `true`. | | `hideSelectedOptionValue` | `boolean` | No | Hides the selected option value display when set to `true`. | | `hideURLParams` | `boolean` | No | Stops reading product context from URL parameters when set to `true`. | | `carousel` | `CarouselConfig` | No | Carousel configuration for the gallery. | | `optionsConfig` | `OptionsConfig` | No | Options UI configuration. | | `useACDL` | `boolean` | No | Enables Adobe Client Data Layer integration when set to `true`. | | `onAddToCart` | `function` | No | Callback invoked when the shopper adds to cart. | | `zoomType` | `'zoom' \| 'overlay'` | No | Image zoom behavior. | | `closeButton` | `boolean` | No | Shows or hides a close control where applicable. | | `disableDropdownPreselection` | `boolean` | No | Disables preselecting the first dropdown option when set to `true`. | ## Slots This container exposes the following slots for customization. For examples and TypeScript shapes, see [ProductDetails (deprecated) slots](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/slots/#productdetails-deprecated-slots). | Slot | Type | Required | Description | |------|------|----------|-------------| | `Title` | `SlotProps` | No | Title area of the PDP. | | `SKU` | `SlotProps` | No | SKU line. | | `RegularPrice` | `SlotProps` | No | Regular price display. | | `SpecialPrice` | `SlotProps` | No | Special or sale price display. | | `Options` | `SlotProps` | No | Configurable options UI. | | `Quantity` | `SlotProps` | No | Quantity selector. | | `Actions` | `SlotProps` | No | Primary actions region (for example, add to cart). | | `ShortDescription` | `SlotProps` | No | Short description block. | | `Description` | `SlotProps` | No | Long description block. | | `Attributes` | `SlotProps` | No | Attributes list. | | `Breadcrumbs` | `SlotProps` | No | Breadcrumb trail. | | `GalleryContent` | `SlotProps` | No | Gallery region content. | | `InfoContent` | `SlotProps` | No | Secondary info column content. | | `Content` | `SlotProps` | No | General content region. | ## Usage The following example shows how older code rendered the `ProductDetails` container (deprecated): ```js await provider.render(ProductDetails, { sku: 'PRODUCT-SKU-123', productData: productData, hideSku: true, slots: { // Add custom slot implementations here }, })(block); ``` --- # ProductDownloadableOptions Container Version: 3.2.0 The `ProductDownloadableOptions` container renders the downloadable link selection UI for downloadable product types. It allows customers to choose which downloadable files (links) to include in their purchase. ## Configuration The `ProductDownloadableOptions` container provides the following configuration options: | Parameter | Type | Req? | Description | |---|---|---|---| | `scope` | `string` | No | Optional scope identifier used to namespace product data events, allowing multiple PDP instances on the same page. | ## Slots This container does not expose any customizable slots. ## Usage The following example demonstrates how to use the `ProductDownloadableOptions` container: ```js await provider.render(ProductDownloadableOptions, { scope: "example", })(block); ``` --- # ProductGallery The `ProductGallery` container displays a gallery of product images and videos on the product details page. The container receives initial product data during [initialization](https://experienceleague.adobe.com/developer/commerce/storefront/dropins/product-details/initialization/) to preload the component and, being event-driven, updates with data emitted to `pdp/data` within the event scope. ## ProductGallery configurations The `ProductGallery` container provides the following configuration options: ```text [ ['Option', 'Type', 'Req?', 'Description'], ['arrowsOnMainImage', 'boolean', 'No', 'Displays navigation arrows on the main image even when thumbnails are shown. Applicable when `controls` is set to `thumbnailsRow` or `thumbnailsColumn`.'], ['controls', 'string', 'No', 'Type of controls for navigation. Options are thumbnailsRow, thumbnailsColumn, dots, or null. Defaults to dots.'], ['loop', 'boolean', 'No', 'Whether to loop the images in the carousel. Defaults to true.'], ['peak', 'boolean', 'No', 'Whether to enable the peak feature. Defaults to false.'], ['gap', 'string', 'No', 'Gap size between images. Options are small, medium, large, or null. Defaults to null.'], ['arrows', 'boolean', 'No', 'Whether to display navigation arrows. Defaults to true.'], ['imageParams', 'object', 'No', 'Parameters for resolving image URLs.'], ['thumbnailParams', 'object', 'No', 'Parameters for resolving thumbnail URLs.'], ['zoom', 'boolean or object', 'No', 'Configuration for the zoom feature. If an object, it can have a closeButton property to show a close button. Defaults to false.'], ['videos', 'boolean or object', 'No', 'Configuration for video support in the gallery. Set to true to enable videos (positioned after images) or pass an object with a position property. Defaults to false (disabled).'], ['scope', 'string', 'No', 'Unique identifier for the PDP context. Only containers rendered with this scope will respond to product events.'], ] ``` ## Videos The `videos` prop enables product video support in the gallery carousel. The following usage example shows the different configuration options: disabled (default), enabled, and placing your videos before or after your static images within the carousel. ### Usage ```js // Disabled (default) — backward compatible // Enabled — videos appear after images // Videos appear before images // Videos appear after images (explicit) ``` ### Supported video sources The gallery supports multiple video sources and formats, rendering each based on its URL type. | Video source | Render method | |--------------|---------------| | Video file URLs — URLs that resolve directly to a video file (.mp4, .webm, .ogg, .mov, .avi, .mkv) | Native `