@dropins/mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +127 -0
- package/README.md +314 -0
- package/dist/common/project-reader.d.ts +55 -0
- package/dist/common/project-reader.js +173 -0
- package/dist/common/registry-loader.d.ts +101 -0
- package/dist/common/registry-loader.js +386 -0
- package/dist/common/response-handling.d.ts +12 -0
- package/dist/common/response-handling.js +21 -0
- package/dist/common/sanitize.d.ts +8 -0
- package/dist/common/sanitize.js +45 -0
- package/dist/common/synonyms.d.ts +9 -0
- package/dist/common/synonyms.js +127 -0
- package/dist/common/telemetry.d.ts +14 -0
- package/dist/common/telemetry.js +54 -0
- package/dist/common/types.d.ts +308 -0
- package/dist/common/types.js +1 -0
- package/dist/common/version.d.ts +2 -0
- package/dist/common/version.js +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +136 -0
- package/dist/operations/analyze-project.d.ts +13 -0
- package/dist/operations/analyze-project.js +125 -0
- package/dist/operations/check-block-health.d.ts +19 -0
- package/dist/operations/check-block-health.js +1149 -0
- package/dist/operations/check-config.d.ts +13 -0
- package/dist/operations/check-config.js +228 -0
- package/dist/operations/explain-event-flow.d.ts +16 -0
- package/dist/operations/explain-event-flow.js +218 -0
- package/dist/operations/get-upgrade-diff.d.ts +13 -0
- package/dist/operations/get-upgrade-diff.js +144 -0
- package/dist/operations/list-api-functions.d.ts +13 -0
- package/dist/operations/list-api-functions.js +53 -0
- package/dist/operations/list-containers.d.ts +13 -0
- package/dist/operations/list-containers.js +44 -0
- package/dist/operations/list-design-tokens.d.ts +13 -0
- package/dist/operations/list-design-tokens.js +47 -0
- package/dist/operations/list-events.d.ts +16 -0
- package/dist/operations/list-events.js +39 -0
- package/dist/operations/list-graphql-queries.d.ts +19 -0
- package/dist/operations/list-graphql-queries.js +84 -0
- package/dist/operations/list-i18n-keys.d.ts +19 -0
- package/dist/operations/list-i18n-keys.js +105 -0
- package/dist/operations/list-models.d.ts +16 -0
- package/dist/operations/list-models.js +80 -0
- package/dist/operations/list-slots.d.ts +16 -0
- package/dist/operations/list-slots.js +81 -0
- package/dist/operations/scaffold-block.d.ts +31 -0
- package/dist/operations/scaffold-block.js +331 -0
- package/dist/operations/scaffold-extension.d.ts +28 -0
- package/dist/operations/scaffold-extension.js +346 -0
- package/dist/operations/scaffold-slot.d.ts +22 -0
- package/dist/operations/scaffold-slot.js +189 -0
- package/dist/operations/search-commerce-docs.d.ts +16 -0
- package/dist/operations/search-commerce-docs.js +101 -0
- package/dist/operations/search-docs.d.ts +23 -0
- package/dist/operations/search-docs.js +298 -0
- package/dist/operations/suggest-event-handler.d.ts +16 -0
- package/dist/operations/suggest-event-handler.js +175 -0
- package/dist/operations/suggest-slot-implementation.d.ts +19 -0
- package/dist/operations/suggest-slot-implementation.js +183 -0
- package/dist/registry/api-functions.json +3045 -0
- package/dist/registry/block-patterns.json +78 -0
- package/dist/registry/containers.json +2003 -0
- package/dist/registry/design-tokens.json +577 -0
- package/dist/registry/docs/boilerplate.json +55 -0
- package/dist/registry/docs/dropins-all.json +97 -0
- package/dist/registry/docs/dropins-b2b.json +607 -0
- package/dist/registry/docs/dropins-cart.json +163 -0
- package/dist/registry/docs/dropins-checkout.json +193 -0
- package/dist/registry/docs/dropins-order.json +139 -0
- package/dist/registry/docs/dropins-payment-services.json +73 -0
- package/dist/registry/docs/dropins-personalization.json +67 -0
- package/dist/registry/docs/dropins-product-details.json +139 -0
- package/dist/registry/docs/dropins-product-discovery.json +85 -0
- package/dist/registry/docs/dropins-recommendations.json +67 -0
- package/dist/registry/docs/dropins-user-account.json +121 -0
- package/dist/registry/docs/dropins-user-auth.json +103 -0
- package/dist/registry/docs/dropins-wishlist.json +85 -0
- package/dist/registry/docs/get-started.json +85 -0
- package/dist/registry/docs/how-tos.json +19 -0
- package/dist/registry/docs/index.json +139 -0
- package/dist/registry/docs/licensing.json +19 -0
- package/dist/registry/docs/merchants.json +523 -0
- package/dist/registry/docs/resources.json +13 -0
- package/dist/registry/docs/sdk.json +139 -0
- package/dist/registry/docs/setup.json +145 -0
- package/dist/registry/docs/troubleshooting.json +19 -0
- package/dist/registry/events.json +2200 -0
- package/dist/registry/examples/index.json +19 -0
- package/dist/registry/examples/storefront-checkout.json +377 -0
- package/dist/registry/examples/storefront-quote-management.json +49 -0
- package/dist/registry/extensions.json +272 -0
- package/dist/registry/graphql.json +3469 -0
- package/dist/registry/i18n.json +1873 -0
- package/dist/registry/models.json +1001 -0
- package/dist/registry/sdk.json +2357 -0
- package/dist/registry/slots.json +2270 -0
- package/dist/registry/tools-components.json +595 -0
- package/dist/resources/guides.d.ts +7 -0
- package/dist/resources/guides.js +625 -0
- package/dist/resources/handlers.d.ts +31 -0
- package/dist/resources/handlers.js +322 -0
- package/package.json +47 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dropin": "storefront-checkout",
|
|
3
|
+
"extensionGuide": "# Commerce Checkout Extensions\n\nThis directory contains extensions for the commerce-checkout block. Extensions allow you to customize checkout behavior without modifying the base block files.\n\nThe extension manager itself is provided by the SDK (`@dropins/tools/lib.js`). The block passes the extensions registered here to `createExtensionManager`, which handles hook execution and external resource loading.\n\n## Overview\n\nThe extension system runs all hooks sequentially. Hooks let you:\n\n- Handle payment provider redirects\n- Add custom payment methods\n- Add custom validation logic\n- Handle order placement\n- Customize address form rendering\n- Customize shipping method rendering\n\n```mermaid\nflowchart TD\n A[Checkout loads] --> B[Load extensions]\n B --> C[checkout/payment-response hook]\n C --> D{shouldExit?}\n D -->|Yes| E[Process payment and display success page]\n D -->|No| F[Render checkout]\n F --> F1[checkout/address-form-render hook]\n F1 --> F2[checkout/shipping-methods-render hook]\n F2 --> G[checkout/payment-methods hook]\n G --> H[User clicks Place Order]\n H --> I[checkout/validate hook]\n I --> J{isValid?}\n J -->|No| K[Show errors]\n J -->|Yes| L[checkout/place-order hook]\n L --> M{preventDefault?}\n M -->|Yes| N[Extension handled order]\n M -->|No| O[Default order placement]\n N --> P[Order complete]\n O --> P\n```\n\n## Quick Start\n\n### Enable an Extension\n\n1. Import your extension in `index.js`\n2. Add it to the default export array\n\n```javascript\nimport myExtension from './my-extension/my-extension.js';\n\nexport default [\n myExtension,\n];\n```\n\n### Disable an Extension\n\nComment it out in `index.js`:\n\n```javascript\nexport default [\n // myExtension, // Disabled\n];\n```\n\n## Extension Structure\n\n```javascript\nexport default {\n id: 'unique-extension-id',\n name: 'My Extension',\n \n externalScripts: [\n 'https://example.com/sdk.js',\n ],\n externalStyles: [\n 'https://example.com/styles.css',\n ],\n \n hooks: {\n 'checkout/payment-methods': async ({ context }) => {\n context.paymentMethods['my-payment'] = {\n render: (ctx) => {\n // Render your payment UI\n },\n };\n },\n \n 'checkout/validate': async ({ context }) => {\n if (context.code !== 'my-payment') return;\n if (!await myCustomValidation()) context.isValid = false;\n },\n \n 'checkout/place-order': async ({ context }) => {\n const { cartId, code } = context;\n \n if (code !== 'my-payment') return;\n \n context.preventDefault = true;\n \n await processPayment();\n await orderApi.placeOrder(cartId);\n },\n },\n};\n```\n\n## Hook Pattern\n\nEvery hook receives `{ context }`:\n\n- **`context`** - Shared object you can read from and write to\n- All hooks run sequentially for each extension\n- Use early `return` to skip your logic if not applicable\n\n## Available Hooks\n\n### `checkout/payment-methods`\n\nAdd payment methods by writing to `context.paymentMethods`.\n\n**Context:** `{ paymentMethods }`\n\n- `paymentMethods` - Object to add your payment methods to\n\n**Example:**\n\n```javascript\n'checkout/payment-methods': async ({ context }) => {\n context.paymentMethods['my-payment'] = {\n autoSync: false,\n render: (ctx) => {\n const container = document.createElement('div');\n ctx.replaceHTML(container);\n },\n };\n}\n```\n\n---\n\n### `checkout/validate`\n\nAdd custom validation logic before order placement. Default form validation runs first - this hook only runs if default validation passes.\n\n**Context:** `{ code, isValid }`\n\n- `code` - The selected payment method code\n- `isValid` - Set to `false` to fail validation\n\n**Pattern:**\n\n- If it's not your payment method → early `return`\n- If it's your payment method → run validation, set `isValid = false` if failed\n\n**Example:**\n\n```javascript\n'checkout/validate': async ({ context }) => {\n const { code } = context;\n \n if (code !== 'my-payment') return;\n \n const customCheck = await myCustomValidation();\n if (!customCheck) {\n context.isValid = false;\n }\n}\n```\n\n---\n\n### `checkout/place-order`\n\nHandle custom payment method order placement.\n\n**Context:** `{ cartId, code, preventDefault }`\n\n- `preventDefault` - Set to `true` to skip the default order placement\n\n**Pattern:**\n\n- If it's not your payment method → early `return`\n- If it's yours → process payment, place order, set `context.preventDefault = true`\n\n**Example:**\n\n```javascript\n'checkout/place-order': async ({ context }) => {\n const { cartId, code } = context;\n \n if (code !== 'my-payment') return;\n \n context.preventDefault = true;\n \n await processPayment();\n await orderApi.placeOrder(cartId);\n}\n```\n\n---\n\n### `checkout/payment-response`\n\nHandle payment responses from external payment providers (e.g., PayPal, Klarna, iDEAL). Runs at the very start of checkout before any initialization.\n\n**Context:** `{ block, shouldExit }`\n\n- `block` - The checkout block element to render into\n- `shouldExit` - Set to `true` to skip normal checkout initialization (e.g., after showing order confirmation)\n\n**Pattern:**\n\n- Check URL params and session storage for pending payment data\n- If pending payment detected → process response, render result, set `shouldExit = true` on success\n- On error → clear block, set up error handler, leave `shouldExit = false` to continue with checkout\n\n**Example:**\n\n```javascript\n'checkout/payment-response': async ({ context }) => {\n const urlParams = new URLSearchParams(window.location.search);\n const redirectResult = urlParams.get('redirectResult');\n \n if (!redirectResult || !hasPendingPaymentData()) return;\n \n const success = await completePayment(context.block, redirectResult);\n context.shouldExit = success;\n}\n```\n\n---\n\n### `checkout/address-form-render`\n\nControl rendering of address forms (shipping/billing). Extensions can wrap the `render` function to augment behavior before/after rendering.\n\n**Context:** `{ container, addressType, formProps, render, getFormContainer, hasCartAddress, addressDataKey }`\n\n- `container` - DOM element where the form should be rendered\n- `addressType` - Either `'shipping'` or `'billing'`\n- `formProps` - Default props passed to the AddressForm container\n- `render` - Async function to render the address form. **Extensions can wrap this function** to add behavior before/after rendering. Accepts an optional props object passed to the `AddressForm` container\n- `getFormContainer` - Function that returns the form container instance (use `.setProps()` to update form values after render)\n- `hasCartAddress` - Whether the cart already has an address of this type\n- `addressDataKey` - Session storage key for address data\n\n**Pattern (Wrapper):**\n\nExtensions wrap `context.render` to control the rendering flow:\n\n```javascript\n'checkout/address-form-render': async ({ context }) => {\n const { addressType, hasCartAddress } = context;\n\n if (addressType !== 'shipping' || hasCartAddress) return;\n\n // 1. Capture original render\n const originalRender = context.render;\n\n // 2. Replace with wrapped version\n context.render = async (props) => {\n // A. Call original render (form appears in DOM)\n const result = await originalRender(props);\n\n // B. Now manipulate the rendered form\n // ... your customizations ...\n\n return result;\n };\n}\n```\n\n---\n\n### `checkout/shipping-methods-render`\n\nControl rendering of shipping methods. Extensions can wrap the `render` function to augment behavior before/after rendering.\n\n**Context:** `{ container, render }`\n\n- `container` - DOM element where shipping methods will be rendered\n- `render` - Async function to render shipping methods. **Extensions can wrap this function** to add behavior before/after rendering. Accepts an optional props object passed to the `ShippingMethods` container\n\n**Slot context for `ShippingMethodItem`:**\n\nWhen the `ShippingMethodItem` slot callback is invoked, it receives a context object (`ctx`) with:\n\n- `method` - The `ShippingMethod` object (includes `value`, `title`, `amount`, `carrier`, etc.)\n- `isSelected` - Whether this method is currently selected\n- `onSelect()` - Call to select this shipping method\n- `replaceWith(element)` - Replace the default item with your custom element\n- `onRender(callback)` - Register a callback invoked on re-renders (e.g., when selection changes). Receives the updated context\n\n**Pattern (Wrapper):**\n\nExtensions wrap `context.render` to control the rendering flow:\n\n```javascript\n'checkout/shipping-methods-render': async ({ context }) => {\n const originalRender = context.render;\n\n context.render = async (props) => {\n // A. Call original render (shipping methods appear in DOM)\n const result = await originalRender(props);\n\n // B. Now manipulate the rendered shipping methods\n // ... your customizations ...\n\n return result;\n };\n}\n```\n\n**Example:**\n\n```javascript\n'checkout/shipping-methods-render': async ({ context }) => {\n const originalRender = context.render;\n\n context.render = (props) => originalRender({\n ...props,\n slots: {\n ...props?.slots,\n ShippingMethodItem: (ctx) => {\n const card = document.createElement('label');\n card.className = 'my-shipping-card';\n card.innerHTML = `\n <input type=\"radio\" name=\"shipping-method\" value=\"${ctx.method.value}\"\n ${ctx.isSelected ? 'checked' : ''} />\n <span>${ctx.method.carrier.title} — ${ctx.method.title}</span>\n <span>$${ctx.method.amount.value.toFixed(2)}</span>\n `;\n\n ctx.replaceWith(card);\n\n card.querySelector('input').addEventListener('change', () => {\n ctx.onSelect();\n });\n\n ctx.onRender(({ isSelected }) => {\n card.classList.toggle('my-shipping-card--selected', isSelected);\n card.querySelector('input').checked = isSelected;\n });\n },\n },\n });\n}\n```\n\n---\n\n## Creating a New Payment Method Extension\n\nHere's a complete example for an external payment method:\n\n```javascript\nimport * as checkoutApi from '@dropins/storefront-checkout/api.js';\nimport * as orderApi from '@dropins/storefront-order/api.js';\n\nlet paymentInstance = null;\n\nexport default {\n id: 'my-payment',\n name: 'My Payment Gateway',\n \n externalScripts: [\n 'https://cdn.mypayment.com/sdk.js',\n ],\n externalStyles: [\n 'https://cdn.mypayment.com/sdk.css',\n ],\n \n hooks: {\n // Handles payment response and sets context.shouldExit\n 'checkout/payment-response': async ({ context }) => {\n const redirectResult = new URLSearchParams(window.location.search).get('result');\n if (!redirectResult) return;\n \n // Do not render the checkout and handle the payment response (e.g., render the checkout success page)\n context.shouldExit = true;\n },\n\n 'checkout/payment-methods': async ({ context }) => {\n context.paymentMethods['my_payment'] = {\n autoSync: false,\n render: async (ctx) => {\n const container = document.createElement('div');\n \n paymentInstance = await window.MyPayment.create({\n apiKey: 'your-api-key',\n container,\n });\n \n ctx.replaceHTML(container);\n },\n };\n },\n\n 'checkout/validate': async ({ context }) => {\n if (context.code !== 'my_payment') return;\n \n if (!paymentInstance?.isValid()) {\n context.isValid = false;\n }\n },\n\n 'checkout/place-order': async ({ context }) => {\n const { cartId, code } = context;\n \n if (code !== 'my_payment') return;\n \n context.preventDefault = true;\n\n const token = await paymentInstance.getToken();\n \n await checkoutApi.setPaymentMethod({\n code: 'my_payment',\n my_payment: { token },\n });\n\n await orderApi.placeOrder(cartId);\n },\n },\n};\n```\n\nThen enable it in `index.js`:\n\n```javascript\nimport myPaymentExtension from './my-payment/my-payment-extension.js';\n\nexport default [\n myPaymentExtension,\n];\n```\n\n## Best Practices\n\n1. **Use module-level variables for state** - Simple and scoped to your extension\n2. **Use early returns** - Skip your logic with `return` if not applicable\n3. **Signal via context mutation** - Use `context.isValid = false` for validation hooks\n4. **Use meaningful extension names** - Make them unique and descriptive\n5. **Handle errors gracefully** - Use try/catch and log errors\n6. **Test with multiple extensions** - Ensure your extension composes properly\n\n## Support\n\nFor issues or questions about the extension system, please refer to the main documentation.\n",
|
|
4
|
+
"extensions": [
|
|
5
|
+
{
|
|
6
|
+
"id": "address-autocomplete",
|
|
7
|
+
"name": "address-autocomplete",
|
|
8
|
+
"provider": "address",
|
|
9
|
+
"description": "address-autocomplete checkout extension using hooks: checkout/address-form-render",
|
|
10
|
+
"hooks": [
|
|
11
|
+
"checkout/address-form-render"
|
|
12
|
+
],
|
|
13
|
+
"files": {
|
|
14
|
+
"address-autocomplete-extension.js": "/* eslint-disable import/no-unresolved */\n\nimport { Input, provider as UI } from '@dropins/tools/components.js';\nimport { debounce } from '@dropins/tools/lib.js';\n\nconst MOCK_ADDRESSES = [\n {\n street: ['123 Main Street', 'Apt 4B'],\n city: 'New York',\n region: { regionCode: 'NY', regionId: 43 },\n postcode: '10001',\n countryCode: 'US',\n },\n {\n street: ['555 Market Street', 'Floor 3'],\n city: 'San Francisco',\n region: { regionCode: 'CA', regionId: 12 },\n postcode: '94102',\n countryCode: 'US',\n },\n];\n\nconst waitForElement = (container, selector) => new Promise((resolve) => {\n const element = container.querySelector(selector);\n if (element) {\n resolve(element);\n return;\n }\n\n const observer = new MutationObserver(() => {\n const el = container.querySelector(selector);\n if (el) {\n observer.disconnect();\n resolve(el);\n }\n });\n\n observer.observe(container, { childList: true, subtree: true });\n});\n\n// Fields to hide until address is selected or manual entry is clicked\nconst ADDRESS_FIELDS = ['street', 'street_multiline_2', 'country_code', 'region', 'city', 'postcode'];\n\nconst searchAddresses = (query) => {\n if (!query || query.length < 2) return [];\n\n const normalizedQuery = query.toLowerCase().trim();\n\n return MOCK_ADDRESSES.filter((address) => {\n const searchableText = [\n ...address.street,\n address.city,\n address.region.regionCode,\n address.postcode,\n ].join(' ').toLowerCase();\n\n return searchableText.includes(normalizedQuery);\n });\n};\n\nconst getFormValues = (form) => Object.fromEntries(new FormData(form));\n\nconst toggleAddressFields = (form, visible) => {\n ADDRESS_FIELDS.forEach((field) => {\n const el = form.querySelector(`[data-testid=\"account-address-form--${field}\"]`);\n el?.classList.toggle('address-autocomplete--hidden', !visible);\n });\n};\n\nconst createAutocompleteUI = (onSelect, onManualEntry) => {\n const wrapper = document.createElement('div');\n wrapper.className = 'address-autocomplete';\n\n const inputContainer = document.createElement('div');\n inputContainer.className = 'address-autocomplete__input-container';\n\n const dropdown = document.createElement('div');\n dropdown.className = 'address-autocomplete__dropdown';\n dropdown.style.display = 'none';\n\n const hideDropdown = () => {\n dropdown.style.display = 'none';\n };\n\n const showDropdown = (addresses) => {\n dropdown.innerHTML = '';\n\n addresses.forEach((address) => {\n const item = document.createElement('div');\n item.className = 'address-autocomplete__item';\n item.innerHTML = `\n <span>${address.street[0]}</span>\n <span class=\"address-autocomplete__item-location\">${address.city}, ${address.region.regionCode} ${address.postcode}</span>\n `;\n item.addEventListener('click', () => {\n onSelect(address);\n wrapper.remove();\n });\n dropdown.appendChild(item);\n });\n\n dropdown.style.display = addresses.length ? 'block' : 'none';\n };\n\n const debouncedSearch = debounce((value) => {\n const results = searchAddresses(value);\n showDropdown(results);\n }, 300);\n\n UI.render(Input, {\n name: 'address-autocomplete',\n placeholder: 'Start typing address',\n floatingLabel: 'Address',\n autocomplete: 'chrome-off',\n onValue: (value) => {\n if (!value || value.length < 2) {\n hideDropdown();\n return;\n }\n\n debouncedSearch(value);\n },\n })(inputContainer);\n\n const manualEntryLink = document.createElement('button');\n manualEntryLink.type = 'button';\n manualEntryLink.className = 'address-autocomplete__manual-entry';\n manualEntryLink.textContent = 'Enter address manually';\n manualEntryLink.addEventListener('click', () => {\n onManualEntry();\n wrapper.remove();\n });\n\n document.addEventListener('click', (e) => {\n if (!wrapper.contains(e.target)) hideDropdown();\n });\n\n wrapper.appendChild(inputContainer);\n wrapper.appendChild(dropdown);\n wrapper.appendChild(manualEntryLink);\n\n return wrapper;\n};\n\nconst extensionBasePath = new URL('.', import.meta.url).pathname;\n\nexport default {\n id: 'address-autocomplete',\n name: 'Address Autocomplete Extension',\n\n externalStyles: [\n `${extensionBasePath}address-autocomplete.css`,\n ],\n\n hooks: {\n 'checkout/address-form-render': async ({ context }) => {\n const {\n addressDataKey,\n addressType,\n container,\n getFormContainer,\n hasCartAddress,\n } = context;\n\n if (addressType !== 'shipping' || hasCartAddress) return;\n\n const originalRender = context.render;\n\n context.render = async (props) => {\n const result = await originalRender(props);\n\n const form = await waitForElement(container, 'form');\n\n toggleAddressFields(form, false);\n\n const handleAddressSelect = (address) => {\n sessionStorage.removeItem(addressDataKey);\n\n const currentValues = getFormValues(form);\n\n getFormContainer().setProps((prev) => ({\n ...prev,\n inputsDefaultValueSet: {\n ...currentValues,\n street: address.street?.[0] || '',\n streetMultiline_2: address.street?.[1] || '',\n city: address.city || '',\n region: address.region || {},\n postcode: address.postcode || '',\n countryCode: address.countryCode || 'US',\n },\n }));\n\n toggleAddressFields(form, true);\n };\n\n const handleManualEntry = () => {\n toggleAddressFields(form, true);\n };\n\n const autocompleteUI = createAutocompleteUI(handleAddressSelect, handleManualEntry);\n form.appendChild(autocompleteUI);\n\n return result;\n };\n },\n },\n};\n",
|
|
15
|
+
"README.md": "# Address Autocomplete Extension\n\nThis extension provides inline address autocomplete for the checkout shipping form. It hides address fields initially and shows an autocomplete input, allowing users to either search for an address or enter it manually.\n\n## How It Works\n\n1. When the checkout page loads for a guest user without an existing shipping address, the form renders with address fields hidden\n2. The user sees: First Name, Last Name, Phone Number, VAT Number fields, plus an autocomplete input and \"Enter address manually\" link\n3. As the user types in the autocomplete (minimum 2 characters), matching addresses appear in a dropdown (currently using dummy data - replace with a real address lookup service for production)\n4. When an address is selected OR \"Enter address manually\" is clicked, the hidden address fields are revealed\n5. If an address was selected, the fields are pre-populated with the selected address data\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `address-autocomplete-extension.js` | Main extension logic and hook handler |\n| `address-autocomplete.css` | Styles for autocomplete UI and field visibility |\n\n## Hook\n\nThis extension uses the `checkout/address-form-render` hook, wrapping the `render` function to add autocomplete behavior after the form renders.\n\nSee the [extensions README](../README.md#checkoutaddress-form-render) for full hook documentation.\n\n## Hidden Fields\n\nThe following fields are hidden until address selection or manual entry:\n- `street`\n- `street_multiline_2`\n- `country_code`\n- `region`\n- `city`\n- `postcode`\n\n## Customization\n\n### Replacing Mock Data with a Real API\n\nThe extension currently uses mock data for demonstration. To integrate with a real address lookup service, replace the `MOCK_ADDRESSES` array and `searchAddresses` function with your API integration:\n\n```javascript\n// Example with a hypothetical address API\nconst searchAddresses = async (query) => {\n if (!query || query.length < 3) return [];\n \n const response = await fetch(`/api/address-lookup?q=${encodeURIComponent(query)}`);\n const data = await response.json();\n \n // Transform API response to match expected format\n return data.suggestions.map(suggestion => ({\n street: [suggestion.line1, suggestion.line2].filter(Boolean),\n city: suggestion.city,\n region: { regionCode: suggestion.state, regionId: suggestion.regionId },\n postcode: suggestion.zip,\n countryCode: suggestion.country,\n }));\n};\n```\n\nThe extension already includes debouncing (300ms) to avoid excessive API calls.\n",
|
|
16
|
+
"address-autocomplete.css": ".address-autocomplete--hidden {\n display: none;\n}\n\n.address-autocomplete {\n position: relative;\n grid-column: span 2;\n}\n\n.address-autocomplete__input-container {\n position: relative;\n}\n\n.address-autocomplete__dropdown {\n position: absolute;\n left: 0;\n right: 0;\n background: var(--color-neutral-50);\n border: var(--shape-border-width-1) solid var(--color-neutral-400);\n border-radius: 0 0 var(--shape-border-radius-1) var(--shape-border-radius-1);\n box-shadow: var(--shape-shadow-2);\n z-index: 1000;\n max-height: 300px;\n overflow-y: auto;\n}\n\n.address-autocomplete__item {\n padding: var(--spacing-small) var(--spacing-medium);\n cursor: pointer;\n font: var(--type-body-2-default-font);\n color: var(--color-neutral-800);\n}\n\n.address-autocomplete__item:hover {\n background-color: var(--color-neutral-100);\n}\n\n.address-autocomplete__item-location {\n display: block;\n color: var(--color-neutral-600);\n}\n\n.address-autocomplete__manual-entry {\n display: inline-block;\n margin-top: var(--spacing-small);\n padding: 0;\n background: none;\n border: none;\n font: var(--type-body-2-default-font);\n color: var(--color-brand-500);\n cursor: pointer;\n text-decoration: underline;\n}\n\n.address-autocomplete__manual-entry:hover {\n color: var(--color-brand-700);\n}\n"
|
|
17
|
+
},
|
|
18
|
+
"sharedFiles": []
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "address-validation",
|
|
22
|
+
"name": "Address Validation Extension",
|
|
23
|
+
"provider": "address",
|
|
24
|
+
"description": "Address Validation Extension checkout extension using hooks: checkout/validate",
|
|
25
|
+
"hooks": [
|
|
26
|
+
"checkout/validate"
|
|
27
|
+
],
|
|
28
|
+
"files": {
|
|
29
|
+
"address-validation-extension.js": "/* eslint-disable no-console */\n/* eslint-disable import/no-unresolved */\n\n// Checkout Dropin\nimport AddressValidation from '@dropins/storefront-checkout/containers/AddressValidation.js';\nimport { render as CheckoutProvider } from '@dropins/storefront-checkout/render.js';\n\n// Tools\nimport { events } from '@dropins/tools/event-bus.js';\n\n// Block Utilities\nimport { showModal, removeModal } from '../../utils.js';\nimport { getContainer, CONTAINERS } from '../../containers.js';\nimport { SHIPPING_ADDRESS_DATA_KEY } from '../../constants.js';\n\n/**\n * Validates the shipping address against an address verification service.\n * Replace this stub with your actual address verification API call.\n *\n * @returns {Promise<Object|null>} - Returns a suggested address object\n */\nconst validateAddress = async () => {\n // ==========================================================================\n // TODO: Replace this stub with your actual address verification API call\n // ==========================================================================\n const shippingAddress = events.lastPayload('checkout/addresses/shipping').data;\n console.log('[Address validation] Current shipping address:', shippingAddress);\n\n return {\n city: 'Bainbridge Island',\n countryCode: 'US',\n postcode: '98110-2450',\n region: 'CA',\n street: ['123 Winslow Way E'],\n };\n};\n\n/**\n * Renders the AddressValidation container in a modal and returns a Promise\n * that resolves with the user's selection.\n *\n * @param {HTMLElement} container - DOM element to render into\n * @param {Object} suggestedAddress - The suggested address from validation\n * @returns {Promise<{selection: string, address: Object}>} - User's selection\n */\nconst renderAddressValidation = (container, suggestedAddress) => new Promise((resolve) => {\n CheckoutProvider.render(AddressValidation, {\n suggestedAddress,\n handleSelectedAddress: ({ selection, address }) => {\n resolve({ selection, address });\n },\n })(container);\n});\n\n// Get the base path for extension assets\nconst extensionBasePath = new URL('.', import.meta.url).pathname;\n\nexport default {\n id: 'address-validation',\n name: 'Address Validation Extension',\n\n externalStyles: [\n `${extensionBasePath}address-validation-extension.css`,\n ],\n\n hooks: {\n /**\n * VALIDATE HOOK: Validate shipping address before placing order\n *\n * This hook validates the shipping address before any payment processing.\n * If the address verification service returns a suggestion, a modal is\n * displayed allowing the shopper to choose between the original and\n * suggested addresses.\n *\n * - If user selects \"suggested\": Updates the form, sets isValid=false\n * so the user can review and click Place Order again.\n * - If user selects \"original\": Leaves isValid=true, order proceeds\n * to place-order hooks (payment processing).\n */\n 'checkout/validate': async ({ context }) => {\n try {\n const suggestion = await validateAddress();\n\n if (suggestion) {\n const container = document.createElement('div');\n container.classList.add('checkout-address-validation');\n\n await showModal(container);\n\n // Wait for user selection\n const { selection, address } = await renderAddressValidation(container, suggestion);\n\n removeModal();\n\n if (selection === 'suggested') {\n // Update the shipping form UI with the suggested address\n // The form's onChange handler will automatically update the cart\n const shippingForm = getContainer(CONTAINERS.SHIPPING_ADDRESS_FORM);\n sessionStorage.removeItem(SHIPPING_ADDRESS_DATA_KEY);\n shippingForm.setProps((prevProps) => ({\n ...prevProps,\n inputsDefaultValueSet: address,\n }));\n\n // Set isValid to false so the user can review the updated address\n // and click Place Order again\n context.isValid = false;\n }\n // If \"original\" selected, isValid remains true and order proceeds\n }\n // If no suggestion, isValid remains true and order proceeds\n } catch (error) {\n console.error('[Address Validation] Error:', error);\n throw error;\n }\n },\n },\n};\n",
|
|
30
|
+
"address-validation-extension.css": "/* Address Validation Extension Styles */\n.modal-content .checkout-address-validation {\n padding: var(--spacing-big);\n}\n"
|
|
31
|
+
},
|
|
32
|
+
"sharedFiles": []
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "adyen-bancontact",
|
|
36
|
+
"name": "Adyen Bancontact",
|
|
37
|
+
"provider": "adyen",
|
|
38
|
+
"description": "Adyen Bancontact checkout extension using hooks: checkout/payment-methods, checkout/validate, checkout/place-order",
|
|
39
|
+
"hooks": [
|
|
40
|
+
"checkout/payment-methods",
|
|
41
|
+
"checkout/validate",
|
|
42
|
+
"checkout/place-order"
|
|
43
|
+
],
|
|
44
|
+
"files": {
|
|
45
|
+
"adyen-bancontact-extension.js": "/* eslint-disable import/no-unresolved */\nimport { events } from '@dropins/tools/event-bus.js';\n\nimport {\n ADYEN_SDK_SCRIPT,\n ADYEN_SDK_STYLE,\n} from './constants.js';\nimport {\n fetchAdyenPaymentMethods,\n createAdyenCheckoutInstance,\n placeOrderWithAdyen,\n submitAdyenPaymentDetails,\n} from './utils.js';\nimport createModal from '../../../modal/modal.js';\n\nconst PAYMENT_METHOD_CODE = 'adyen_bcmc';\n\nlet adyenCard = null;\nlet currentOrderData = null;\nlet adyenCheckoutInstance = null;\nlet currentPaymentData = null;\nlet threeDSModal = null;\nlet threeDSModalContent = null;\n\nexport default {\n id: 'adyen-bancontact',\n name: 'Adyen Bancontact',\n\n externalScripts: [ADYEN_SDK_SCRIPT],\n externalStyles: [ADYEN_SDK_STYLE],\n\n hooks: {\n 'checkout/payment-methods': async ({ context }) => {\n // Disable vault for Bancontact\n context.paymentMethods.adyen_bcmc_vault = {\n enabled: false,\n };\n\n context.paymentMethods[PAYMENT_METHOD_CODE] = {\n autoSync: false,\n render: async (ctx) => {\n const container = document.createElement('div');\n container.className = 'adyen-bancontact-container';\n\n ctx.appendChild(container);\n\n ctx.onRender(async () => {\n if (container.hasChildNodes()) {\n return;\n }\n\n adyenCard = null;\n currentOrderData = null;\n adyenCheckoutInstance = null;\n currentPaymentData = null;\n\n try {\n // Use Card component instead of Bancontact (per Adyen docs)\n const { Card } = window.AdyenWeb;\n\n const { paymentMethodsResponse } = await fetchAdyenPaymentMethods(ctx.cartId);\n\n const checkout = await createAdyenCheckoutInstance(paymentMethodsResponse, {\n onError: (error) => {\n console.error('[Bancontact] Checkout error:', error);\n threeDSModal?.removeModal();\n events.emit('checkout/error', {\n message: error.message || 'Bancontact payment failed.',\n code: 'payment_error',\n });\n // eslint-disable-next-line no-underscore-dangle\n adyenCard?._orderPromise?.reject(error);\n },\n onSubmit: async (state, component) => {\n const additionalData = {\n stateData: JSON.stringify(state.data),\n // XXX: brand_code is not needed for Adyen v10 (2.4.8)\n brand_code: 'bcmc',\n };\n\n try {\n const paymentMethod = {\n code: PAYMENT_METHOD_CODE,\n adyen_additional_data: additionalData,\n };\n\n // Use placeOrderWithAdyen to get action for 3DS\n const result = await placeOrderWithAdyen(ctx.cartId, paymentMethod);\n currentOrderData = result?.orderV2;\n const adyenStatus = result?.order?.adyen_payment_status;\n\n if (adyenStatus?.action) {\n const action = JSON.parse(adyenStatus.action);\n\n // Store paymentData for later use in onAdditionalDetails\n if (action.paymentData) {\n currentPaymentData = action.paymentData;\n } else {\n // eslint-disable-next-line no-console\n console.warn('[Bancontact] ⚠️ No paymentData in action! Full action:', JSON.stringify(action, null, 2));\n }\n\n // Show modal for 3DS challenge\n if (!threeDSModal) {\n threeDSModalContent = document.createElement('div');\n threeDSModalContent.className = 'adyen-3ds-content';\n threeDSModal = await createModal([threeDSModalContent]);\n }\n\n threeDSModalContent.innerHTML = '';\n threeDSModal.showModal();\n\n // Hide checkout loaders\n document.querySelectorAll('.checkout--loading, .dropin-loader, .checkout__loader').forEach((el) => {\n el.style.display = 'none';\n el.classList.remove('checkout--loading');\n });\n\n // Mount action in modal - this will trigger the 3DS iframe\n adyenCheckoutInstance.createFromAction(action).mount(threeDSModalContent);\n\n // Note: When 3DS challenge completes, Adyen SDK will call onAdditionalDetails\n } else if (adyenStatus?.isFinal) {\n events.emit('order/placed', currentOrderData);\n events.emit('cart/reset', {});\n // eslint-disable-next-line no-underscore-dangle\n adyenCard._orderPromise?.resolve();\n }\n } catch (error) {\n // eslint-disable-next-line no-console\n console.error('[Bancontact] onSubmit error:', error);\n threeDSModal?.removeModal();\n // Clear stored payment data on error\n currentPaymentData = null;\n component.setStatus('ready');\n events.emit('checkout/error', {\n message: error.message || 'Bancontact payment failed.',\n code: 'payment_error',\n });\n // eslint-disable-next-line no-underscore-dangle\n adyenCard._orderPromise?.reject(error);\n }\n },\n onAdditionalDetails: async (state, component) => {\n try {\n // Extract the 3DS details from state.data.details\n // state.data structure is: { details: { threeDSResult: '...' } }\n const threeDSDetails = state.data.details || state.data;\n\n if (!currentPaymentData) {\n // eslint-disable-next-line no-console\n console.error('[Bancontact] ❌ currentPaymentData is null/undefined! Cannot proceed!');\n throw new Error('Payment data is missing. Please try again.');\n }\n\n // Merge 3DS details with paymentData\n const detailsWithPaymentData = {\n ...threeDSDetails,\n paymentData: currentPaymentData,\n };\n\n const response = await submitAdyenPaymentDetails(\n ctx.cartId,\n detailsWithPaymentData,\n currentOrderData?.number,\n );\n\n if (response?.isFinal) {\n if (response.resultCode === 'Authorised' || response.resultCode === 'Received') {\n // Hide modal AFTER successful payment\n threeDSModal?.removeModal();\n threeDSModal = null;\n threeDSModalContent = null;\n\n // Clear stored payment data\n currentPaymentData = null;\n\n events.emit('order/placed', currentOrderData);\n events.emit('cart/reset', {});\n // eslint-disable-next-line no-underscore-dangle\n adyenCard._orderPromise?.resolve();\n } else {\n throw new Error(`Payment ${response.resultCode || 'failed'}`);\n }\n } else {\n throw new Error('Payment verification incomplete');\n }\n } catch (error) {\n // eslint-disable-next-line no-console\n console.error('[Bancontact] onAdditionalDetails error:', error);\n threeDSModal?.removeModal();\n threeDSModal = null;\n threeDSModalContent = null;\n // Clear stored payment data on error\n currentPaymentData = null;\n if (component) component.setStatus('ready');\n events.emit('checkout/error', {\n message: error.message || 'Payment verification failed',\n code: 'payment_error',\n });\n // eslint-disable-next-line no-underscore-dangle\n adyenCard._orderPromise?.reject(error);\n }\n },\n });\n\n adyenCheckoutInstance = checkout;\n\n // Use Card component with bcmc brand only\n adyenCard = new Card(checkout, {\n showPayButton: false,\n hasHolderName: true,\n holderNameRequired: true,\n // Brand filter handled by backend payment methods\n });\n\n adyenCard.mount(container);\n } catch (error) {\n events.emit('checkout/error', {\n message: error.message || 'Bancontact payment failed. Please try again.',\n code: 'payment_error',\n });\n }\n });\n },\n };\n },\n\n 'checkout/validate': async ({ context }) => {\n if (context.code !== PAYMENT_METHOD_CODE) return;\n\n if (!adyenCard) {\n events.emit('checkout/error', {\n message: 'Bancontact form not loaded. Please refresh and try again.',\n code: 'payment_error',\n });\n context.isValid = false;\n return;\n }\n\n if (!adyenCard.state?.isValid) {\n adyenCard.showValidation?.();\n context.isValid = false;\n }\n },\n\n 'checkout/place-order': async ({ context }) => {\n if (context.code !== PAYMENT_METHOD_CODE) return;\n\n context.preventDefault = true;\n\n await new Promise((resolve, reject) => {\n // eslint-disable-next-line no-underscore-dangle\n adyenCard._orderPromise = { resolve, reject };\n adyenCard.submit();\n });\n },\n },\n};\n"
|
|
46
|
+
},
|
|
47
|
+
"sharedFiles": [
|
|
48
|
+
"adyen/constants.js",
|
|
49
|
+
"adyen/queries.js",
|
|
50
|
+
"adyen/utils.js"
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "adyen-blik",
|
|
55
|
+
"name": "Adyen BLIK",
|
|
56
|
+
"provider": "adyen",
|
|
57
|
+
"description": "Adyen BLIK checkout extension using hooks: checkout/payment-methods, checkout/validate, checkout/place-order",
|
|
58
|
+
"hooks": [
|
|
59
|
+
"checkout/payment-methods",
|
|
60
|
+
"checkout/validate",
|
|
61
|
+
"checkout/place-order"
|
|
62
|
+
],
|
|
63
|
+
"files": {
|
|
64
|
+
"adyen-blik-extension.js": "/* eslint-disable import/no-unresolved */\nimport { events } from '@dropins/tools/event-bus.js';\nimport * as orderApi from '@dropins/storefront-order/api.js';\n\nimport {\n ADYEN_SDK_SCRIPT,\n ADYEN_SDK_STYLE,\n} from './constants.js';\nimport {\n fetchAdyenPaymentMethods,\n createAdyenCheckoutInstance,\n getCartAmount,\n} from './utils.js';\n\nconst PAYMENT_METHOD_CODE = 'adyen_blik';\nconst ADYEN_PAYMENT_TYPE = 'blik';\n\nlet adyenBlik = null;\n\nexport default {\n id: 'adyen-blik',\n name: 'Adyen BLIK',\n\n externalScripts: [ADYEN_SDK_SCRIPT],\n externalStyles: [ADYEN_SDK_STYLE],\n\n hooks: {\n 'checkout/payment-methods': async ({ context }) => {\n context.paymentMethods.adyen_blik_vault = {\n enabled: false,\n };\n context.paymentMethods[PAYMENT_METHOD_CODE] = {\n autoSync: false,\n render: async (ctx) => {\n const container = document.createElement('div');\n container.className = 'adyen-blik-container';\n\n ctx.appendChild(container);\n\n ctx.onRender(async () => {\n if (container.hasChildNodes()) {\n return;\n }\n\n adyenBlik = null;\n\n try {\n const {\n paymentMethodsResponse,\n paymentMethodsExtraDetails,\n } = await fetchAdyenPaymentMethods(ctx.cartId);\n\n const paymentMethods = paymentMethodsResponse.paymentMethods || [];\n const hasBlikMethod = paymentMethods.some(\n (method) => method.type === ADYEN_PAYMENT_TYPE,\n );\n if (!hasBlikMethod) {\n const blikDetails = paymentMethodsExtraDetails.find(\n (method) => method.type === ADYEN_PAYMENT_TYPE,\n );\n if (blikDetails) {\n paymentMethodsResponse.paymentMethods = [\n ...paymentMethods,\n {\n type: ADYEN_PAYMENT_TYPE,\n name: 'BLIK',\n configuration: blikDetails.configuration,\n },\n ];\n }\n }\n const hasRenderableBlik = (\n paymentMethodsResponse.paymentMethods || []\n ).some((method) => method.type === ADYEN_PAYMENT_TYPE);\n if (!hasRenderableBlik) {\n return;\n }\n\n const checkout = await createAdyenCheckoutInstance(paymentMethodsResponse, {\n countryCode: 'PL',\n amount: getCartAmount(),\n onSubmit: async (state, component) => {\n const additionalData = {\n stateData: JSON.stringify(state.data),\n brand_code: ADYEN_PAYMENT_TYPE,\n };\n\n try {\n const paymentMethod = {\n code: PAYMENT_METHOD_CODE,\n adyen_additional_data: additionalData,\n };\n\n await orderApi.setPaymentMethodAndPlaceOrder(ctx.cartId, paymentMethod);\n\n component.setStatus('success');\n // eslint-disable-next-line no-underscore-dangle\n adyenBlik._orderPromise?.resolve();\n } catch (error) {\n console.error('[Adyen BLIK] Payment failed:', error);\n component.setStatus('ready');\n events.emit('checkout/error', {\n message: error.message || 'BLIK payment failed. Please try again.',\n code: 'payment_error',\n });\n // eslint-disable-next-line no-underscore-dangle\n adyenBlik._orderPromise?.reject(error);\n }\n },\n onError: (error) => {\n console.error('[Adyen BLIK] Error:', error);\n events.emit('checkout/error', {\n message: error.message || 'BLIK encountered an error.',\n code: 'payment_error',\n });\n },\n });\n\n const componentConfig = {\n showPayButton: false,\n };\n const { Blik } = window.AdyenWeb || {};\n adyenBlik = Blik\n ? new Blik(checkout, componentConfig)\n : checkout.create(ADYEN_PAYMENT_TYPE, componentConfig);\n adyenBlik.mount(container);\n } catch (error) {\n console.error('[Adyen BLIK] Failed to initialize:', error);\n }\n });\n },\n };\n },\n\n 'checkout/validate': async ({ context }) => {\n if (context.code !== PAYMENT_METHOD_CODE) return;\n\n if (!adyenBlik) {\n events.emit('checkout/error', {\n message: 'BLIK form not loaded. Please refresh and try again.',\n code: 'payment_error',\n });\n context.isValid = false;\n return;\n }\n\n if (!adyenBlik.state?.isValid) {\n adyenBlik.showValidation?.();\n context.isValid = false;\n }\n },\n\n 'checkout/place-order': async ({ context }) => {\n if (context.code !== PAYMENT_METHOD_CODE) return;\n\n context.preventDefault = true;\n\n await new Promise((resolve, reject) => {\n // eslint-disable-next-line no-underscore-dangle\n adyenBlik._orderPromise = { resolve, reject };\n adyenBlik.submit();\n });\n },\n },\n};\n"
|
|
65
|
+
},
|
|
66
|
+
"sharedFiles": [
|
|
67
|
+
"adyen/constants.js",
|
|
68
|
+
"adyen/queries.js",
|
|
69
|
+
"adyen/utils.js"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "adyen-cc",
|
|
74
|
+
"name": "Adyen Credit Card",
|
|
75
|
+
"provider": "adyen",
|
|
76
|
+
"description": "Adyen Credit Card checkout extension using hooks: checkout/payment-methods, checkout/validate, checkout/place-order",
|
|
77
|
+
"hooks": [
|
|
78
|
+
"checkout/payment-methods",
|
|
79
|
+
"checkout/validate",
|
|
80
|
+
"checkout/place-order"
|
|
81
|
+
],
|
|
82
|
+
"files": {
|
|
83
|
+
"adyen-cc-extension.js": "/* eslint-disable import/no-unresolved */\nimport { events } from '@dropins/tools/event-bus.js';\nimport * as orderApi from '@dropins/storefront-order/api.js';\n\nimport {\n ADYEN_SDK_SCRIPT,\n ADYEN_SDK_STYLE,\n} from './constants.js';\nimport {\n fetchAdyenPaymentMethods,\n createAdyenCheckoutInstance,\n} from './utils.js';\n\nconst PAYMENT_METHOD_CODE = 'adyen_cc';\n\nlet adyenCard = null;\n\nexport default {\n id: 'adyen-cc',\n name: 'Adyen Credit Card',\n\n externalScripts: [ADYEN_SDK_SCRIPT],\n externalStyles: [ADYEN_SDK_STYLE],\n\n hooks: {\n 'checkout/payment-methods': async ({ context }) => {\n context.paymentMethods[PAYMENT_METHOD_CODE] = {\n autoSync: false,\n render: async (ctx) => {\n const container = document.createElement('div');\n container.className = 'adyen-card-container';\n\n // Check if customer is logged in\n const currentCheckout = events.lastPayload('checkout/updated')\n || events.lastPayload('checkout/initialized');\n const isLoggedIn = !currentCheckout?.isGuest;\n\n ctx.appendChild(container);\n\n ctx.onRender(async () => {\n if (container.hasChildNodes()) {\n return;\n }\n\n adyenCard = null;\n\n try {\n const { Card } = window.AdyenWeb || {};\n\n const { paymentMethodsResponse } = await fetchAdyenPaymentMethods(ctx.cartId);\n\n const checkout = await createAdyenCheckoutInstance(paymentMethodsResponse, {\n onSubmit: async (state, component) => {\n const additionalData = {\n stateData: JSON.stringify(state.data),\n };\n\n try {\n const paymentMethod = {\n code: PAYMENT_METHOD_CODE,\n adyen_additional_data_cc: additionalData,\n };\n\n await orderApi.setPaymentMethodAndPlaceOrder(ctx.cartId, paymentMethod);\n\n // eslint-disable-next-line no-underscore-dangle\n adyenCard._orderPromise?.resolve();\n } catch (error) {\n console.error('[Adyen Credit Card] Payment failed:', error);\n component.setStatus('ready');\n events.emit('checkout/error', {\n message: error.message || 'Credit card payment failed. Please try again.',\n code: 'payment_error',\n });\n // eslint-disable-next-line no-underscore-dangle\n adyenCard._orderPromise?.reject(error);\n }\n },\n });\n\n adyenCard = new Card(checkout, {\n showPayButton: false,\n enableStoreDetails: isLoggedIn,\n });\n adyenCard.mount(container);\n } catch (error) {\n console.error('[Adyen Credit Card] Failed to initialize:', error);\n }\n });\n },\n };\n },\n\n 'checkout/validate': async ({ context }) => {\n if (context.code !== PAYMENT_METHOD_CODE) return;\n\n if (!adyenCard) {\n events.emit('checkout/error', {\n message: 'Credit card form not loaded. Please refresh and try again.',\n code: 'payment_error',\n });\n context.isValid = false;\n return;\n }\n\n if (!adyenCard.state?.isValid) {\n adyenCard.showValidation?.();\n context.isValid = false;\n }\n },\n\n 'checkout/place-order': async ({ context }) => {\n if (context.code !== PAYMENT_METHOD_CODE) return;\n\n context.preventDefault = true;\n\n await new Promise((resolve, reject) => {\n // eslint-disable-next-line no-underscore-dangle\n adyenCard._orderPromise = { resolve, reject };\n adyenCard.submit();\n });\n },\n },\n};\n"
|
|
84
|
+
},
|
|
85
|
+
"sharedFiles": [
|
|
86
|
+
"adyen/constants.js",
|
|
87
|
+
"adyen/queries.js",
|
|
88
|
+
"adyen/utils.js"
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"id": "adyen-cc-vault",
|
|
93
|
+
"name": "Adyen Credit Card Vault",
|
|
94
|
+
"provider": "adyen",
|
|
95
|
+
"description": "Adyen Credit Card Vault checkout extension using hooks: checkout/payment-methods, checkout/place-order",
|
|
96
|
+
"hooks": [
|
|
97
|
+
"checkout/payment-methods",
|
|
98
|
+
"checkout/place-order"
|
|
99
|
+
],
|
|
100
|
+
"files": {
|
|
101
|
+
"adyen-cc-vault-extension.js": "/* eslint-disable import/no-unresolved */\nimport { events } from '@dropins/tools/event-bus.js';\nimport * as orderApi from '@dropins/storefront-order/api.js';\nimport { fetchGraphQl } from '@dropins/storefront-checkout/api.js';\n\nimport {\n ADYEN_CLIENT_KEY,\n ADYEN_ENVIRONMENT,\n ADYEN_SDK_SCRIPT,\n ADYEN_SDK_STYLE,\n} from './constants.js';\nimport { fetchAdyenPaymentMethods } from './utils.js';\nimport { GET_CUSTOMER_PAYMENT_TOKENS } from './queries.js';\n\nconst PAYMENT_METHOD_CODE = 'adyen_cc_vault';\nconst BASE_PAYMENT_CODE = 'adyen_cc';\n\nlet selectedVaultComponent = null;\n\nexport default {\n id: 'adyen-cc-vault',\n name: 'Adyen Credit Card Vault',\n\n externalScripts: [ADYEN_SDK_SCRIPT],\n externalStyles: [ADYEN_SDK_STYLE],\n\n hooks: {\n 'checkout/payment-methods': async ({ context }) => {\n context.paymentMethods[PAYMENT_METHOD_CODE] = {\n autoSync: false,\n render: async (ctx) => {\n // Create container for stored credit card payment\n const $cardVaultContainer = document.createElement('div');\n $cardVaultContainer.className = 'adyen-card-vault-container';\n\n // Append the container to the slot\n ctx.appendChild($cardVaultContainer);\n\n // Initialize Adyen stored card payment each time the slot renders\n ctx.onRender(async () => {\n // Check if already mounted\n if ($cardVaultContainer.hasChildNodes()) {\n return;\n }\n\n // Reset selected component\n selectedVaultComponent = null;\n\n try {\n // Load Adyen SDK\n const { AdyenCheckout, Card } = window.AdyenWeb || {};\n\n if (!AdyenCheckout) {\n console.error('[Adyen CC Vault] AdyenCheckout not available');\n return;\n }\n\n // Fetch Adyen payment methods including stored payment methods\n const { paymentMethodsResponse } = await fetchAdyenPaymentMethods(ctx.cartId);\n\n // Find ALL stored card payment methods (scheme type)\n const storedCardMethods = paymentMethodsResponse.storedPaymentMethods?.filter(\n (method) => method.type === 'scheme',\n ) || [];\n\n if (storedCardMethods.length === 0) {\n console.warn('[Adyen CC Vault] No stored card payment methods found');\n return;\n }\n\n // Fetch Adobe Commerce vault tokens to get the correct public_hash\n let vaultTokens = [];\n try {\n const vaultResponse = await fetchGraphQl(GET_CUSTOMER_PAYMENT_TOKENS, {});\n vaultTokens = vaultResponse.data?.customerPaymentTokens?.items || [];\n } catch (error) {\n console.error('[Adyen CC Vault] Failed to fetch vault tokens:', error);\n return;\n }\n\n // Match each stored card with its vault token\n const matchedCards = storedCardMethods.map((storedCard) => {\n const vaultToken = vaultTokens.find((token) => {\n // Vault tokens are stored with base payment method code 'adyen_cc'\n if (token.payment_method_code !== BASE_PAYMENT_CODE) {\n return false;\n }\n\n // Parse the details JSON to match with Adyen stored payment method\n try {\n const details = JSON.parse(token.details);\n // Match by last 4 digits and card type/brand\n const maskedCCMatch = details.maskedCC === storedCard.lastFour;\n const typeMatch = details.type?.toLowerCase()\n === storedCard.brand?.toLowerCase();\n return maskedCCMatch && typeMatch;\n } catch (e) {\n return false;\n }\n });\n\n return {\n storedCard,\n vaultToken,\n };\n }).filter((match) => match.vaultToken);\n\n if (matchedCards.length === 0) {\n console.warn('[Adyen CC Vault] No stored cards with matching vault tokens found');\n return;\n }\n\n // Create Adyen checkout instance (shared for all cards)\n const checkout = await AdyenCheckout({\n clientKey: ADYEN_CLIENT_KEY,\n locale: 'en_US',\n environment: ADYEN_ENVIRONMENT,\n countryCode: 'US',\n paymentMethodsResponse,\n onSubmit: async (state, component) => {\n const selectedCard = matchedCards.find(\n (card) => card.storedCard.id === state.data.paymentMethod.storedPaymentMethodId,\n );\n\n if (!selectedCard) {\n console.error('[Adyen CC Vault] No matching vault token found');\n component.setStatus('ready');\n events.emit('checkout/error', {\n message: 'Payment configuration error',\n code: 'payment_error',\n });\n return;\n }\n\n const additionalData = {\n stateData: JSON.stringify(state.data),\n public_hash: selectedCard.vaultToken.public_hash,\n };\n\n try {\n const paymentMethod = {\n code: PAYMENT_METHOD_CODE,\n adyen_additional_data_cc: additionalData,\n };\n\n await orderApi.setPaymentMethodAndPlaceOrder(ctx.cartId, paymentMethod);\n\n component.setStatus('success');\n // eslint-disable-next-line no-underscore-dangle\n selectedVaultComponent._orderPromise?.resolve();\n } catch (error) {\n console.error('[Adyen CC Vault] Payment failed:', error);\n component.setStatus('ready');\n // eslint-disable-next-line no-underscore-dangle\n selectedVaultComponent._orderPromise?.reject(error);\n }\n },\n onError: (error) => {\n console.error('[Adyen CC Vault] Error:', error);\n },\n });\n\n // Track selected card and mounted component\n let selectedCardIndex = 0;\n let mountedCardComponent = null;\n\n // Create main container with accordion style\n const $mainContainer = document.createElement('div');\n $mainContainer.className = 'adyen-stored-cards-accordion';\n $mainContainer.style.cssText = 'display: flex; flex-direction: column; gap: 0;';\n\n // Function to mount the selected card's payment component\n const mountCardPaymentForm = (cardContainer, index) => {\n const { storedCard } = matchedCards[index];\n\n // Find or create payment form container for this card\n let $formContainer = cardContainer.querySelector('.card-payment-form');\n if (!$formContainer) {\n $formContainer = document.createElement('div');\n $formContainer.className = 'card-payment-form';\n $formContainer.style.cssText = `\n padding: 16px;\n border-top: 1px solid #e0e0e0;\n background: #fafafa;\n `;\n cardContainer.appendChild($formContainer);\n } else {\n $formContainer.innerHTML = '';\n }\n\n // Create Card component with stored payment method\n const cardComponent = new Card(checkout, {\n ...storedCard,\n showPayButton: false,\n });\n\n cardComponent.mount($formContainer);\n return cardComponent;\n };\n\n // Render each card as an accordion item\n matchedCards.forEach((matchedCard, index) => {\n const { storedCard } = matchedCard;\n\n // Create accordion item container\n const $accordionItem = document.createElement('div');\n $accordionItem.className = 'adyen-stored-card-accordion-item';\n $accordionItem.style.cssText = `\n border: 1px solid #e0e0e0;\n border-radius: 8px;\n margin-bottom: 8px;\n overflow: hidden;\n transition: all 0.3s ease;\n `;\n\n // Create card header (always visible)\n const $cardHeader = document.createElement('div');\n $cardHeader.className = 'card-accordion-header';\n $cardHeader.style.cssText = `\n padding: 16px;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n background: ${index === selectedCardIndex ? '#f0f7ff' : 'white'};\n border-left: 4px solid ${index === selectedCardIndex ? '#0066cc' : 'transparent'};\n transition: all 0.2s ease;\n `;\n\n const cardBrand = storedCard.brand || 'Card';\n const cardNumber = `${cardBrand} •••• ${storedCard.lastFour}`;\n const cardExpiry = `${storedCard.expiryMonth}/${storedCard.expiryYear}`;\n const holderInfo = storedCard.holderName\n ? ` • ${storedCard.holderName}`\n : '';\n\n $cardHeader.innerHTML = `\n <input\n type=\"radio\"\n name=\"stored-card-selection\"\n value=\"${index}\"\n ${index === selectedCardIndex ? 'checked' : ''}\n style=\"width: 18px; height: 18px; cursor: pointer; margin: 0;\"\n >\n <div style=\"flex: 1; min-width: 0;\">\n <div style=\"display: flex; justify-content: space-between; align-items: center; gap: 8px;\">\n <span style=\"text-transform: capitalize; font-weight: 500; font-size: 15px;\">\n ${cardNumber}\n </span>\n <span style=\"font-size: 13px; color: #666; white-space: nowrap;\">\n ${cardExpiry}${holderInfo}\n </span>\n </div>\n </div>\n `;\n\n // Handle card selection (accordion behavior)\n $cardHeader.addEventListener('click', (e) => {\n e.preventDefault();\n\n // If clicking the already selected card, do nothing (keep it expanded)\n if (selectedCardIndex === index) {\n return;\n }\n\n // Collapse previously selected card\n const $previousItem = $mainContainer.children[selectedCardIndex];\n if ($previousItem) {\n const $prevForm = $previousItem.querySelector('.card-payment-form');\n if ($prevForm) {\n $prevForm.remove();\n }\n const $prevHeader = $previousItem.querySelector('.card-accordion-header');\n if ($prevHeader) {\n $prevHeader.style.background = 'white';\n $prevHeader.style.borderLeft = '4px solid transparent';\n const $prevRadio = $prevHeader.querySelector('input[type=\"radio\"]');\n if ($prevRadio) $prevRadio.checked = false;\n }\n }\n\n // Unmount previous component\n if (mountedCardComponent) {\n try {\n mountedCardComponent.unmount();\n } catch (err) {\n // Component may already be unmounted\n }\n }\n\n // Update selected index\n selectedCardIndex = index;\n\n // Expand current card\n $cardHeader.style.background = '#f0f7ff';\n $cardHeader.style.borderLeft = '4px solid #0066cc';\n const $radio = $cardHeader.querySelector('input[type=\"radio\"]');\n if ($radio) $radio.checked = true;\n\n // Mount payment form for selected card\n mountedCardComponent = mountCardPaymentForm($accordionItem, index);\n\n // Update the global selected component\n selectedVaultComponent = mountedCardComponent;\n });\n\n // Add hover effect\n $cardHeader.addEventListener('mouseenter', () => {\n if (selectedCardIndex !== index) {\n $cardHeader.style.background = '#f9f9f9';\n }\n });\n $cardHeader.addEventListener('mouseleave', () => {\n if (selectedCardIndex !== index) {\n $cardHeader.style.background = 'white';\n }\n });\n\n $accordionItem.appendChild($cardHeader);\n $mainContainer.appendChild($accordionItem);\n });\n\n $cardVaultContainer.appendChild($mainContainer);\n\n // Mount the first card's payment form by default\n const $firstItem = $mainContainer.children[0];\n if ($firstItem) {\n mountedCardComponent = mountCardPaymentForm($firstItem, 0);\n selectedVaultComponent = mountedCardComponent;\n }\n } catch (error) {\n console.error('[Adyen CC Vault] Failed to initialize card vault payment:', error);\n }\n });\n },\n };\n },\n\n 'checkout/place-order': async ({ context }) => {\n const { code } = context;\n\n if (code !== PAYMENT_METHOD_CODE) return;\n\n context.preventDefault = true;\n\n if (!selectedVaultComponent) {\n console.error('[Adyen CC Vault] Card not rendered or selected');\n events.emit('checkout/error', {\n message: 'Please select a saved card',\n code: 'payment_error',\n });\n return;\n }\n\n if (!selectedVaultComponent.state?.isValid) {\n selectedVaultComponent.showValidation?.();\n return;\n }\n\n await new Promise((resolve, reject) => {\n // eslint-disable-next-line no-underscore-dangle\n selectedVaultComponent._orderPromise = { resolve, reject };\n selectedVaultComponent.submit();\n });\n },\n },\n};\n"
|
|
102
|
+
},
|
|
103
|
+
"sharedFiles": [
|
|
104
|
+
"adyen/constants.js",
|
|
105
|
+
"adyen/queries.js",
|
|
106
|
+
"adyen/utils.js"
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"id": "adyen-googlepay",
|
|
111
|
+
"name": "Adyen Google Pay",
|
|
112
|
+
"provider": "adyen",
|
|
113
|
+
"description": "Adyen Google Pay checkout extension using hooks: checkout/payment-methods",
|
|
114
|
+
"hooks": [
|
|
115
|
+
"checkout/payment-methods"
|
|
116
|
+
],
|
|
117
|
+
"files": {
|
|
118
|
+
"adyen-googlepay-extension.js": "/* eslint-disable import/no-unresolved */\nimport { events } from '@dropins/tools/event-bus.js';\nimport * as orderApi from '@dropins/storefront-order/api.js';\nimport { fetchGraphQl } from '@dropins/storefront-checkout/api.js';\n\nimport { GET_ADYEN_PAYMENT_METHODS } from './queries.js';\nimport {\n ADYEN_SDK_SCRIPT,\n ADYEN_SDK_STYLE,\n} from './constants.js';\nimport {\n registerHideDefaultPlaceOrder,\n createAdyenCheckoutInstance,\n getCartAmount,\n validateCheckoutForms,\n} from './utils.js';\n\nconst PAYMENT_METHOD_CODE = 'adyen_googlepay';\n\nlet adyenGooglePay = null;\n\n// Google Pay needs special handling for brands, so we keep this local\nasync function fetchGooglePayMethods(cartId) {\n const response = await fetchGraphQl(GET_ADYEN_PAYMENT_METHODS, {\n variables: { cartId },\n });\n\n if (response.errors?.length) {\n throw new Error(response.errors[0].message);\n }\n\n const data = response.data?.adyenPaymentMethods;\n if (!data) {\n throw new Error('No payment methods data returned');\n }\n\n const paymentMethodsResponse = data.paymentMethodsResponse || { paymentMethods: [] };\n const googlepayMethod = paymentMethodsResponse.paymentMethods.find(\n (method) => method.type === 'googlepay',\n );\n if (googlepayMethod) {\n googlepayMethod.brands = ['googlepay'];\n }\n\n return {\n paymentMethodsExtraDetails: data.paymentMethodsExtraDetails || [],\n paymentMethodsResponse,\n };\n}\n\nregisterHideDefaultPlaceOrder(PAYMENT_METHOD_CODE);\n\nexport default {\n id: 'adyen-googlepay',\n name: 'Adyen Google Pay',\n\n externalScripts: [ADYEN_SDK_SCRIPT],\n externalStyles: [ADYEN_SDK_STYLE],\n\n hooks: {\n 'checkout/payment-methods': async ({ context }) => {\n context.paymentMethods.adyen_googlepay_vault = {\n enabled: false,\n };\n context.paymentMethods[PAYMENT_METHOD_CODE] = {\n autoSync: false,\n render: async (ctx) => {\n const container = document.createElement('div');\n container.className = 'adyen-googlepay-container';\n\n ctx.appendChild(container);\n\n ctx.onRender(async () => {\n if (container.hasChildNodes()) {\n return;\n }\n\n adyenGooglePay = null;\n\n try {\n const { GooglePay } = window.AdyenWeb || {};\n\n const { paymentMethodsResponse } = await fetchGooglePayMethods(ctx.cartId);\n\n const checkout = await createAdyenCheckoutInstance(paymentMethodsResponse, {\n amount: getCartAmount(),\n onSubmit: async (state, _component, actions) => {\n try {\n const additionalData = {\n stateData: JSON.stringify(state.data),\n brand_code: 'googlepay',\n };\n const paymentMethod = {\n code: PAYMENT_METHOD_CODE,\n adyen_additional_data: additionalData,\n };\n\n await orderApi.setPaymentMethodAndPlaceOrder(ctx.cartId, paymentMethod);\n\n actions.resolve({\n resultCode: 'Authorised',\n });\n } catch (error) {\n console.error('[Adyen Google Pay] Payment failed:', error);\n events.emit('checkout/error', {\n message: error.message || 'Google Pay payment failed. Please try again.',\n code: 'payment_error',\n });\n actions.reject();\n }\n },\n onError: (error) => {\n console.error('[Adyen Google Pay] Error:', error);\n events.emit('checkout/error', {\n message: error.message || 'Google Pay encountered an error. Please try again.',\n code: 'payment_error',\n });\n },\n });\n\n adyenGooglePay = new GooglePay(checkout, {\n buttonColor: 'black',\n buttonType: 'pay',\n buttonSizeMode: 'fill',\n allowedCardNetworks: ['AMEX', 'DISCOVER', 'MASTERCARD', 'VISA'],\n allowCreditCards: true,\n allowPrepaidCards: true,\n billingAddressRequired: false,\n onClick: (resolve) => {\n if (!validateCheckoutForms()) {\n return;\n }\n\n resolve();\n },\n });\n\n adyenGooglePay.mount(container);\n } catch (error) {\n console.error('[Adyen Google Pay] Failed to initialize:', error);\n }\n });\n },\n };\n },\n },\n};\n"
|
|
119
|
+
},
|
|
120
|
+
"sharedFiles": [
|
|
121
|
+
"adyen/constants.js",
|
|
122
|
+
"adyen/queries.js",
|
|
123
|
+
"adyen/utils.js"
|
|
124
|
+
]
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"id": "adyen-ideal",
|
|
128
|
+
"name": "Adyen iDEAL",
|
|
129
|
+
"provider": "adyen",
|
|
130
|
+
"description": "Adyen iDEAL checkout extension using hooks: checkout/payment-response, checkout/payment-methods, checkout/place-order",
|
|
131
|
+
"hooks": [
|
|
132
|
+
"checkout/payment-response",
|
|
133
|
+
"checkout/payment-methods",
|
|
134
|
+
"checkout/place-order"
|
|
135
|
+
],
|
|
136
|
+
"files": {
|
|
137
|
+
"adyen-ideal-extension.js": "/* eslint-disable import/no-unresolved */\nimport {\n createAdyenSessionKeys,\n handlePaymentResponse,\n handleExternalPlaceOrder,\n} from './utils.js';\n\nconst PAYMENT_METHOD_CODE = 'adyen_ideal';\nconst ADYEN_PAYMENT_TYPE = 'ideal';\nconst ADYEN_SESSION_KEYS = createAdyenSessionKeys(PAYMENT_METHOD_CODE);\n\nexport default {\n id: 'adyen-ideal',\n name: 'Adyen iDEAL',\n\n hooks: {\n 'checkout/payment-response': async ({ context }) => {\n await handlePaymentResponse(context, {\n sessionKeys: ADYEN_SESSION_KEYS,\n errorPrefix: '[Adyen iDEAL]',\n defaultErrorMessage: 'iDEAL payment failed. Please try again.',\n });\n },\n\n 'checkout/payment-methods': async ({ context }) => {\n context.paymentMethods.adyen_ideal_vault = {\n enabled: false,\n };\n context.paymentMethods[PAYMENT_METHOD_CODE] = {\n autoSync: false,\n };\n },\n\n 'checkout/place-order': async ({ context }) => {\n await handleExternalPlaceOrder(context, {\n paymentMethodCode: PAYMENT_METHOD_CODE,\n adyenPaymentType: ADYEN_PAYMENT_TYPE,\n sessionKeys: ADYEN_SESSION_KEYS,\n errorPrefix: '[Adyen iDEAL]',\n });\n },\n },\n};\n"
|
|
138
|
+
},
|
|
139
|
+
"sharedFiles": [
|
|
140
|
+
"adyen/constants.js",
|
|
141
|
+
"adyen/queries.js",
|
|
142
|
+
"adyen/utils.js"
|
|
143
|
+
]
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"id": "adyen-klarna-account",
|
|
147
|
+
"name": "Adyen Klarna Pay Over Time",
|
|
148
|
+
"provider": "adyen",
|
|
149
|
+
"description": "Adyen Klarna Pay Over Time checkout extension using hooks: checkout/payment-response, checkout/payment-methods, checkout/place-order",
|
|
150
|
+
"hooks": [
|
|
151
|
+
"checkout/payment-response",
|
|
152
|
+
"checkout/payment-methods",
|
|
153
|
+
"checkout/place-order"
|
|
154
|
+
],
|
|
155
|
+
"files": {
|
|
156
|
+
"adyen-klarna-account-extension.js": "/* eslint-disable import/no-unresolved */\nimport {\n createAdyenSessionKeys,\n handlePaymentResponse,\n handleExternalPlaceOrder,\n} from './utils.js';\n\nconst PAYMENT_METHOD_CODE = 'adyen_klarna_account';\nconst ADYEN_PAYMENT_TYPE = 'klarna_account';\nconst ADYEN_SESSION_KEYS = createAdyenSessionKeys(PAYMENT_METHOD_CODE);\n\nexport default {\n id: 'adyen-klarna-account',\n name: 'Adyen Klarna Pay Over Time',\n\n hooks: {\n 'checkout/payment-response': async ({ context }) => {\n await handlePaymentResponse(context, {\n sessionKeys: ADYEN_SESSION_KEYS,\n errorPrefix: '[Adyen Klarna Pay Over Time]',\n defaultErrorMessage: 'Klarna payment failed. Please try again.',\n });\n },\n\n 'checkout/payment-methods': async ({ context }) => {\n context.paymentMethods.adyen_klarna_account_vault = {\n enabled: false,\n };\n context.paymentMethods[PAYMENT_METHOD_CODE] = {\n autoSync: false,\n };\n },\n\n 'checkout/place-order': async ({ context }) => {\n await handleExternalPlaceOrder(context, {\n paymentMethodCode: PAYMENT_METHOD_CODE,\n adyenPaymentType: ADYEN_PAYMENT_TYPE,\n sessionKeys: ADYEN_SESSION_KEYS,\n errorPrefix: '[Adyen Klarna Pay Over Time]',\n });\n },\n },\n};\n"
|
|
157
|
+
},
|
|
158
|
+
"sharedFiles": [
|
|
159
|
+
"adyen/constants.js",
|
|
160
|
+
"adyen/queries.js",
|
|
161
|
+
"adyen/utils.js"
|
|
162
|
+
]
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"id": "adyen-klarna",
|
|
166
|
+
"name": "Adyen Klarna",
|
|
167
|
+
"provider": "adyen",
|
|
168
|
+
"description": "Adyen Klarna checkout extension using hooks: checkout/payment-response, checkout/payment-methods, checkout/place-order",
|
|
169
|
+
"hooks": [
|
|
170
|
+
"checkout/payment-response",
|
|
171
|
+
"checkout/payment-methods",
|
|
172
|
+
"checkout/place-order"
|
|
173
|
+
],
|
|
174
|
+
"files": {
|
|
175
|
+
"adyen-klarna-extension.js": "/* eslint-disable import/no-unresolved */\nimport {\n createAdyenSessionKeys,\n handlePaymentResponse,\n handleExternalPlaceOrder,\n} from './utils.js';\n\nconst PAYMENT_METHOD_CODE = 'adyen_klarna';\nconst ADYEN_PAYMENT_TYPE = 'klarna';\nconst ADYEN_SESSION_KEYS = createAdyenSessionKeys(PAYMENT_METHOD_CODE);\n\nexport default {\n id: 'adyen-klarna',\n name: 'Adyen Klarna',\n\n hooks: {\n 'checkout/payment-response': async ({ context }) => {\n await handlePaymentResponse(context, {\n sessionKeys: ADYEN_SESSION_KEYS,\n errorPrefix: '[Adyen Klarna]',\n defaultErrorMessage: 'Klarna payment failed. Please try again.',\n });\n },\n\n 'checkout/payment-methods': async ({ context }) => {\n context.paymentMethods.adyen_klarna_vault = {\n enabled: false,\n };\n context.paymentMethods[PAYMENT_METHOD_CODE] = {\n autoSync: false,\n };\n },\n\n 'checkout/place-order': async ({ context }) => {\n await handleExternalPlaceOrder(context, {\n paymentMethodCode: PAYMENT_METHOD_CODE,\n adyenPaymentType: ADYEN_PAYMENT_TYPE,\n sessionKeys: ADYEN_SESSION_KEYS,\n errorPrefix: '[Adyen Klarna]',\n });\n },\n },\n};\n"
|
|
176
|
+
},
|
|
177
|
+
"sharedFiles": [
|
|
178
|
+
"adyen/constants.js",
|
|
179
|
+
"adyen/queries.js",
|
|
180
|
+
"adyen/utils.js"
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"id": "adyen-paypal",
|
|
185
|
+
"name": "Adyen PayPal",
|
|
186
|
+
"provider": "adyen",
|
|
187
|
+
"description": "Adyen PayPal checkout extension using hooks: checkout/payment-response, checkout/payment-methods",
|
|
188
|
+
"hooks": [
|
|
189
|
+
"checkout/payment-response",
|
|
190
|
+
"checkout/payment-methods"
|
|
191
|
+
],
|
|
192
|
+
"files": {
|
|
193
|
+
"adyen-paypal-extension.js": "/* eslint-disable import/no-unresolved */\nimport { events } from '@dropins/tools/event-bus.js';\n\nimport {\n ADYEN_SDK_SCRIPT,\n ADYEN_SDK_STYLE,\n} from './constants.js';\nimport {\n createAdyenSessionKeys,\n fetchAdyenPaymentMethods,\n initiateExternalPayment,\n handlePaymentResponse,\n registerHideDefaultPlaceOrder,\n createAdyenCheckoutInstance,\n getCartAmount,\n validateCheckoutForms,\n} from './utils.js';\n\nconst PAYMENT_METHOD_CODE = 'adyen_paypal';\nconst ADYEN_PAYMENT_TYPE = 'paypal';\nconst ADYEN_SESSION_KEYS = createAdyenSessionKeys('adyen_paypal');\n\nlet adyenPayPal = null;\n\nregisterHideDefaultPlaceOrder(PAYMENT_METHOD_CODE);\n\nexport default {\n id: 'adyen-paypal',\n name: 'Adyen PayPal',\n\n externalScripts: [ADYEN_SDK_SCRIPT],\n externalStyles: [ADYEN_SDK_STYLE],\n\n hooks: {\n 'checkout/payment-response': async ({ context }) => {\n await handlePaymentResponse(context, {\n sessionKeys: ADYEN_SESSION_KEYS,\n errorPrefix: '[Adyen PayPal]',\n defaultErrorMessage: 'PayPal payment failed. Please try again.',\n });\n },\n\n 'checkout/payment-methods': async ({ context }) => {\n context.paymentMethods.adyen_paypal_vault = {\n enabled: false,\n };\n context.paymentMethods[PAYMENT_METHOD_CODE] = {\n autoSync: false,\n render: async (ctx) => {\n const container = document.createElement('div');\n container.className = 'adyen-paypal-container';\n\n ctx.appendChild(container);\n\n ctx.onRender(async () => {\n if (container.hasChildNodes()) {\n return;\n }\n\n adyenPayPal = null;\n\n try {\n const { PayPal } = window.AdyenWeb || {};\n\n const { paymentMethodsResponse } = await fetchAdyenPaymentMethods(ctx.cartId);\n\n const checkout = await createAdyenCheckoutInstance(paymentMethodsResponse, {\n amount: getCartAmount(),\n });\n\n adyenPayPal = new PayPal(checkout, {\n style: {\n layout: 'vertical',\n color: 'gold',\n shape: 'rect',\n label: 'paypal',\n height: 48,\n },\n blockPayPalCreditButton: false,\n blockPayPalPayLaterButton: false,\n onClick: async (data, actions) => {\n if (!validateCheckoutForms()) {\n return actions.reject();\n }\n\n try {\n const result = await initiateExternalPayment(ctx.cartId, {\n paymentMethodCode: PAYMENT_METHOD_CODE,\n adyenPaymentType: ADYEN_PAYMENT_TYPE,\n sessionKeys: ADYEN_SESSION_KEYS,\n });\n\n if (result?.isRedirecting) {\n // Keep the button in loading state while redirecting\n return new Promise(() => {});\n }\n } catch (error) {\n console.error('[Adyen PayPal] Payment failed:', error);\n events.emit('checkout/error', {\n message: error.message || 'PayPal payment failed',\n code: 'payment_error',\n });\n }\n\n return actions.reject();\n },\n onCancel: () => {},\n onError: (error) => {\n console.error('[Adyen PayPal] Error:', error);\n },\n });\n\n adyenPayPal.mount(container);\n } catch (error) {\n console.error('[Adyen PayPal] Failed to initialize:', error);\n }\n });\n },\n };\n },\n },\n};\n"
|
|
194
|
+
},
|
|
195
|
+
"sharedFiles": [
|
|
196
|
+
"adyen/constants.js",
|
|
197
|
+
"adyen/queries.js",
|
|
198
|
+
"adyen/utils.js"
|
|
199
|
+
]
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"id": "bopis",
|
|
203
|
+
"name": "Acme - Barton Springs",
|
|
204
|
+
"provider": "bopis",
|
|
205
|
+
"description": "Acme - Barton Springs checkout extension using hooks: checkout/address-form-render, checkout/validate",
|
|
206
|
+
"hooks": [
|
|
207
|
+
"checkout/address-form-render",
|
|
208
|
+
"checkout/validate"
|
|
209
|
+
],
|
|
210
|
+
"files": {
|
|
211
|
+
"bopis-extension.js": "/* eslint-disable import/no-unresolved */\n\nimport * as checkoutApi from '@dropins/storefront-checkout/api.js';\nimport { ToggleButton, RadioButton, provider as UI } from '@dropins/tools/components.js';\n\nconst MOCK_PICKUP_LOCATIONS = [\n { name: 'Acme - Barton Springs', pickup_location_code: 'ACME_BARTON' },\n { name: 'Acme - Round Rock', pickup_location_code: 'ACME_ROUND_ROCK' },\n { name: 'Acme - Mountain Galleria', pickup_location_code: 'ACME_MOUNTAIN' },\n];\n\n// TODO: Replace with real GraphQL query:\n// const fetchPickupLocations = async () => checkoutApi.fetchGraphQl(`\n// query pickupLocations {\n// pickupLocations {\n// items { name, pickup_location_code }\n// }\n// }\n// `, { method: 'GET', cache: 'no-cache' })\n// .then((res) => res.data.pickupLocations.items);\n\nconst fetchPickupLocations = async () => MOCK_PICKUP_LOCATIONS;\n\nconst PICKUP_FORM_NAME = 'bopis-pickup-location';\n\n// Module-level state for validation\nconst state = {\n deliveryType: 'delivery',\n};\n\nconst createDeliveryMethodUI = async (onToggle) => {\n const wrapper = document.createElement('div');\n wrapper.className = 'bopis-delivery-method';\n\n const titleContainer = document.createElement('div');\n titleContainer.className = 'bopis-delivery-method__title';\n titleContainer.textContent = 'Delivery Method';\n wrapper.appendChild(titleContainer);\n\n const buttonsContainer = document.createElement('div');\n buttonsContainer.className = 'bopis-delivery-method__buttons';\n\n const deliveryButtonContainer = document.createElement('div');\n const pickupButtonContainer = document.createElement('div');\n\n const buttons = {};\n\n buttons.delivery = await UI.render(ToggleButton, {\n label: 'Delivery',\n selected: true,\n onChange: () => {\n buttons.delivery.setProps((prev) => ({ ...prev, selected: true }));\n buttons.pickup.setProps((prev) => ({ ...prev, selected: false }));\n onToggle('delivery');\n },\n })(deliveryButtonContainer);\n\n buttons.pickup = await UI.render(ToggleButton, {\n label: 'In-store Pickup',\n selected: false,\n onChange: () => {\n buttons.pickup.setProps((prev) => ({ ...prev, selected: true }));\n buttons.delivery.setProps((prev) => ({ ...prev, selected: false }));\n onToggle('pickup');\n },\n })(pickupButtonContainer);\n\n buttonsContainer.appendChild(deliveryButtonContainer);\n buttonsContainer.appendChild(pickupButtonContainer);\n wrapper.appendChild(buttonsContainer);\n\n return wrapper;\n};\n\nconst createPickupLocationsUI = async (locations, onSelect) => {\n const form = document.createElement('form');\n form.name = PICKUP_FORM_NAME;\n form.className = 'bopis-pickup-locations';\n\n await Promise.all(locations.map(async (location) => {\n const radioContainer = document.createElement('div');\n radioContainer.className = 'bopis-pickup-locations__item';\n\n await UI.render(RadioButton, {\n label: location.name,\n name: 'pickup-location',\n value: location.pickup_location_code,\n onChange: () => onSelect(location.pickup_location_code),\n required: true,\n })(radioContainer);\n\n form.appendChild(radioContainer);\n }));\n\n return form;\n};\n\nconst extensionBasePath = new URL('.', import.meta.url).pathname;\n\nexport default {\n id: 'bopis',\n name: 'Buy Online Pickup In Store',\n\n externalStyles: [\n `${extensionBasePath}bopis.css`,\n ],\n\n hooks: {\n 'checkout/address-form-render': async ({ context }) => {\n const { addressType, container } = context;\n\n if (addressType !== 'shipping') return;\n\n const originalRender = context.render;\n\n context.render = async (props) => {\n const $shippingMethods = document.querySelector('.checkout__delivery');\n\n const $pickupLocations = document.createElement('div');\n $pickupLocations.style.display = 'none';\n\n const pickupLocations = await fetchPickupLocations();\n\n const pickupLocationsUI = await createPickupLocationsUI(\n pickupLocations,\n async (locationCode) => {\n await checkoutApi.setShippingAddress({\n address: {},\n pickupLocationCode: locationCode,\n });\n },\n );\n $pickupLocations.appendChild(pickupLocationsUI);\n\n const handleToggle = (type) => {\n state.deliveryType = type;\n\n if (type === 'delivery') {\n container.style.display = '';\n if ($shippingMethods) $shippingMethods.style.display = '';\n $pickupLocations.style.display = 'none';\n } else {\n container.style.display = 'none';\n if ($shippingMethods) $shippingMethods.style.display = 'none';\n $pickupLocations.style.display = '';\n }\n };\n\n const deliveryMethodUI = await createDeliveryMethodUI(handleToggle);\n container.parentNode.insertBefore(deliveryMethodUI, container);\n\n container.parentNode.insertBefore($pickupLocations, container);\n\n const result = await originalRender(props);\n\n return result;\n };\n },\n\n 'checkout/validate': async ({ context }) => {\n if (state.deliveryType !== 'pickup') return;\n\n const form = document.querySelector(`form[name=\"${PICKUP_FORM_NAME}\"]`);\n if (form && !form.reportValidity()) {\n context.isValid = false;\n }\n },\n },\n};\n",
|
|
212
|
+
"README.md": "# BOPIS Extension\n\nBuy Online, Pickup In Store (BOPIS) extension for the checkout.\n\n## Overview\n\nThis extension adds a delivery method selector that allows customers to choose between:\n- **Delivery** - Standard shipping to an address\n- **In-store Pickup** - Pick up at a store location\n\n## How It Works\n\n1. Uses the `checkout/address-form-render` hook to insert UI before the shipping form\n2. When \"Delivery\" is selected: shows shipping address form and shipping methods\n3. When \"In-store Pickup\" is selected: hides shipping form, displays pickup locations\n4. Uses the `checkout/validate` hook to ensure a pickup location is selected before placing order\n\n## Files\n\n- `bopis-extension.js` - Main extension logic\n- `bopis.css` - Styles for delivery method toggle and pickup locations\n\n## Customization\n\n### Fetching Real Pickup Locations\n\nReplace the mock data with a real GraphQL query:\n\n```javascript\nconst fetchPickupLocations = async () => checkoutApi.fetchGraphQl(`\n query pickupLocations {\n pickupLocations {\n items { name, pickup_location_code }\n }\n }\n`, { method: 'GET', cache: 'no-cache' })\n .then((res) => res.data.pickupLocations.items);\n```\n\n## Prerequisites\n\nPickup locations must be configured in Adobe Commerce Admin before using this extension with real data.\n",
|
|
213
|
+
"bopis.css": ".bopis-delivery-method__title {\n color: var(--color-neutral-800);\n font: var(--type-headline-2-default-font);\n letter-spacing: var(--type-headline-2-default-letter-spacing);\n margin: 0 0 var(--spacing-medium) 0;\n}\n\n.bopis-delivery-method__buttons {\n display: flex;\n gap: var(--spacing-small, 12px);\n}\n\n.bopis-delivery-method__buttons > div {\n flex: 1;\n}\n\n.bopis-pickup-locations {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-xsmall, 8px);\n}\n\n.bopis-pickup-locations__item {\n padding: var(--spacing-xsmall, 8px) 0;\n}\n\n.checkout__shipping-form {\n padding-top: 0;\n border-top: none;\n}"
|
|
214
|
+
},
|
|
215
|
+
"sharedFiles": []
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"id": "braintree-payment",
|
|
219
|
+
"name": "Braintree Payment Gateway",
|
|
220
|
+
"provider": "braintree",
|
|
221
|
+
"description": "Braintree Payment Gateway checkout extension using hooks: checkout/payment-methods, checkout/place-order",
|
|
222
|
+
"hooks": [
|
|
223
|
+
"checkout/payment-methods",
|
|
224
|
+
"checkout/place-order"
|
|
225
|
+
],
|
|
226
|
+
"files": {
|
|
227
|
+
"braintree-extension.js": "/* eslint-disable import/no-unresolved */\n/* eslint-disable no-console */\nimport * as checkoutApi from '@dropins/storefront-checkout/api.js';\nimport * as orderApi from '@dropins/storefront-order/api.js';\n\nconst BRAINTREE_AUTHORIZATION_TOKEN = '<YOUR_BRAINTREE_SANDBOX_TOKEN>';\n\nlet braintreeInstance = null;\n\nexport default {\n id: 'braintree-payment',\n name: 'Braintree Payment Gateway',\n\n externalScripts: [\n 'https://js.braintreegateway.com/web/dropin/1.43.0/js/dropin.min.js',\n ],\n\n hooks: {\n 'checkout/payment-methods': async ({ context }) => {\n console.log('[Braintree Extension] Registering payment method');\n\n context.paymentMethods.braintree = {\n autoSync: false,\n render: async (ctx) => {\n const container = document.createElement('div');\n\n window.braintree.dropin.create({\n authorization: BRAINTREE_AUTHORIZATION_TOKEN,\n container,\n }, (err, instance) => {\n if (err) {\n console.error('[Braintree Extension] Error creating dropin:', err);\n return;\n }\n braintreeInstance = instance;\n console.log('[Braintree Extension] Dropin created successfully');\n });\n\n ctx.replaceHTML(container);\n },\n };\n },\n\n 'checkout/place-order': async ({ context }) => {\n const { cartId, code } = context;\n\n if (code !== 'braintree') return;\n\n context.preventDefault = true;\n\n console.log('[Braintree Extension] Processing payment...');\n\n await new Promise((resolve, reject) => {\n braintreeInstance.requestPaymentMethod(async (err, payload) => {\n if (err) {\n console.error('[Braintree Extension] Payment error:', err);\n reject(err);\n return;\n }\n\n try {\n console.log('[Braintree Extension] Setting payment method on cart...');\n await checkoutApi.setPaymentMethod({\n code: 'braintree',\n braintree: {\n is_active_payment_token_enabler: false,\n payment_method_nonce: payload.nonce,\n },\n });\n\n console.log('[Braintree Extension] Placing order...');\n await orderApi.placeOrder(cartId);\n\n console.log('[Braintree Extension] Order placed successfully');\n resolve();\n } catch (error) {\n console.error('[Braintree Extension] Error placing order:', error);\n reject(error);\n }\n });\n });\n },\n },\n};\n"
|
|
228
|
+
},
|
|
229
|
+
"sharedFiles": []
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"id": "custom-shipping-methods",
|
|
233
|
+
"name": "Custom Shipping Methods",
|
|
234
|
+
"provider": "custom-shipping-methods",
|
|
235
|
+
"description": "Custom Shipping Methods checkout extension",
|
|
236
|
+
"hooks": [],
|
|
237
|
+
"files": {
|
|
238
|
+
"custom-shipping-methods-extension.js": "/**\n * Custom Shipping Methods Extension\n *\n * Replaces the default shipping method item UI with a custom card\n * design via the ShippingMethodItem slot.\n */\n\nconst extensionBasePath = new URL('.', import.meta.url).pathname;\n\nfunction buildCard({ method, isSelected }) {\n const price = method.amount.value === 0\n ? 'FREE'\n : `$${method.amount.value.toFixed(2)}`;\n\n const card = document.createElement('label');\n card.className = `custom-shipping-card${isSelected ? ' custom-shipping-card--selected' : ''}`;\n card.innerHTML = `\n <input\n type=\"radio\"\n name=\"shipping-method\"\n value=\"${method.value}\"\n ${isSelected ? 'checked' : ''}\n class=\"custom-shipping-card__input\"\n />\n <div class=\"custom-shipping-card__content\">\n <div class=\"custom-shipping-card__details\">\n <span class=\"custom-shipping-card__title\">${method.carrier.title}</span>\n <span class=\"custom-shipping-card__description\">${method.title}</span>\n </div>\n <span class=\"custom-shipping-card__price\">${price}</span>\n </div>\n `;\n\n return card;\n}\n\nexport default {\n id: 'custom-shipping-methods',\n name: 'Custom Shipping Methods',\n\n externalStyles: [\n `${extensionBasePath}custom-shipping-methods-extension.css`,\n ],\n\n hooks: {\n 'checkout/shipping-methods-render': async ({ context }) => {\n const originalRender = context.render;\n\n context.render = (props) => originalRender({\n ...props,\n slots: {\n ...props?.slots,\n ShippingMethodItem: (ctx) => {\n const card = buildCard(ctx);\n ctx.replaceWith(card);\n\n card.querySelector('input').addEventListener('change', () => {\n ctx.onSelect();\n });\n\n ctx.onRender(({ isSelected }) => {\n card.classList.toggle('custom-shipping-card--selected', isSelected);\n card.querySelector('input').checked = isSelected;\n });\n },\n },\n });\n },\n },\n};\n",
|
|
239
|
+
"README.md": "# Custom Shipping Methods Extension\n\nReplaces the default shipping method item UI with custom styled cards.\n\n## Overview\n\nThis extension demonstrates how to use the `checkout/shipping-methods-render` hook to fully replace the default shipping method selector with a custom card-based design.\n\n## How It Works\n\n1. Uses the `checkout/shipping-methods-render` hook to provide a `ShippingMethodItem` slot\n2. Each shipping method is rendered as a styled card with carrier name, description, and price\n3. Selection state is managed via `ctx.onSelect()` and `ctx.onRender()` to keep styles in sync\n\n## Files\n\n| File | Description |\n|------|-------------|\n| `custom-shipping-methods-extension.js` | Main extension logic and slot implementation |\n| `custom-shipping-methods-extension.css` | Styles for the custom shipping method cards |\n\n## Hook\n\nThis extension uses the `checkout/shipping-methods-render` hook, assigning a `ShippingMethodItem` slot that replaces the default item rendering.\n\nSee the [extensions README](../README.md#checkoutshipping-methods-render) for full hook documentation.\n\n```\n",
|
|
240
|
+
"custom-shipping-methods-extension.css": ".custom-shipping-card {\n display: block;\n cursor: pointer;\n border: 2px solid var(--color-neutral-300);\n border-radius: var(--shape-border-radius-3);\n padding: var(--spacing-medium);\n}\n\n.custom-shipping-card--selected {\n border-color: var(--color-brand-500);\n background: var(--color-brand-50);\n}\n\n.custom-shipping-card__input {\n position: absolute;\n opacity: 0;\n pointer-events: none;\n}\n\n.custom-shipping-card__content {\n display: flex;\n align-items: center;\n gap: var(--spacing-medium);\n}\n\n.custom-shipping-card__details {\n flex: 1;\n display: flex;\n flex-direction: column;\n}\n\n.custom-shipping-card__title {\n font: var(--type-body-1-strong-font);\n letter-spacing: var(--type-body-1-strong-letter-spacing);\n}\n\n.custom-shipping-card__description {\n font: var(--type-body-2-default-font);\n letter-spacing: var(--type-body-2-default-letter-spacing);\n color: var(--color-neutral-600);\n}\n\n.custom-shipping-card__price {\n font: var(--type-headline-2-strong-font);\n letter-spacing: var(--type-headline-2-strong-letter-spacing);\n flex-shrink: 0;\n}\n"
|
|
241
|
+
},
|
|
242
|
+
"sharedFiles": []
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"id": "recurring-order",
|
|
246
|
+
"name": "Recurring Order Extension",
|
|
247
|
+
"provider": "recurring-order",
|
|
248
|
+
"description": "Recurring Order Extension checkout extension using hooks: checkout/validate, checkout/place-order",
|
|
249
|
+
"hooks": [
|
|
250
|
+
"checkout/validate",
|
|
251
|
+
"checkout/place-order"
|
|
252
|
+
],
|
|
253
|
+
"files": {
|
|
254
|
+
"recurring-order-extension.js": "/* eslint-disable no-console */\n/* eslint-disable import/no-unresolved */\n\n/**\n * Recurring Order Extension\n *\n * Adds \"Is Recurring\" (toggle) and \"Frequency\" (dropdown) fields to checkout.\n * On place-order, sets them as order custom_attributes via a custom GraphQL\n * mutation. Replace the mutation with your own backend endpoint as needed.\n */\n\nimport { fetchGraphQl } from '@dropins/storefront-checkout/api.js';\n\nconst extensionBasePath = new URL('.', import.meta.url).pathname;\n\nlet isRecurring = false;\nlet frequency = 'month';\n\nconst FREQUENCY_OPTIONS = [\n { value: 'day', label: 'Daily' },\n { value: 'week', label: 'Weekly' },\n { value: 'month', label: 'Monthly' },\n];\n\nconst SET_ORDER_ATTRIBUTES_MUTATION = `\n mutation setCartCustomAttributes($cartId: String!, $attributes: [CustomAttributeInput!]!) {\n setCustomAttributesOnCart(\n input: { cart_id: $cartId, custom_attributes: $attributes }\n ) {\n cart { id }\n }\n }\n`;\n\nfunction buildRecurringOrderUI() {\n const wrapper = document.createElement('div');\n wrapper.className = 'recurring-order-extension';\n\n wrapper.innerHTML = `\n <label class=\"recurring-order-extension__toggle-label\" for=\"recurring-order-toggle\">\n <h3 class=\"recurring-order-extension__title\">Recurring Order Options</h3>\n <input type=\"checkbox\" id=\"recurring-order-toggle\" role=\"switch\" />\n </label>\n <div class=\"recurring-order-extension__field recurring-order-extension__frequency\" data-hidden=\"true\">\n <label for=\"recurring-order-frequency\">Frequency</label>\n <select id=\"recurring-order-frequency\">\n ${FREQUENCY_OPTIONS.map(\n (o) => `<option value=\"${o.value}\"${o.value === frequency ? ' selected' : ''}>${o.label}</option>`,\n ).join('')}\n </select>\n </div>\n `;\n\n const toggle = wrapper.querySelector('#recurring-order-toggle');\n const freqWrapper = wrapper.querySelector('.recurring-order-extension__frequency');\n const freqSelect = wrapper.querySelector('#recurring-order-frequency');\n\n toggle.addEventListener('change', (e) => {\n isRecurring = e.target.checked;\n freqWrapper.dataset.hidden = String(!isRecurring);\n console.log('[Recurring Order] is_recurring:', isRecurring);\n });\n\n freqSelect.addEventListener('change', (e) => {\n frequency = e.target.value;\n console.log('[Recurring Order] frequency:', frequency);\n });\n\n return wrapper;\n}\n\nexport default {\n id: 'recurring-order',\n name: 'Recurring Order Extension',\n\n externalScripts: [],\n externalStyles: [`${extensionBasePath}recurring-order-extension.css`],\n\n hooks: {\n /**\n * Inject recurring order fields after shipping methods are rendered.\n */\n 'checkout/shipping-methods-render': async ({ context }) => {\n const originalRender = context.render;\n\n context.render = async (props) => {\n const result = await originalRender(props);\n\n if (!context.container.querySelector('.recurring-order-extension')) {\n context.container.appendChild(buildRecurringOrderUI());\n }\n\n return result;\n };\n },\n\n /**\n * Validate: if recurring order is enabled, ensure a frequency is selected.\n */\n 'checkout/validate': async ({ context }) => {\n if (!isRecurring) return;\n\n if (!frequency) {\n console.warn('[Recurring Order] Validation failed — no frequency selected');\n context.isValid = false;\n }\n },\n\n /**\n * Before order placement, persist recurring order attributes on the cart.\n */\n 'checkout/place-order': async ({ context }) => {\n if (!isRecurring) return;\n\n console.log('[Recurring Order] Setting custom attributes:', { is_recurring: isRecurring, frequency });\n\n try {\n await fetchGraphQl(SET_ORDER_ATTRIBUTES_MUTATION, {\n variables: {\n cartId: context.cartId,\n attributes: [\n { attribute_code: 'is_recurring', value: String(isRecurring) },\n { attribute_code: 'frequency', value: frequency },\n ],\n },\n });\n } catch (err) {\n console.error('[Recurring Order] Failed to set custom attributes:', err);\n }\n },\n },\n};\n",
|
|
255
|
+
"recurring-order-extension.css": ".recurring-order-extension {\n margin-top: 24px;\n padding: 20px;\n border: 1px solid var(--color-neutral-400, #d1d5db);\n border-radius: 8px;\n background: var(--color-neutral-50, #f9fafb);\n font: var(--type-body-2-default-font);\n letter-spacing: var(--type-body-1-default-letter-spacing);\n }\n \n .recurring-order-extension__toggle-label {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n cursor: pointer;\n margin-bottom: 16px;\n }\n \n .recurring-order-extension__toggle-label .recurring-order-extension__title {\n margin: 0;\n font: var(--type-body-1-default-font);\n letter-spacing: var(--type-headline-2-default-letter-spacing);\n }\n \n .recurring-order-extension__field {\n margin-bottom: 12px;\n }\n \n .recurring-order-extension__field:last-child {\n margin-bottom: 0;\n }\n \n .recurring-order-extension__toggle-label input[type='checkbox'] {\n width: 40px;\n height: 22px;\n appearance: none;\n background: var(--color-neutral-400, #d1d5db);\n border-radius: 12px;\n position: relative;\n cursor: pointer;\n transition: background 0.2s;\n }\n \n .recurring-order-extension__toggle-label input[type='checkbox']::after {\n content: '';\n position: absolute;\n top: 2px;\n left: 2px;\n width: 18px;\n height: 18px;\n background: #fff;\n border-radius: 50%;\n transition: transform 0.2s;\n }\n \n .recurring-order-extension__toggle-label input[type='checkbox']:checked {\n background: var(--color-positive, #16a34a);\n }\n \n .recurring-order-extension__toggle-label input[type='checkbox']:checked::after {\n transform: translateX(18px);\n }\n \n .recurring-order-extension__frequency {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n \n .recurring-order-extension__frequency[data-hidden='true'] {\n display: none;\n }\n \n .recurring-order-extension__frequency label {\n font: var(--type-body-2-default-font);\n letter-spacing: var(--type-body-2-default-letter-spacing);\n }\n \n .recurring-order-extension__frequency select {\n padding: 8px 12px;\n border: 1px solid var(--color-neutral-400, #d1d5db);\n border-radius: 6px;\n font: var(--type-body-2-default-font);\n letter-spacing: var(--type-body-1-default-letter-spacing);\n background: #fff;\n }\n "
|
|
256
|
+
},
|
|
257
|
+
"sharedFiles": []
|
|
258
|
+
}
|
|
259
|
+
],
|
|
260
|
+
"sharedExtensionFiles": {
|
|
261
|
+
"adyen/constants.js": "// Adyen Configuration\nexport const ADYEN_CLIENT_KEY = 'test_UJLHEXDC5JDOZBLAHE7EB4XCAEANSI6H';\nexport const ADYEN_ENVIRONMENT = 'test';\n\n// Adyen SDK\nexport const ADYEN_SDK_VERSION = '6.16.0';\nexport const ADYEN_SDK_SCRIPT = `https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/${ADYEN_SDK_VERSION}/adyen.js`;\nexport const ADYEN_SDK_STYLE = `https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/${ADYEN_SDK_VERSION}/adyen.css`;\n",
|
|
262
|
+
"adyen/queries.js": "/* eslint-disable import/prefer-default-export */\n/**\n * GraphQL mutation to place order with Adyen payment method\n *\n * IMPORTANT: This mutation requires the PLACE_ORDER_FRAGMENT extension\n * configured in your build.mjs (see README Step 11). The fragment extension\n * adds adyen_payment_status fields needed for redirect handling (e.g., Klarna, 3DS).\n *\n * Required fields from fragment extension:\n * - order.adyen_payment_status.isFinal\n * - order.adyen_payment_status.resultCode\n * - order.adyen_payment_status.action (for redirect URL)\n * - order.adyen_payment_status.additionalData\n */\nexport const PLACE_ORDER_WITH_ADYEN = `\n mutation placeOrderWithAdyen($cartId: String!, $paymentMethod: PaymentMethodInput!) {\n setPaymentMethodOnCart(input: { cart_id: $cartId, payment_method: $paymentMethod }) {\n cart {\n selected_payment_method {\n code\n title\n }\n }\n }\n placeOrder(input: { cart_id: $cartId }) {\n errors {\n code\n message\n }\n orderV2 {\n number\n email\n token\n }\n order {\n order_number\n adyen_payment_status {\n isFinal\n resultCode\n action\n additionalData\n }\n }\n }\n }\n`;\n\n/**\n * GraphQL mutation to process Adyen payment details after redirect\n * Used when returning from payment provider (e.g., Klarna, iDEAL, PayPal)\n */\nexport const ADYEN_PAYMENT_DETAILS = `\n mutation adyenPaymentDetails($cartId: String!, $payload: String!) {\n adyenPaymentDetails(cart_id: $cartId, payload: $payload) {\n isFinal\n resultCode\n }\n }\n`;\n\n/**\n * GraphQL query to fetch Adyen payment methods configuration\n */\nexport const GET_ADYEN_PAYMENT_METHODS = `\n query getAdyenPaymentMethods($cartId: String!) {\n adyenPaymentMethods(cart_id: $cartId) {\n paymentMethodsExtraDetails {\n type\n icon {\n url\n width\n height\n }\n isOpenInvoice\n configuration {\n amount {\n value\n currency\n }\n currency\n }\n }\n paymentMethodsResponse {\n paymentMethods {\n name\n type\n brands\n configuration {\n merchantId\n gatewayMerchantId\n }\n }\n storedPaymentMethods {\n id\n brand\n type\n name\n lastFour\n expiryMonth\n expiryYear\n holderName\n supportedShopperInteractions\n networkTxReference\n iban\n ownerName\n shopperEmail\n }\n }\n }\n }\n`;\n\n/**\n * GraphQL query to fetch customer payment tokens (vault tokens)\n * Used to match stored Adyen payment methods with Adobe Commerce vault tokens\n */\nexport const GET_CUSTOMER_PAYMENT_TOKENS = `\n query getCustomerPaymentTokens {\n customerPaymentTokens {\n items {\n public_hash\n payment_method_code\n details\n type\n }\n }\n }\n`;\n",
|
|
263
|
+
"adyen/utils.js": "/* eslint-disable import/no-unresolved */\nimport { events } from '@dropins/tools/event-bus.js';\nimport * as orderApi from '@dropins/storefront-order/api.js';\nimport { fetchGraphQl } from '@dropins/storefront-checkout/api.js';\nimport { setMetaTags, validateForms } from '@dropins/storefront-checkout/lib/utils.js';\n\nimport { buildOrderDetailsUrl } from '../../utils.js';\nimport {\n BILLING_FORM_NAME,\n LOGIN_FORM_NAME,\n PURCHASE_ORDER_FORM_NAME,\n SHIPPING_FORM_NAME,\n TERMS_AND_CONDITIONS_FORM_NAME,\n} from '../../constants.js';\nimport { renderCheckoutSuccess } from '../../../commerce-checkout-success/commerce-checkout-success.js';\n\nimport {\n PLACE_ORDER_WITH_ADYEN,\n ADYEN_PAYMENT_DETAILS,\n GET_ADYEN_PAYMENT_METHODS,\n} from './queries.js';\nimport { ADYEN_CLIENT_KEY, ADYEN_ENVIRONMENT } from './constants.js';\n\nexport { ADYEN_CLIENT_KEY, ADYEN_ENVIRONMENT };\n\n// ============================================================================\n// Hide Default Place Order Button\n// ============================================================================\n\nconst hideButtonPaymentCodes = new Set();\n\nexport function registerHideDefaultPlaceOrder(code) {\n hideButtonPaymentCodes.add(code);\n}\n\nevents.on('checkout/values', (payload) => {\n const button = document.querySelector('.checkout__place-order');\n if (!button) return;\n\n if (hideButtonPaymentCodes.has(payload.selectedPaymentMethod?.code)) {\n button.style.display = 'none';\n } else {\n button.style.removeProperty('display');\n }\n});\n\n// ============================================================================\n// Session Keys Helper\n// ============================================================================\n\n/**\n * Create session storage keys for a payment method\n * @param {string} prefix - Payment method prefix (e.g., 'adyen_paypal')\n * @returns {{ PENDING_CART_ID: string, PENDING_ORDER_NUMBER: string, PENDING_ORDER_TOKEN: string }}\n */\nexport function createAdyenSessionKeys(prefix) {\n return {\n PENDING_CART_ID: `${prefix}_pending_cart_id`,\n PENDING_ORDER_NUMBER: `${prefix}_pending_order_number`,\n PENDING_ORDER_TOKEN: `${prefix}_pending_order_token`,\n };\n}\n\n// ============================================================================\n// API Functions\n// ============================================================================\n\n/**\n * Fetch Adyen payment methods for a cart\n * @param {string} cartId\n * @returns {Promise<{ paymentMethodsExtraDetails: Array, paymentMethodsResponse: object }>}\n */\nexport async function fetchAdyenPaymentMethods(cartId) {\n const response = await fetchGraphQl(GET_ADYEN_PAYMENT_METHODS, {\n variables: { cartId },\n });\n\n if (response.errors?.length) {\n throw new Error(response.errors[0].message);\n }\n\n const data = response.data?.adyenPaymentMethods;\n if (!data) {\n throw new Error('No payment methods data returned');\n }\n\n return {\n paymentMethodsExtraDetails: data.paymentMethodsExtraDetails || [],\n paymentMethodsResponse: data.paymentMethodsResponse || { paymentMethods: [] },\n };\n}\n\n/**\n * Place an order with Adyen payment\n * @param {string} cartId\n * @param {object} paymentMethod\n * @returns {Promise<object>}\n *\n * @deprecated Use orderapi graphQl override instead\n */\nexport async function placeOrderWithAdyen(cartId, paymentMethod) {\n const response = await fetchGraphQl(PLACE_ORDER_WITH_ADYEN, {\n variables: { cartId, paymentMethod },\n });\n\n if (response.errors?.length) {\n throw new Error(response.errors[0].message);\n }\n\n const placeOrderResult = response.data?.placeOrder;\n\n if (placeOrderResult?.errors?.length) {\n throw new Error(placeOrderResult.errors[0].message);\n }\n\n return placeOrderResult;\n}\n\n/**\n * Submit Adyen payment details (for 3DS, redirect results, etc.)\n * @param {string} cartId\n * @param {object} details - The details object from Adyen SDK\n * @param {string} [orderId] - Optional order ID\n * @returns {Promise<{ isFinal: boolean, resultCode: string }>}\n */\nexport async function submitAdyenPaymentDetails(cartId, details, orderId = null) {\n // Handle paymentData separately if it's in the details object\n // Adyen requires paymentData at the root level, not nested in details\n const payloadObj = {\n orderId,\n };\n\n // If details contains paymentData, extract it to root level\n if (details.paymentData) {\n payloadObj.paymentData = details.paymentData;\n // Create a copy of details without paymentData\n const { paymentData, ...detailsWithoutPaymentData } = details;\n payloadObj.details = detailsWithoutPaymentData;\n // eslint-disable-next-line no-console\n console.log('[submitAdyenPaymentDetails] ✅ Extracted paymentData to root level');\n } else {\n payloadObj.details = details;\n // eslint-disable-next-line no-console\n console.log('[submitAdyenPaymentDetails] ⚠️ No paymentData in details');\n }\n\n // eslint-disable-next-line no-console\n console.log('[submitAdyenPaymentDetails] 📦 Final payload structure:', {\n hasOrderId: !!payloadObj.orderId,\n hasPaymentData: !!payloadObj.paymentData,\n hasDetails: !!payloadObj.details,\n paymentDataLength: payloadObj.paymentData?.length,\n });\n\n const payload = JSON.stringify(payloadObj);\n\n const response = await fetchGraphQl(ADYEN_PAYMENT_DETAILS, {\n variables: { cartId, payload },\n });\n\n if (response.errors?.length) {\n throw new Error(response.errors[0].message);\n }\n\n return response.data?.adyenPaymentDetails;\n}\n\n// ============================================================================\n// UI Helpers\n// ============================================================================\n\n/**\n * Show processing payment UI\n * @param {HTMLElement} block\n */\nexport function showProcessingPaymentUI(block) {\n setMetaTags('Order Confirmation');\n document.title = 'Processing Payment...';\n block.innerHTML = `\n <div class=\"checkout checkout--loading\" style=\"display: flex; justify-content: center;\n align-items: center; min-height: 400px; flex-direction: column; gap: 16px;\">\n <p style=\"font: var(--type-headline-2-strong-font); letter-spacing: var(--type-headline-2-strong-letter-spacing);\n color: #666; display: flex; align-items: center; gap: var(--spacing-small);\">\n <span class=\"dropin-loader dropin-loader--spinner\"></span>\n Processing your payment...\n </p>\n </div>\n `;\n}\n\n// ============================================================================\n// External Payment Flow\n// ============================================================================\n\n/**\n * Initiate an external Adyen payment (PayPal, Klarna, iDEAL, etc.)\n * @param {string} cartId\n * @param {object} config\n * @param {string} config.paymentMethodCode - e.g., 'adyen_paypal'\n * @param {string} config.adyenPaymentType - e.g., 'paypal', 'klarna', 'ideal'\n * @param {object} config.sessionKeys - From createAdyenSessionKeys()\n * @returns {Promise<{ isRedirecting: boolean } | undefined>}\n */\nexport async function initiateExternalPayment(cartId, config) {\n const { paymentMethodCode, adyenPaymentType, sessionKeys } = config;\n const returnUrl = window.location.origin + window.location.pathname;\n\n const additionalData = {\n stateData: JSON.stringify({\n paymentMethod: { type: adyenPaymentType },\n returnUrl,\n }),\n brand_code: adyenPaymentType,\n returnUrl,\n };\n\n const paymentMethod = {\n code: paymentMethodCode,\n adyen_additional_data: additionalData,\n };\n\n const result = await placeOrderWithAdyen(cartId, paymentMethod);\n const adyenStatus = result?.order?.adyen_payment_status;\n\n if (adyenStatus?.action) {\n const action = JSON.parse(adyenStatus.action);\n\n if (action.type === 'redirect' && action.url) {\n sessionStorage.setItem(sessionKeys.PENDING_CART_ID, cartId);\n\n const orderNum = result?.orderV2?.number || result?.order?.order_number;\n if (orderNum) {\n sessionStorage.setItem(sessionKeys.PENDING_ORDER_NUMBER, orderNum);\n }\n if (result?.orderV2?.token) {\n sessionStorage.setItem(sessionKeys.PENDING_ORDER_TOKEN, result.orderV2.token);\n }\n\n window.location.href = action.url;\n return { isRedirecting: true };\n }\n }\n\n if (adyenStatus?.isFinal && result?.orderV2) {\n events.emit('order/placed', result.orderV2);\n events.emit('cart/reset', undefined);\n }\n\n return undefined;\n}\n\n/**\n * Process payment response after returning from external payment provider\n * @param {HTMLElement} block\n * @param {string} redirectResult\n * @param {object} config\n * @param {object} config.sessionKeys - From createAdyenSessionKeys()\n * @param {string} config.errorPrefix - e.g., '[Adyen PayPal]'\n * @param {string} config.defaultErrorMessage - e.g., 'PayPal payment failed. Please try again.'\n * @returns {Promise<boolean>} - true on success, false on error\n */\nasync function processPaymentResponse(block, redirectResult, config) {\n const { sessionKeys, errorPrefix, defaultErrorMessage } = config;\n\n const storedCartId = sessionStorage.getItem(sessionKeys.PENDING_CART_ID);\n const storedOrderNumber = sessionStorage.getItem(sessionKeys.PENDING_ORDER_NUMBER);\n const storedOrderToken = sessionStorage.getItem(sessionKeys.PENDING_ORDER_TOKEN);\n\n if (!storedCartId || !storedOrderNumber) {\n return false;\n }\n\n showProcessingPaymentUI(block);\n\n try {\n const payload = JSON.stringify({\n orderId: storedOrderNumber,\n details: { redirectResult },\n });\n\n const response = await fetchGraphQl(ADYEN_PAYMENT_DETAILS, {\n variables: { cartId: storedCartId, payload },\n });\n\n sessionStorage.removeItem(sessionKeys.PENDING_CART_ID);\n sessionStorage.removeItem(sessionKeys.PENDING_ORDER_NUMBER);\n sessionStorage.removeItem(sessionKeys.PENDING_ORDER_TOKEN);\n\n if (response.errors?.length) {\n throw new Error(response.errors[0].message);\n }\n\n const paymentStatus = response.data?.adyenPaymentDetails;\n\n if (paymentStatus?.isFinal && paymentStatus?.resultCode === 'Authorised') {\n const orderData = await orderApi.guestOrderByToken(storedOrderToken);\n\n if (orderData) {\n window.history.replaceState({}, '', window.location.pathname);\n events.emit('order/placed', orderData);\n events.emit('cart/reset', undefined);\n await renderCheckoutSuccess(block, { orderData });\n window.history.pushState({}, '', buildOrderDetailsUrl(orderData));\n document.title = 'Order Confirmation';\n return true;\n }\n\n throw new Error('Unable to retrieve order details');\n }\n\n throw new Error(`Payment ${paymentStatus?.resultCode || 'failed'}`);\n } catch (error) {\n console.error(`${errorPrefix} Error processing redirect:`, error);\n\n sessionStorage.removeItem(sessionKeys.PENDING_CART_ID);\n sessionStorage.removeItem(sessionKeys.PENDING_ORDER_NUMBER);\n sessionStorage.removeItem(sessionKeys.PENDING_ORDER_TOKEN);\n\n window.history.replaceState({}, '', window.location.pathname);\n block.innerHTML = '';\n\n const errorMessage = error.message || defaultErrorMessage;\n events.on(\n 'checkout/initialized',\n () => {\n events.emit('checkout/error', {\n message: errorMessage,\n code: 'payment_error',\n });\n },\n { once: true },\n );\n\n return false;\n }\n}\n\n// ============================================================================\n// Shared Helpers\n// ============================================================================\n\n/**\n * Get cart amount for Adyen payment methods (value in cents)\n * @returns {{ value: number, currency: string }}\n */\nexport function getCartAmount() {\n const cart = events.lastPayload('cart/data') || events.lastPayload('cart/initialized');\n const grandTotal = cart?.total?.includingTax;\n return {\n value: Math.round((grandTotal?.value || 0) * 100),\n currency: grandTotal?.currency || 'USD',\n };\n}\n\n/**\n * Validate all checkout forms\n * @returns {boolean}\n */\nexport function validateCheckoutForms() {\n return validateForms([\n { name: LOGIN_FORM_NAME },\n { name: SHIPPING_FORM_NAME },\n { name: BILLING_FORM_NAME },\n { name: PURCHASE_ORDER_FORM_NAME },\n { name: TERMS_AND_CONDITIONS_FORM_NAME },\n ]);\n}\n\n/**\n * Create an Adyen checkout instance with default configuration\n * @param {object} paymentMethodsResponse - Payment methods from API\n * @param {object} options - Additional options (amount, onSubmit, onError, etc.)\n * @returns {Promise<object>} - Adyen checkout instance\n */\nexport async function createAdyenCheckoutInstance(paymentMethodsResponse, options = {}) {\n const { AdyenCheckout } = window.AdyenWeb || {};\n\n if (!AdyenCheckout) {\n throw new Error('AdyenCheckout not available');\n }\n\n return AdyenCheckout({\n clientKey: ADYEN_CLIENT_KEY,\n locale: 'en_US',\n environment: ADYEN_ENVIRONMENT,\n countryCode: 'US',\n paymentMethodsResponse,\n analytics: {\n enabled: false,\n },\n risk: {\n enabled: false,\n },\n ...options,\n });\n}\n\n// ============================================================================\n// External Payment Helpers\n// ============================================================================\n\n/**\n * Handle payment response for redirect-based payment methods\n * Use this in checkout/payment-response hook\n *\n * Sets context.shouldExit = true on success, false on error\n *\n * @param {object} context - Hook context\n * @param {object} config - Configuration\n * @param {object} config.sessionKeys - From createAdyenSessionKeys()\n * @param {string} config.errorPrefix - e.g., '[Adyen Klarna]'\n * @param {string} config.defaultErrorMessage - Error message to show on failure\n */\nexport async function handlePaymentResponse(context, config) {\n const { sessionKeys, errorPrefix, defaultErrorMessage } = config;\n\n // Check for pending payment return\n const urlParams = new URLSearchParams(window.location.search);\n const redirectResult = urlParams.get('redirectResult');\n const hasPendingData = Boolean(\n sessionStorage.getItem(sessionKeys.PENDING_CART_ID)\n && sessionStorage.getItem(sessionKeys.PENDING_ORDER_NUMBER),\n );\n\n if (!redirectResult || !hasPendingData) return;\n\n const success = await processPaymentResponse(context.block, redirectResult, {\n sessionKeys,\n errorPrefix,\n defaultErrorMessage,\n });\n context.shouldExit = success;\n}\n\n/**\n * Handle place order hook for external payment methods\n * Use this in checkout/place-order hook\n * @param {object} context - Hook context\n * @param {object} config - Configuration\n * @param {string} config.paymentMethodCode - e.g., 'adyen_klarna'\n * @param {string} config.adyenPaymentType - e.g., 'klarna'\n * @param {object} config.sessionKeys - From createAdyenSessionKeys()\n * @param {string} config.errorPrefix - e.g., '[Adyen Klarna]'\n */\nexport async function handleExternalPlaceOrder(context, config) {\n const {\n paymentMethodCode,\n adyenPaymentType,\n sessionKeys,\n errorPrefix,\n } = config;\n const { code, cartId } = context;\n\n if (code !== paymentMethodCode) return;\n\n context.preventDefault = true;\n\n try {\n const result = await initiateExternalPayment(cartId, {\n paymentMethodCode,\n adyenPaymentType,\n sessionKeys,\n });\n\n if (result?.isRedirecting) {\n // Keep in loading state while redirecting\n await new Promise(() => {});\n }\n } catch (error) {\n console.error(`${errorPrefix} Payment error:`, error);\n throw error;\n }\n}\n"
|
|
264
|
+
},
|
|
265
|
+
"blocks": [
|
|
266
|
+
{
|
|
267
|
+
"name": "commerce-b2b-po-checkout-success",
|
|
268
|
+
"description": "Example commerce-b2b-po-checkout-success block for storefront-checkout",
|
|
269
|
+
"files": {
|
|
270
|
+
"commerce-b2b-po-checkout-success.js": "/* eslint-disable import/no-unresolved */\n\n// Tools and initializers\nimport { Button, provider as UI } from '@dropins/tools/components.js';\n\n// Purchase Order Dropin\nimport PurchaseOrderConfirmation from '@dropins/storefront-purchase-order/containers/PurchaseOrderConfirmation.js';\nimport { render as POProvider } from '@dropins/storefront-purchase-order/render.js';\n\n// Checkout API/utils\nimport { createFragment, createScopedSelector } from '@dropins/storefront-checkout/lib/utils.js';\n\n// Commerce helpers\nimport { rootLink, CUSTOMER_PO_DETAILS_PATH } from '../../scripts/commerce.js';\nimport { loadCSS } from '../../scripts/aem.js';\n\n// Initialize dropins\nimport '../../scripts/initializers/purchase-order.js';\n\n// ----------------------------------------------------------------------------\n// Local selectors and fragments (order confirmation only)\n// ----------------------------------------------------------------------------\n\nconst selectors = Object.freeze({\n poConfirmation: {\n content: '.po-confirmation__content',\n footer: '.po-confirmation__footer',\n continueButton: '.po-confirmation-footer__continue-button',\n },\n});\n\nfunction createPOConfirmationFragment() {\n return createFragment(`\n <div class=\"po-confirmation\">\n <div class=\"po-confirmation__content\"></div>\n <div class=\"po-confirmation__footer\"></div>\n </div>\n `);\n}\n\n// ----------------------------------------------------------------------------\n// Local renderers (order confirmation only)\n// ----------------------------------------------------------------------------\n\nasync function renderPOConfirmationContainer(container, poNumber, poUid) {\n return POProvider.render(PurchaseOrderConfirmation, {\n purchaseOrderNumber: poNumber,\n routePurchaseOrderDetails: () => rootLink(`${CUSTOMER_PO_DETAILS_PATH}?poRef=${poUid}`),\n })(container);\n}\n\nasync function renderPOConfirmationFooterButton(container) {\n UI.render(Button, {\n children: 'Continue shopping',\n 'data-testid': 'po-confirmation-footer__continue-button',\n className: 'po-confirmation-footer__continue-button',\n size: 'medium',\n variant: 'primary',\n type: 'submit',\n href: rootLink('/'),\n })(container);\n}\n\nasync function renderPOConfirmationContent(container, poData = {}) {\n // Scroll to the top of the page\n window.scrollTo(0, 0);\n\n // Create a purchase order confirmation layout using fragments\n const poConfirmationFragment = createPOConfirmationFragment();\n\n // Create a scoped selector for PO confirmation fragment (following a multistep pattern)\n const getPOElement = createScopedSelector(poConfirmationFragment);\n\n // Get all PO confirmation elements using centralized selectors\n const $poConfirmationContent = getPOElement(selectors.poConfirmation.content);\n const $poConfirmationFooter = getPOElement(selectors.poConfirmation.footer);\n\n container.replaceChildren(poConfirmationFragment);\n\n await Promise.all([\n renderPOConfirmationContainer($poConfirmationContent, poData?.number, poData?.uid),\n renderPOConfirmationFooterButton($poConfirmationFooter),\n ]);\n}\n\nexport async function renderPOSuccess(container, poData) {\n await loadCSS(`${window.hlx.codeBasePath}/blocks/commerce-b2b-po-checkout-success/commerce-b2b-po-checkout-success.css`);\n return renderPOConfirmationContent(container, poData);\n}\n\nexport default async function decorate(block) {\n await renderPOConfirmationContent(block);\n}\n",
|
|
271
|
+
"commerce-b2b-po-checkout-success.css": ".po-confirmation {\n padding-bottom: var(--spacing-large);\n padding-top: var(--spacing-large);\n }\n\n .po-confirmation__footer {\n max-width: max-content;\n }\n",
|
|
272
|
+
"README.md": "# Commerce B2B Purchase Order Checkout Success Block\n\n## Overview\n\nThe Commerce B2B PO Checkout Success block renders the post-purchase order confirmation experience for B2B purchase orders using the @dropins/storefront-purchase-order PurchaseOrderConfirmation container. It provides purchase order confirmation display with automatic scroll positioning, purchase order details navigation, and a continue shopping action.\n\n## Integration\n\n### Block Configuration\n\n| Configuration Key | Type | Default | Description | Required | Side Effects |\n| ----------------- | ---- | ------- | ------------------------------------------- | -------- | ------------ |\n| – | – | – | This block has no authorable configuration. | – | – |\n\n<!-- ### URL Parameters\n\nNo URL parameters directly affect this block's behavior. The Purchase Order drop-in may use URL parameters to identify the purchase order. -->\n\n<!-- ### Local Storage\n\nNo localStorage keys are used by this block. -->\n\n<!-- ### Events\n\n#### Event Listeners\n\nNo events are listened to by this block.\n\n#### Event Emitters\n\nNo events are emitted by this block. -->\n\n## Public API\n\n### Exports\n\n- `default export decorate(block)` — Block decorator that renders the purchase order success view into the provided block element.\n- `export async function renderPOSuccess(container, { poData } = {})` — Renders the success view into the specified container. Pass `poData` with a `number` property to provide the purchase order number for confirmation display and details navigation.\n\n### Usage Examples\n\n**Programmatic usage:**\n\n```js\nimport { renderPOSuccess } from \"./blocks/commerce-b2b-po-checkout-success/commerce-b2b-po-checkout-success.js\";\n\n// Render with purchase order data\nawait renderPOSuccess(container, { poData: { number: \"PO-12345\" } });\n```\n\n**Block usage:**\n\n```html\n<div class=\"block commerce-b2b-po-checkout-success\"></div>\n```\n\n## Behavior Patterns\n\n### Page Context Detection\n\n- **Purchase Order Confirmation**: Displays confirmation for the newly created purchase order\n- **Scroll Positioning**: Automatically scrolls to top of page on load for proper confirmation visibility\n- **CSS Loading**: Dynamically loads block-specific styles (`commerce-b2b-po-checkout-success.css`)\n\n### User Interaction Flows\n\n1. **Page Load**: Block initializes and scrolls window to top position\n2. **Fragment Creation**: Creates DOM structure using centralized fragment utilities\n3. **Content Rendering**: Renders PurchaseOrderConfirmation container with purchase order number\n4. **Navigation Setup**: Configures purchase order details route callback using `routePurchaseOrderDetails`\n5. **Footer Display**: Renders continue shopping button with navigation to homepage\n6. **Parallel Rendering**: Content and footer render concurrently using Promise.all for optimal performance\n\n### Error Handling\n\n- **Missing PO Data**: If no `poData` is provided to `renderPOSuccess`, the Purchase Order drop-in will attempt to fetch data when possible\n- **Container Errors**: PurchaseOrderConfirmation container handles its own data fetching states and error UI\n- **Rendering Errors**: If container rendering fails, error states are managed by the drop-in's internal error handling\n- **Fallback Behavior**: Block relies on drop-in's built-in error recovery and user feedback mechanisms\n\n## DOM Structure\n\nThis block builds a scoped fragment using centralized selectors that follows this structure:\n\n```html\n<div class=\"po-confirmation\">\n <div class=\"po-confirmation__content\">\n <!-- PurchaseOrderConfirmation container renders here -->\n </div>\n <div class=\"po-confirmation__footer\">\n <button class=\"po-confirmation-footer__continue-button\">\n Continue shopping\n </button>\n </div>\n</div>\n```\n\n### Rendered Containers\n\n- **PurchaseOrderConfirmation** — Displays purchase order confirmation details from `@dropins/storefront-purchase-order`\n - Receives `purchaseOrderNumber` to identify the order\n - Receives `routePurchaseOrderDetails` callback for navigation to `/customer/purchase-order-details?poRef={poNumber}`\n- **Continue Shopping Button** — UI Button component with medium size, primary variant, navigates to homepage via `rootLink('/')`\n\n## Styling\n\n- **CSS File**: `commerce-b2b-po-checkout-success.css`\n- **Class Naming**: Follows BEM-style `po-confirmation__*` pattern for scoping\n- **Layout Spacing**: Uses CSS custom property `--spacing-large` for vertical padding on main container\n- **Footer Constraint**: Footer width constrained to `max-content` for optimal button sizing\n\n### CSS Custom Properties\n\n- `--spacing-large` — Applied to top and bottom padding of `.po-confirmation`\n\n## Dependencies\n\n### Drop-in Packages\n\n- `@dropins/storefront-purchase-order` — PurchaseOrderConfirmation container and render provider\n- `@dropins/storefront-checkout` — Fragment creation and scoped selector utilities\n- `@dropins/tools` — UI Button component and provider\n\n### Local Modules\n\n- `scripts/commerce.js` — rootLink helper for navigation\n- `scripts/aem.js` — loadCSS utility for dynamic stylesheet loading\n- `scripts/initializers/purchase-order.js` — Purchase Order drop-in initializer\n\n## Implementation Details\n\n### Selectors\n\nCentralized selectors object using frozen constants:\n\n```js\nconst selectors = Object.freeze({\n poConfirmation: {\n content: \".po-confirmation__content\",\n footer: \".po-confirmation__footer\",\n continueButton: \".po-confirmation-footer__continue-button\",\n },\n});\n```\n\n### Fragment Creation\n\nUses `createFragment` utility to build DOM structure declaratively, then applies `createScopedSelector` for type-safe element selection.\n\n### Rendering Strategy\n\n- **Parallel Rendering**: Content and footer render concurrently for performance\n- **Container Replacement**: Uses `replaceChildren` to cleanly replace block content with confirmation fragment\n- **Async Loading**: CSS and container rendering happen asynchronously\n\n## Notes\n\n- This block is specifically designed for B2B purchase order workflows and complements the standard `commerce-checkout-success` block for regular orders\n- The block is read-only and focused on confirmation and post-purchase actions\n- Navigation to purchase order details is handled via the `routePurchaseOrderDetails` callback which uses the `poRef` query parameter\n- No authentication or permission checks are performed as this is a post-checkout confirmation page\n- The block follows the same architectural patterns as the multistep checkout flow for consistency\n"
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"name": "commerce-b2b-quote-checkout",
|
|
277
|
+
"description": "Example commerce-b2b-quote-checkout block for storefront-checkout",
|
|
278
|
+
"files": {
|
|
279
|
+
"commerce-b2b-quote-checkout.js": "/* eslint-disable import/no-unresolved */\n/* eslint-disable no-unused-vars */\n\n// Dropin Tools\nimport { events } from '@dropins/tools/event-bus.js';\nimport { initReCaptcha } from '@dropins/tools/recaptcha.js';\n\n// Order Dropin Modules\nimport * as orderApi from '@dropins/storefront-order/api.js';\n\n// Checkout Dropin Libraries\nimport {\n createScopedSelector,\n isVirtualCart,\n setMetaTags,\n validateForms,\n} from '@dropins/storefront-checkout/lib/utils.js';\n\n// Purchase Order Dropin\nimport * as poApi from '@dropins/storefront-purchase-order/api.js';\nimport { PO_PERMISSIONS } from '@dropins/storefront-purchase-order/api.js';\n\n// Block Utilities\nimport {\n buildOrderDetailsUrl,\n displayOverlaySpinner,\n removeOverlaySpinner,\n} from './utils.js';\n\n// Fragment functions\nimport {\n createCheckoutFragment,\n createAddressSummary,\n selectors,\n} from './fragments.js';\n\n// Container functions\nimport {\n renderBillingAddressFormSkeleton,\n renderBillToShippingAddress,\n renderCheckoutHeader,\n renderCustomerBillingAddresses,\n renderLoginForm,\n renderOrderSummary,\n renderPaymentMethods,\n renderPlaceOrder,\n renderQuoteSummaryList,\n renderServerError,\n renderShippingAddressFormSkeleton,\n renderShippingMethods,\n renderTermsAndConditions,\n} from './containers.js';\n\n// Constants\nimport {\n BILLING_ADDRESS_DATA_KEY,\n BILLING_FORM_NAME,\n LOGIN_FORM_NAME,\n PURCHASE_ORDER_FORM_NAME,\n TERMS_AND_CONDITIONS_FORM_NAME,\n} from './constants.js';\nimport { CUSTOMER_PO_DETAILS_PATH, rootLink } from '../../scripts/commerce.js';\n\n// Success block entry points\nimport { renderCheckoutSuccess, preloadCheckoutSuccess } from '../commerce-checkout-success/commerce-checkout-success.js';\nimport { renderPOSuccess } from '../commerce-b2b-po-checkout-success/commerce-b2b-po-checkout-success.js';\n\n// Initializers\nimport '../../scripts/initializers/account.js';\nimport '../../scripts/initializers/checkout.js';\nimport '../../scripts/initializers/order.js';\nimport '../../scripts/initializers/quote-management.js';\n\n// Checkout success block CSS preload\npreloadCheckoutSuccess();\n\nexport default async function decorate(block) {\n const permissions = events.lastPayload('auth/permissions');\n const isPoEnabled = permissions ? !(permissions[PO_PERMISSIONS.PO_ALL] === false) : false;\n\n // Container and component references\n let billingForm;\n let shippingAddresses;\n let billingAddresses;\n\n const billingFormRef = { current: null };\n const loaderRef = { current: null };\n\n setMetaTags('B2B Checkout');\n document.title = 'B2B Checkout';\n\n events.on('order/placed', () => {\n setMetaTags('B2B Order Confirmation');\n document.title = 'B2B Order Confirmation';\n });\n\n // Create the checkout layout using fragments\n const checkoutFragment = createCheckoutFragment();\n\n // Create scoped selector for the checkout fragment\n const getElement = createScopedSelector(checkoutFragment);\n\n // Get all checkout elements using centralized selectors\n const $content = getElement(selectors.checkout.content);\n const $loader = getElement(selectors.checkout.loader);\n const $heading = getElement(selectors.checkout.heading);\n const $serverError = getElement(selectors.checkout.serverError);\n const $login = getElement(selectors.checkout.login);\n const $shippingForm = getElement(selectors.checkout.shippingForm);\n const $billToShipping = getElement(selectors.checkout.billToShipping);\n const $delivery = getElement(selectors.checkout.delivery);\n const $paymentMethods = getElement(selectors.checkout.paymentMethods);\n const $billingForm = getElement(selectors.checkout.billingForm);\n const $orderSummary = getElement(selectors.checkout.orderSummary);\n const $quoteSummary = getElement(selectors.checkout.quoteSummary);\n const $placeOrder = getElement(selectors.checkout.placeOrder);\n const $termsAndConditions = getElement(selectors.checkout.termsAndConditions);\n\n block.appendChild(checkoutFragment);\n\n // Create validation and place order handlers\n const handleValidation = () => validateForms([\n { name: LOGIN_FORM_NAME },\n { name: PURCHASE_ORDER_FORM_NAME },\n { name: BILLING_FORM_NAME, ref: billingFormRef },\n { name: TERMS_AND_CONDITIONS_FORM_NAME },\n ]);\n\n const handlePlaceOrder = async ({ quoteId }) => {\n await displayOverlaySpinner(loaderRef, $loader);\n try {\n if (isPoEnabled) {\n await poApi.placePurchaseOrder(quoteId);\n } else {\n await orderApi.placeNegotiableQuoteOrder(quoteId);\n }\n } catch (error) {\n console.error(error);\n throw error;\n } finally {\n removeOverlaySpinner(loaderRef, $loader);\n }\n };\n\n // First, render the place order component\n await renderPlaceOrder($placeOrder, {\n handleValidation,\n handlePlaceOrder,\n isPoEnabled,\n });\n\n // Render the remaining containers\n const [\n _header,\n _serverError,\n _loginForm,\n _shippingFormSkeleton,\n _billToShipping,\n _shippingMethods,\n _paymentMethods,\n _billingFormSkeleton,\n _orderSummary,\n _quoteSummary,\n _termsAndConditions,\n ] = await Promise.all([\n renderCheckoutHeader($heading, 'B2B Checkout'),\n\n renderServerError($serverError, $content),\n\n renderLoginForm($login),\n\n renderShippingAddressFormSkeleton($shippingForm),\n\n renderBillToShippingAddress($billToShipping),\n\n renderShippingMethods($delivery),\n\n renderPaymentMethods($paymentMethods),\n\n renderBillingAddressFormSkeleton($billingForm),\n\n renderOrderSummary($orderSummary),\n\n renderQuoteSummaryList($quoteSummary),\n\n renderTermsAndConditions($termsAndConditions),\n ]);\n\n async function initializeCheckout(data) {\n await initReCaptcha(0);\n\n if (data?.uid && data.shippingAddresses?.[0]) {\n const quoteAddress = data.shippingAddresses[0];\n const quoteAddressSummary = createAddressSummary(quoteAddress, null, 'Shipping address');\n $shippingForm.innerHTML = '';\n $shippingForm.appendChild(quoteAddressSummary);\n }\n\n removeOverlaySpinner(loaderRef, $loader);\n await displayCustomerAddressForms(data);\n }\n\n async function displayCustomerAddressForms(data) {\n if (isVirtualCart(data)) {\n shippingAddresses?.remove();\n shippingAddresses = null;\n $shippingForm.innerHTML = '';\n }\n\n if (!billingAddresses) {\n billingForm?.remove();\n billingForm = null;\n billingFormRef.current = null;\n\n billingAddresses = await renderCustomerBillingAddresses(\n $billingForm,\n billingFormRef,\n data,\n );\n }\n }\n\n async function handleCheckoutInitialized(data) {\n await initializeCheckout(data);\n }\n\n async function handleCheckoutUpdated(data) {\n await initializeCheckout(data);\n }\n\n function handleCheckoutValues(payload) {\n const { isBillToShipping } = payload;\n $billingForm.style.display = isBillToShipping ? 'none' : 'block';\n }\n\n async function handleOrderPlaced(orderData) {\n // Clear address form data\n sessionStorage.removeItem(BILLING_ADDRESS_DATA_KEY);\n\n const url = buildOrderDetailsUrl(orderData);\n\n window.history.pushState({}, '', url);\n\n await renderCheckoutSuccess(block, { orderData });\n }\n\n async function handlePurchaseOrderPlaced(poData) {\n // Clear address form data\n sessionStorage.removeItem(BILLING_ADDRESS_DATA_KEY);\n\n const url = rootLink(`${CUSTOMER_PO_DETAILS_PATH}?poRef=${poData?.uid}`);\n\n window.history.pushState({}, '', url);\n\n await renderPOSuccess(block, poData);\n }\n\n events.on('checkout/initialized', handleCheckoutInitialized, { eager: true });\n events.on('checkout/updated', handleCheckoutUpdated);\n events.on('checkout/values', handleCheckoutValues);\n events.on('order/placed', handleOrderPlaced);\n events.on('purchase-order/placed', handlePurchaseOrderPlaced);\n}\n",
|
|
280
|
+
"commerce-b2b-quote-checkout.css": ".checkout__content {\n display: grid;\n grid-template-columns: 1fr;\n gap: var(--spacing-big) 0;\n}\n\n.checkout__main {\n display: grid;\n row-gap: var(--spacing-xbig);\n margin-top: var(--spacing-medium);\n}\n\n.checkout__aside {\n display: grid;\n gap: var(--spacing-xbig);\n}\n\n.checkout-header h1 {\n margin: 0;\n}\n\n/* Block dividers */\n.checkout__block.checkout__heading .dropin-header-container {\n gap: var(--spacing-xsmall);\n}\n\n.checkout__shipping-form {\n padding-top: var(--spacing-xbig);\n border-top: var(--shape-border-width-3) solid var(--color-neutral-400);\n}\n\n.checkout__payment-methods {\n padding-top: var(--spacing-xbig);\n border-top: var(--shape-border-width-3) solid var(--color-neutral-400);\n padding-bottom: var(--spacing-xbig);\n border-bottom: var(--shape-border-width-3) solid var(--color-neutral-400);\n}\n\n/* Server error visibility */\n.checkout__server-error {\n display: none;\n}\n\n/* Show when it contains actual error content */\n.checkout__server-error:has(.dropin-illustrated-message) {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n}\n\n/* Safari fallback: show when not empty, but this may show empty divs briefly */\n@supports not selector(:has(*)) {\n .checkout__server-error:not(:empty) {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n }\n}\n\n/* Hide empty blocks */\n.checkout__block:empty {\n display: none;\n}\n\n/* Hide main containers when there is a server error */\n.checkout__content--error .checkout__login,\n.checkout__content--error .checkout__shipping-form,\n.checkout__content--error .checkout__bill-to-shipping,\n.checkout__content--error .checkout__delivery,\n.checkout__content--error .checkout__payment-methods,\n.checkout__content--error .checkout__billing-form,\n.checkout__content--error .checkout__terms-and-conditions {\n display: none !important;\n}\n\n/* Hide blocks with empty divs */\n.checkout__delivery:has(> div:first-child:empty),\n.checkout__bill-to-shipping:has(> :empty),\n.checkout__gift-options:has(.cart-gift-options-view--readonly:empty) {\n display: none;\n}\n\n/* Hide aside containers when there is a server error */\n.checkout__content--error .checkout__aside {\n display: none;\n}\n\n/* Place order button styling */\n.checkout__place-order {\n grid-column: unset;\n justify-items: unset;\n margin-top: calc(var(--spacing-big) * -1);\n}\n\n/* Hide the place order button when there is a server error */\n.checkout__content--error .checkout__place-order {\n display: none;\n}\n\n.checkout__loader {\n align-items: center;\n background: var(--color-neutral-50);\n display: flex;\n height: 100vh;\n justify-content: center;\n left: 0;\n opacity: 0.5;\n position: fixed;\n top: 0;\n width: 100%;\n z-index: 9999;\n}\n\n.checkout__loader:empty {\n display: none;\n}\n\n/* remove margin from the heading divider */\n.checkout__heading .dropin-divider {\n margin: 0;\n}\n\n/* Quote Summary */\n.checkout__block .quote-management-quote-summary-list {\n padding: var(--spacing-medium);\n}\n\n/* Order Summary Coupon */\n.dropin-accordion-section__heading {\n margin: var(--spacing-medium) auto;\n}\n\n.cart-coupons__accordion {\n margin-top: var(--spacing-xsmall);\n}\n\n/* temporary fix to hide the default quote summary list heading */\n[data-testid=\"default-quote-summary-list-heading\"] {\n display: none;\n}\n\n.quote-summary-list__heading {\n display: flex;\n justify-content: space-between;\n align-items: flex-end;\n}\n\n.quote-summary-list__heading-text {\n font: var(--type-headline-2-strong-font);\n letter-spacing: var(--type-headline-2-strong-letter-spacing);\n color: var(--color-neutral-800);\n}\n\n.quote-management-quote-summary-list__heading {\n row-gap: var(--spacing-small);\n padding-top: 0;\n}\n\n.quote-management-quote-summary-list__heading-text {\n font: var(--type-headline-2-strong-font);\n letter-spacing: var(--type-headline-2-strong-letter-spacing);\n color: var(--color-neutral-800);\n}\n\n.quote-summary-list__edit {\n font: var(--type-body-2-strong-font);\n letter-spacing: var(--type-body-2-strong-letter-spacing);\n}\n\n.checkout__block\n.quote-management-quote-summary-list\n.quote-management-quote-summary-list__footer-divider {\n margin: var(--spacing-small) 0;\n}\n\n/* Address form */\n.checkout__shipping-form .account-address-form-wrapper__title,\n.checkout__shipping-form .dropin-header-container__title,\n.checkout__billing-form .account-address-form-wrapper__title,\n.checkout__billing-form .dropin-header-container__title {\n font: var(--type-headline-2-default-font);\n letter-spacing: var(--type-headline-2-default-letter-spacing);\n color: var(--color-neutral-800);\n margin: 0 0 var(--spacing-medium) 0;\n}\n\n.checkout__shipping-form .dropin-header-container .dropin-divider,\n.checkout__billing-form .dropin-header-container .dropin-divider {\n display: none;\n}\n\n/* Read-only Address Summary Styles */\n.checkout__summary {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-xsmall);\n}\n\n.checkout__summary--inline {\n align-items: flex-start;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.checkout__summary-content {\n flex: 1;\n}\n\n.checkout__address-summary-content {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-xsmall);\n}\n\n.checkout__address-summary-details * {\n font: var(--type-body-1-default-font);\n letter-spacing: var(--type-body-1-default-letter-spacing);\n}\n\n.checkout__address-summary-note {\n font: var(--type-body-2-default-font);\n letter-spacing: var(--type-body-2-default-letter-spacing);\n color: var(--color-neutral-700);\n}\n\n@media only screen and (min-width: 320px) and (max-width: 768px) {\n .checkout__main,\n .checkout__aside {\n display: contents;\n }\n\n .checkout__block {\n order: 3;\n }\n\n .checkout__heading {\n order: 1;\n }\n\n .checkout__quote-summary {\n order: 2;\n }\n\n .checkout__place-order {\n order: 4;\n }\n}\n\n@media only screen and (min-width: 768px) {\n .checkout__content {\n display: grid;\n align-items: start;\n grid-template-columns: repeat(var(--grid-4-columns), 1fr);\n gap: var(--spacing-big) var(--grid-4-gutters);\n }\n\n .checkout__content--error {\n display: grid;\n grid-template-columns: 1fr;\n }\n\n .checkout__main {\n grid-column: 1 / span 7;\n row-gap: var(--spacing-xbig);\n }\n\n .checkout__aside {\n grid-column: 9 / span 4;\n gap: var(--spacing-xbig);\n }\n\n .checkout__place-order {\n margin-top: 0;\n }\n}\n",
|
|
281
|
+
"README.md": "# Commerce B2B Quote Checkout\n\n## Overview\n\nThe Commerce B2B Quote Checkout block provides a comprehensive **one-page checkout** experience for **negotiable quotes** with dynamic form handling, payment processing, billing address management, and order placement. It integrates multiple dropin containers with dynamic UI state management and validation.\n\n## Integration\n\n<!-- ### Block Configuration\n\nNo block configuration is read via `readBlockConfig()`. -->\n\n### URL Parameters\n\n - `quoteId` (required): The quote UID to check out (e.g., `?quoteId=<uid>`).\n\n<!-- ### Local Storage\n\nNo localStorage keys are used by this block. -->\n\n### Events\n\n#### Event Listeners\n\n- `events.on('authenticated', callback)` - Handles user authentication state changes\n- `events.on('checkout/initialized', callback)` - Handles checkout initialization with eager loading\n- `events.on('checkout/updated', callback)` - Handles checkout data updates\n- `events.on('checkout/values', callback)` - Handles checkout form value changes\n- `events.on('checkout/error', callback)` - Handles checkout error scenarios\n- `events.on('order/placed', callback)` - Handles successful order placement\n\n#### Event Emitters\n\n- `events.emit('checkout/addresses/billing', values)` - Emits billing address form values with debouncing\n\n## Behavior Patterns\n\n### Page Context Detection\n\n- **Unauthenticated Users**: When user is not authenticated, they will be redirected to the _/customer/login_ URL.\n- **Authenticated Users**: When user is authenticated, renders full checkout interface with shipping, billing, payment, and order summary\n- **No Permissions to Checkout Quote**: Displays an error when the user lacks permissions to proceed with checkout\n\n### User Interaction Flows\n\n1. **Initialization**: Block sets up meta tags, renders checkout layout, and initializes all containers\n2. **Address Management**: Users can enter billing addresses with real-time validation, while the shipping address is displayed based on the one set in the quote and cannot be edited.\n3. **Payment Processing**: Users can select payment methods and enter information with validation\n4. **Order Placement**: Users can place orders with comprehensive form validation and payment processing\n5. **Error Handling**: Block shows appropriate error states and recovery options for various failure scenarios\n\n### Error Handling\n\n- **Unable to Fetch Quote**: Displays an error when the quote cannot be retrieved\n- **Form Validation Errors**: Individual form validation with scroll-to-error functionality\n- **Payment Processing Errors**: Payment service error handling\n- **Server Errors**: Server error display with retry functionality\n- **Network Errors**: Graceful handling of network failures with user feedback\n- **Fallback Behavior**: Always falls back to appropriate error states with recovery options",
|
|
282
|
+
"constants.js": "// Form and address constants\nconst BILLING_FORM_NAME = 'selectedBillingAddress';\nconst BILLING_ADDRESS_DATA_KEY = `${BILLING_FORM_NAME}_addressData`;\nconst LOGIN_FORM_NAME = 'login-form';\nconst PURCHASE_ORDER_FORM_NAME = 'purchase-order';\nconst TERMS_AND_CONDITIONS_FORM_NAME = 'checkout-terms-and-conditions__form';\n\n// Timing constants\nconst DEBOUNCE_TIME = 1000;\nconst ADDRESS_INPUT_DEBOUNCE_TIME = 500;\n\n// Block and styling constants\nconst CHECKOUT_BLOCK = 'checkout__block';\nconst CHECKOUT_ERROR_CLASS = 'checkout__content--error';\nconst CHECKOUT_HEADER_CLASS = 'checkout-header';\n\n// Default values\nconst USER_TOKEN_COOKIE_NAME = 'auth_dropin_user_token';\n\nexport {\n // Form and address constants\n ADDRESS_INPUT_DEBOUNCE_TIME,\n BILLING_ADDRESS_DATA_KEY,\n BILLING_FORM_NAME,\n LOGIN_FORM_NAME,\n PURCHASE_ORDER_FORM_NAME,\n TERMS_AND_CONDITIONS_FORM_NAME,\n\n // Timing constants\n DEBOUNCE_TIME,\n\n // Block and styling constants\n CHECKOUT_BLOCK,\n CHECKOUT_ERROR_CLASS,\n CHECKOUT_HEADER_CLASS,\n\n // Default values\n USER_TOKEN_COOKIE_NAME,\n};\n",
|
|
283
|
+
"containers.js": "/* eslint-disable max-len */\n/* eslint-disable import/no-unresolved */\n/* eslint-disable no-unused-vars */\n/* eslint-disable no-shadow */\n/* eslint-disable no-use-before-define */\n/* eslint-disable prefer-const */\n\n// Checkout Dropin\nimport * as checkoutApi from '@dropins/storefront-checkout/api.js';\nimport BillToShippingAddress from '@dropins/storefront-checkout/containers/BillToShippingAddress.js';\nimport LoginForm from '@dropins/storefront-checkout/containers/LoginForm.js';\nimport PaymentMethods from '@dropins/storefront-checkout/containers/PaymentMethods.js';\nimport PlaceOrder from '@dropins/storefront-checkout/containers/PlaceOrder.js';\nimport ServerError from '@dropins/storefront-checkout/containers/ServerError.js';\nimport ShippingMethods from '@dropins/storefront-checkout/containers/ShippingMethods.js';\nimport TermsAndConditions from '@dropins/storefront-checkout/containers/TermsAndConditions.js';\nimport { render as CheckoutProvider } from '@dropins/storefront-checkout/render.js';\n\n// Auth Dropin\nimport * as authApi from '@dropins/storefront-auth/api.js';\n\n// Account Dropin\nimport Addresses from '@dropins/storefront-account/containers/Addresses.js';\nimport AddressForm from '@dropins/storefront-account/containers/AddressForm.js';\nimport { render as AccountProvider } from '@dropins/storefront-account/render.js';\n\n// Quote Management Dropin\nimport OrderSummary from '@dropins/storefront-quote-management/containers/OrderSummary.js';\nimport QuoteSummaryList from '@dropins/storefront-quote-management/containers/QuoteSummaryList.js';\nimport { render as QuoteManagementProvider } from '@dropins/storefront-quote-management/render.js';\n\n// Tools\nimport {\n Header,\n provider as UI,\n} from '@dropins/tools/components.js';\nimport { events } from '@dropins/tools/event-bus.js';\nimport { debounce } from '@dropins/tools/lib.js';\nimport { tryRenderAemAssetsImage } from '@dropins/tools/lib/aem/assets.js';\n\n// Checkout Dropin Libs\nimport {\n setAddressOnCart,\n getCartAddress,\n transformCartAddressToFormValues,\n} from '@dropins/storefront-checkout/lib/utils.js';\n\n// External dependencies\nimport { fetchPlaceholders, rootLink, CUSTOMER_NEGOTIABLE_QUOTE_PATH } from '../../scripts/commerce.js';\n\n// Constants\nimport {\n ADDRESS_INPUT_DEBOUNCE_TIME,\n BILLING_ADDRESS_DATA_KEY,\n BILLING_FORM_NAME,\n CHECKOUT_ERROR_CLASS,\n CHECKOUT_HEADER_CLASS,\n DEBOUNCE_TIME,\n LOGIN_FORM_NAME,\n} from './constants.js';\n\n/**\n * Container IDs for registry management\n * @enum {string}\n */\nexport const CONTAINERS = Object.freeze({\n // Static containers (rendered in Promise.all)\n CHECKOUT_HEADER: 'checkoutHeader',\n SERVER_ERROR: 'serverError',\n LOGIN_FORM: 'loginForm',\n SHIPPING_ADDRESS_FORM_SKELETON: 'shippingAddressFormSkeleton',\n BILL_TO_SHIPPING_ADDRESS: 'billToShippingAddress',\n SHIPPING_METHODS: 'shippingMethods',\n PAYMENT_METHODS: 'paymentMethods',\n BILLING_ADDRESS_FORM_SKELETON: 'billingAddressFormSkeleton',\n ORDER_SUMMARY: 'orderSummary',\n QUOTE_SUMMARY_LIST: 'quoteSummaryList',\n TERMS_AND_CONDITIONS: 'termsAndConditions',\n PLACE_ORDER_BUTTON: 'placeOrderButton',\n CUSTOMER_BILLING_ADDRESSES: 'customerBillingAddresses',\n\n // Dynamic containers (conditional rendering)\n BILLING_ADDRESS_FORM: 'billingAddressForm',\n});\n\n/**\n * A Map to store the API of rendered containers.\n * The key is a unique string ID, and the value is the containers's API object.\n * (e.g., { setProps: (props) => {...}, remove: () => {...} })\n */\nconst registry = new Map();\n\n/**\n * Checks if a container with the given ID has been rendered.\n * This is used to prevent multiple instances of the same container from being rendered.\n * @param {string} id - The unique ID of the container to check.\n * @returns {boolean} - Returns true if the container has been rendered, false otherwise.\n */\nexport const hasContainer = (id) => registry.has(id);\n\n/**\n * Helper to get a container from the registry or render and register it if not present.\n * @async\n * @param {string} id - Unique identifier for the container.\n * @param {Function} renderFn - Async function that renders the container.\n * @returns {Promise<Object>} - The rendered container API.\n */\nconst renderContainer = async (id, renderFn) => {\n if (registry.has(id)) {\n return registry.get(id);\n }\n\n try {\n const container = await renderFn();\n registry.set(id, container);\n return container;\n } catch (error) {\n console.error(`Error rendering container ${id}:`, error);\n throw error;\n }\n};\n\n/**\n * Unmounts and removes a container from the registry.\n * This function checks if the container is registered, removes it from the DOM,\n * and deletes its reference from the registry.\n * @param {string} id - The unique ID of the container to unmount.\n * @return {void}\n */\nexport const unmountContainer = (id) => {\n if (!registry.has(id)) {\n return;\n }\n\n const containerApi = registry.get(id);\n containerApi.remove();\n registry.delete(id);\n};\n\n/**\n * Renders the checkout page header with title and styling\n * @param {HTMLElement} container - DOM element to render the header in\n * @param {string} title - The title to display in the header\n * @returns {Promise<Object>} - The rendered checkout header component\n */\nexport const renderCheckoutHeader = async (container, title) => renderContainer(\n CONTAINERS.CHECKOUT_HEADER,\n async () => UI.render(Header, {\n className: CHECKOUT_HEADER_CLASS,\n divider: true,\n level: 1,\n size: 'large',\n title,\n })(container),\n);\n\n/**\n * Renders server error handling with retry functionality and error state management\n * @param {HTMLElement} container - DOM element to render the error component in\n * @param {HTMLElement} contentElement - Main content element to add error styling to\n * @returns {Promise<Object>} - The rendered server error component\n */\nexport const renderServerError = async (container, contentElement) => renderContainer(\n CONTAINERS.SERVER_ERROR,\n async () => CheckoutProvider.render(ServerError, {\n autoScroll: true,\n onRetry: (error) => {\n if (error.code === 'QUOTE_PERMISSION_DENIED' || error.code === 'QUOTE_DATA_ERROR') {\n document.location.reload();\n return;\n }\n\n contentElement.classList.remove(CHECKOUT_ERROR_CLASS);\n },\n onServerError: () => {\n contentElement.classList.add(CHECKOUT_ERROR_CLASS);\n },\n })(container),\n);\n\n/**\n * Renders the login form for guest checkout with authentication options\n * Uses the existing 'authenticated' event system for decoupled communication\n * @param {HTMLElement} container - DOM element to render the login form in\n * @returns {Promise<Object>} - The rendered login form component\n */\nexport const renderLoginForm = async (container) => renderContainer(\n CONTAINERS.LOGIN_FORM,\n async () => CheckoutProvider.render(LoginForm, {\n name: LOGIN_FORM_NAME,\n onSignOutClick: () => {\n authApi.revokeCustomerToken();\n },\n })(container),\n);\n\n/**\n * Renders the shipping address form skeleton (initial placeholder)\n * @param {HTMLElement} container - DOM element to render the form in\n * @returns {Promise<Object>} - The rendered shipping address form skeleton\n */\nexport const renderShippingAddressFormSkeleton = async (container) => renderContainer(\n CONTAINERS.SHIPPING_ADDRESS_FORM_SKELETON,\n async () => AccountProvider.render(AddressForm, {\n fieldIdPrefix: 'shipping',\n isOpen: true,\n showFormLoader: true,\n })(container),\n);\n\n/**\n * Renders the billing address form skeleton (initial placeholder)\n * @param {HTMLElement} container - DOM element to render the form in\n * @returns {Promise<Object>} - The rendered billing address form skeleton\n */\nexport const renderBillingAddressFormSkeleton = async (container) => renderContainer(\n CONTAINERS.BILLING_ADDRESS_FORM_SKELETON,\n async () => AccountProvider.render(AddressForm, {\n fieldIdPrefix: 'billing',\n isOpen: true,\n showFormLoader: true,\n })(container),\n);\n\n/**\n * Renders checkbox to set billing address same as shipping address - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render the checkbox in\n * @returns {Promise<Object>} - The rendered bill to shipping address component\n */\nexport const renderBillToShippingAddress = async (container) => renderContainer(\n CONTAINERS.BILL_TO_SHIPPING_ADDRESS,\n async () => {\n const setBillingAddressOnCart = setAddressOnCart({ type: 'billing' });\n\n return CheckoutProvider.render(BillToShippingAddress, {\n onChange: (checked) => {\n const billingFormValues = events.lastPayload('checkout/addresses/billing');\n\n if (!checked && billingFormValues) {\n setBillingAddressOnCart(billingFormValues);\n }\n },\n })(container);\n },\n);\n\n/**\n * Renders available shipping methods with selection interface\n * @param {HTMLElement} container - DOM element to render shipping methods in\n * @returns {Promise<Object>} - The rendered shipping methods component\n */\nexport const renderShippingMethods = async (container) => renderContainer(\n CONTAINERS.SHIPPING_METHODS,\n async () => CheckoutProvider.render(ShippingMethods)(container),\n);\n\n/**\n * Renders payment methods for B2B quote checkout\n * @param {HTMLElement} container - DOM element to render payment methods in\n * @returns {Promise<Object>} - The rendered payment methods component\n */\nexport const renderPaymentMethods = async (container) => renderContainer(\n CONTAINERS.PAYMENT_METHODS,\n async () => CheckoutProvider.render(PaymentMethods)(container),\n);\n\n/**\n * Renders terms and conditions with agreement slots and manual consent mode\n * @param {HTMLElement} container - DOM element to render the terms in\n * @returns {Promise<Object>} - The rendered terms and conditions component\n */\nexport const renderTermsAndConditions = async (container) => renderContainer(\n CONTAINERS.TERMS_AND_CONDITIONS,\n async () => CheckoutProvider.render(TermsAndConditions, {\n slots: {\n Agreements: (ctx) => {\n ctx.appendAgreement(() => ({\n name: 'default',\n mode: 'manual',\n translationId: 'Checkout.TermsAndConditions.label',\n }));\n },\n },\n })(container),\n);\n\n// ============================================================================\n// SUMMARY CONTAINERS\n// ============================================================================\n\n/**\n * Renders order summary\n * @param {HTMLElement} container - DOM element to render order summary in\n * @returns {Promise<Object>} - The rendered order summary component\n */\nexport const renderOrderSummary = async (container) => renderContainer(\n CONTAINERS.ORDER_SUMMARY,\n async () => QuoteManagementProvider.render(OrderSummary)(container),\n);\n\n/**\n * Renders quote summary list with custom heading and thumbnail slots\n * @param {HTMLElement} container - DOM element to render quote summary list in\n * @returns {Promise<Object>} - The rendered quote summary list component\n */\nexport const renderQuoteSummaryList = async (container) => renderContainer(\n CONTAINERS.QUOTE_SUMMARY_LIST,\n async () => QuoteManagementProvider.render(QuoteSummaryList, {\n variant: 'secondary',\n slots: {\n Heading: (headingCtx) => {\n const title = 'Your Quote ({count})';\n\n const quoteSummaryListHeading = document.createElement('div');\n quoteSummaryListHeading.classList.add('quote-summary-list__heading');\n\n const quoteSummaryListHeadingText = document.createElement('div');\n quoteSummaryListHeadingText.classList.add(\n 'quote-summary-list__heading-text',\n );\n\n quoteSummaryListHeadingText.innerText = title.replace(\n '({count})',\n headingCtx.count ? `(${headingCtx.count})` : '',\n );\n const editQuoteLink = document.createElement('a');\n editQuoteLink.classList.add('quote-summary-list__edit');\n editQuoteLink.href = rootLink(`${CUSTOMER_NEGOTIABLE_QUOTE_PATH}?quoteid=${headingCtx.quoteId}`);\n editQuoteLink.rel = 'noreferrer';\n editQuoteLink.innerText = 'Edit';\n\n quoteSummaryListHeading.appendChild(quoteSummaryListHeadingText);\n quoteSummaryListHeading.appendChild(editQuoteLink);\n headingCtx.appendChild(quoteSummaryListHeading);\n\n headingCtx.onChange((nextHeadingCtx) => {\n quoteSummaryListHeadingText.innerText = title.replace(\n '({count})',\n nextHeadingCtx.count ? `(${nextHeadingCtx.count})` : '',\n );\n });\n },\n Thumbnail: (ctx) => {\n const { item, defaultImageProps } = ctx;\n tryRenderAemAssetsImage(ctx, {\n alias: item.sku,\n imageProps: defaultImageProps,\n\n params: {\n width: defaultImageProps.width,\n height: defaultImageProps.height,\n },\n });\n },\n },\n })(container),\n);\n\n/**\n * Renders place order button with handler functions - follows multi-step pattern\n * @param {HTMLElement} container - DOM element to render the place order button in\n * @param {Object} options - Configuration object with handler functions\n * @param {Function} options.handleValidation - Validation handler function\n * @param {Function} options.handlePlaceOrder - Place order handler function\n * @param {Boolean} options.isPoEnabled - Indicate if PO enabled or not (B2B)\n * @returns {Promise<Object>} - The rendered place order component\n */\nexport const renderPlaceOrder = async (container, options = {}) => renderContainer(\n CONTAINERS.PLACE_ORDER_BUTTON,\n async () => CheckoutProvider.render(PlaceOrder, {\n handleValidation: options.handleValidation,\n handlePlaceOrder: options.handlePlaceOrder,\n slots: {\n Content: (placeOrderCtx) => {\n const spanElement = document.createElement('span');\n spanElement.innerText = options.isPoEnabled ? 'Place Purchase Order' : 'Place Order';\n placeOrderCtx.replaceWith(spanElement);\n },\n },\n })(container),\n);\n\n/**\n * Renders customer billing addresses selector/form for authenticated users - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render billing addresses in\n * @param {Object} formRef - React-style ref for form reference\n * @param {Object} data - Cart data containing billing address information\n * @returns {Promise<Object>} - The rendered customer billing addresses component\n */\nexport const renderCustomerBillingAddresses = async (container, formRef, data) => renderContainer(\n CONTAINERS.CUSTOMER_BILLING_ADDRESSES,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n const cartBillingAddress = getCartAddress(data, 'billing');\n\n const customerBillingAddressUid = cartBillingAddress\n ? cartBillingAddress?.customerAddressUid ?? 0\n : undefined;\n\n const billingAddressCache = sessionStorage.getItem(BILLING_ADDRESS_DATA_KEY);\n\n // Clear persisted billing address if cart has a billing address\n if (cartBillingAddress && billingAddressCache) {\n sessionStorage.removeItem(BILLING_ADDRESS_DATA_KEY);\n }\n\n const storeConfig = checkoutApi.getStoreConfigCache();\n\n const inputsDefaultValueSet = cartBillingAddress && cartBillingAddress.customerAddressUid === undefined\n ? transformCartAddressToFormValues(cartBillingAddress)\n : { countryCode: storeConfig.defaultCountry };\n\n const hasCartBillingAddress = Boolean(data.billingAddress);\n let isFirstRenderBilling = true;\n\n const setBillingAddressOnCart = setAddressOnCart({\n type: 'billing',\n debounceMs: DEBOUNCE_TIME,\n });\n\n const notifyBillingValues = debounce((values) => {\n events.emit('checkout/addresses/billing', values);\n }, ADDRESS_INPUT_DEBOUNCE_TIME);\n\n return AccountProvider.render(Addresses, {\n addressFormTitle: placeholders?.Checkout?.Addresses?.billToNewAddress,\n defaultSelectAddressId: customerBillingAddressUid,\n formName: BILLING_FORM_NAME,\n forwardFormRef: formRef,\n inputsDefaultValueSet,\n minifiedView: false,\n onAddressData: (values) => {\n const canSetBillingAddressOnCart = !isFirstRenderBilling || !hasCartBillingAddress;\n if (canSetBillingAddressOnCart) setBillingAddressOnCart(values);\n if (isFirstRenderBilling) isFirstRenderBilling = false;\n notifyBillingValues(values);\n },\n selectable: true,\n selectBilling: true,\n showBillingCheckBox: false,\n showSaveCheckBox: true,\n showShippingCheckBox: false,\n title: placeholders?.Checkout?.Addresses?.billingAddressTitle,\n })(container);\n },\n);\n",
|
|
284
|
+
"fragments.js": "// eslint-disable-next-line import/no-unresolved\nimport { createFragment } from '@dropins/storefront-checkout/lib/utils.js';\n\nimport { CHECKOUT_BLOCK } from './constants.js';\n\n/**\n * A frozen, nested object of CSS selectors\n * @readonly\n */\nexport const selectors = Object.freeze({\n checkout: {\n content: '.checkout__content',\n loader: '.checkout__loader',\n heading: '.checkout__heading',\n serverError: '.checkout__server-error',\n login: '.checkout__login',\n shippingForm: '.checkout__shipping-form',\n billToShipping: '.checkout__bill-to-shipping',\n delivery: '.checkout__delivery',\n paymentMethods: '.checkout__payment-methods',\n billingForm: '.checkout__billing-form',\n orderSummary: '.checkout__order-summary',\n quoteSummary: '.checkout__quote-summary',\n placeOrder: '.checkout__place-order',\n termsAndConditions: '.checkout__terms-and-conditions',\n main: '.checkout__main',\n aside: '.checkout__aside',\n },\n});\n\n// =============================================================================\n// CHECKOUT\n// =============================================================================\n\n/**\n * Creates the main checkout fragment with all checkout blocks.\n * @returns {DocumentFragment} The complete checkout fragment.\n */\nexport function createCheckoutFragment() {\n return createFragment(`\n <div class=\"checkout__wrapper\">\n <div class=\"checkout__loader\"></div>\n <div class=\"checkout__content\">\n <div class=\"checkout__main\">\n <div class=\"checkout__heading ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__server-error ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__login ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__shipping-form ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__bill-to-shipping ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__delivery ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__payment-methods ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__billing-form ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__terms-and-conditions ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__place-order ${CHECKOUT_BLOCK}\"></div>\n </div>\n <div class=\"checkout__aside\">\n <div class=\"checkout__order-summary ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__quote-summary ${CHECKOUT_BLOCK}\"></div>\n </div>\n </div>\n </div>\n `);\n}\n\n/**\n * Creates a generic summary container with standardized classes.\n * @param {Element|DocumentFragment} content - The content node to include inside the summary.\n * @returns {HTMLElement} The summary root element.\n */\nexport const createSummary = (content) => {\n const summaryDiv = document.createElement('div');\n summaryDiv.className = 'checkout__summary checkout__summary--inline';\n\n const contentDiv = document.createElement('div');\n contentDiv.className = 'checkout__summary-content';\n contentDiv.appendChild(content);\n\n summaryDiv.appendChild(contentDiv);\n\n return summaryDiv;\n};\n\n/**\n * Creates a read-only address summary element for B2B negotiable quotes.\n * Accepts Address model shape from Negotiable Quote transforms.\n * @param {Object} [data={}] - Address data in Negotiable Quote Address shape.\n * @param {string|null} [note=null] - Optional note to display under the address.\n * @param {string|null} [title=null] - Optional title to display above the address.\n * @returns {HTMLElement} The address summary element with optional title.\n */\nexport function createAddressSummary(data = {}, note = null, title = null) {\n const {\n firstName = '',\n lastName = '',\n street = [],\n city = '',\n region, // Region object: { code, name } | undefined\n postCode = '',\n country, // Country object: { code, label } | undefined\n telephone = '',\n } = data || {};\n\n const streetAddress = Array.isArray(street) ? street.join(', ') : street;\n const regionCode = region?.code || region?.name || '';\n const countryCode = country?.code || '';\n\n const detailsDiv = document.createElement('div');\n detailsDiv.className = 'checkout__address-summary-details';\n\n const nameDiv = document.createElement('div');\n nameDiv.textContent = `${firstName} ${lastName}`.trim();\n detailsDiv.appendChild(nameDiv);\n\n if (streetAddress) {\n const streetDiv = document.createElement('div');\n streetDiv.textContent = streetAddress;\n detailsDiv.appendChild(streetDiv);\n }\n\n const cityDiv = document.createElement('div');\n cityDiv.textContent = [city, regionCode].filter(Boolean).join(', ') + (postCode ? ` ${postCode}` : '');\n detailsDiv.appendChild(cityDiv);\n\n const countryDiv = document.createElement('div');\n countryDiv.textContent = countryCode;\n detailsDiv.appendChild(countryDiv);\n\n if (telephone) {\n const telDiv = document.createElement('div');\n telDiv.textContent = telephone;\n detailsDiv.appendChild(telDiv);\n }\n\n const contentDiv = document.createElement('div');\n contentDiv.className = 'checkout__address-summary-content';\n contentDiv.appendChild(detailsDiv);\n\n const noteText = note || '⚠️ This shipping address is part of the Negotiated Quote and cannot be changed during checkout.';\n if (noteText) {\n const noteDiv = document.createElement('div');\n noteDiv.className = 'checkout__address-summary-note';\n noteDiv.textContent = noteText;\n contentDiv.appendChild(noteDiv);\n }\n\n if (title) {\n const wrapperDiv = document.createElement('div');\n\n const titleDiv = document.createElement('div');\n titleDiv.setAttribute('data-testid', 'addressesFormTitle');\n titleDiv.className = 'account-address-form-wrapper__title';\n titleDiv.textContent = title;\n\n wrapperDiv.appendChild(titleDiv);\n wrapperDiv.appendChild(createSummary(contentDiv));\n\n return wrapperDiv;\n }\n\n return createSummary(contentDiv);\n}\n",
|
|
285
|
+
"utils.js": "/* eslint-disable import/no-unresolved */\nimport { ProgressSpinner, provider as UI } from '@dropins/tools/components.js';\nimport { tryRenderAemAssetsImage } from '@dropins/tools/lib/aem/assets.js';\nimport { ORDER_DETAILS_PATH, rootLink } from '../../scripts/commerce.js';\nimport { getUserTokenCookie } from '../../scripts/initializers/index.js';\n\n/**\n * Displays an overlay spinner in the specified container\n * @param {Object} loaderRef - Ref object to store the spinner component\n * @param {HTMLElement} $loader - DOM element to render the spinner in\n */\nexport const displayOverlaySpinner = async (loaderRef, $loader) => {\n if (loaderRef.current) return;\n\n loaderRef.current = await UI.render(ProgressSpinner, {\n className: '.checkout__overlay-spinner',\n })($loader);\n};\n\n/**\n * Removes the overlay spinner and cleans up references\n * @param {Object} loaderRef - Ref object containing the spinner component\n * @param {HTMLElement} $loader - DOM element containing the spinner\n */\nexport const removeOverlaySpinner = (loaderRef, $loader) => {\n if (!loaderRef.current) return;\n\n loaderRef.current.remove();\n loaderRef.current = null;\n $loader.innerHTML = '';\n};\n\n/**\n * Renders AEM asset images for gift option swatches\n * @param {Object} ctx - The context object containing imageSwatchContext and defaultImageProps\n */\nexport function swatchImageSlot(ctx) {\n const { imageSwatchContext, defaultImageProps } = ctx;\n tryRenderAemAssetsImage(ctx, {\n alias: imageSwatchContext.label,\n imageProps: defaultImageProps,\n wrapper: document.createElement('span'),\n params: {\n width: defaultImageProps.width,\n height: defaultImageProps.height,\n },\n });\n}\n\n/**\n * Builds the order details URL based on authentication status\n * @param {Object} orderData - Order data containing number and token\n * @param {string} orderDetailsPath - Path to the order details page\n * @returns {string} The constructed order details URL\n */\nexport function buildOrderDetailsUrl(orderData, orderDetailsPath = ORDER_DETAILS_PATH) {\n const token = getUserTokenCookie();\n const orderRef = token ? orderData.number : orderData.token;\n const orderNumber = orderData.number;\n const encodedOrderRef = encodeURIComponent(orderRef);\n const encodedOrderNumber = encodeURIComponent(orderNumber);\n\n return token\n ? rootLink(`${orderDetailsPath}?orderRef=${encodedOrderRef}`)\n : rootLink(`${orderDetailsPath}?orderRef=${encodedOrderRef}&orderNumber=${encodedOrderNumber}`);\n}\n"
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
"name": "commerce-checkout",
|
|
290
|
+
"description": "Example commerce-checkout block for storefront-checkout",
|
|
291
|
+
"files": {
|
|
292
|
+
"commerce-checkout.js": "/* eslint-disable import/no-unresolved */\n/* eslint-disable no-unused-vars */\n\n// Dropin Tools\nimport { events } from '@dropins/tools/event-bus.js';\nimport { initReCaptcha } from '@dropins/tools/recaptcha.js';\n\n// Order Dropin Modules\nimport * as orderApi from '@dropins/storefront-order/api.js';\n\n// Checkout Dropin Libraries\nimport {\n createScopedSelector,\n isVirtualCart,\n setMetaTags,\n validateForms,\n} from '@dropins/storefront-checkout/lib/utils.js';\n\n// Payment Services Dropin\nimport { PaymentMethodCode } from '@dropins/storefront-payment-services/api.js';\n\n// Block Utilities\nimport {\n buildOrderDetailsUrl,\n displayOverlaySpinner,\n removeOverlaySpinner,\n} from './utils.js';\n\n// Fragment functions\nimport {\n createCheckoutFragment,\n selectors,\n} from './fragments.js';\n\n// Container functions\nimport {\n renderAddressForm,\n renderBillingAddressFormSkeleton,\n renderBillToShippingAddress,\n renderCartSummaryList,\n renderCheckoutHeader,\n renderCustomerBillingAddresses,\n renderCustomerShippingAddresses,\n renderGiftOptions,\n renderLoginForm,\n renderMergedCartBanner,\n renderOrderSummary,\n renderOutOfStock,\n renderPaymentMethods,\n renderPlaceOrder,\n renderServerError,\n renderShippingAddressFormSkeleton,\n renderShippingMethods,\n renderTermsAndConditions,\n} from './containers.js';\n\n// Constants\nimport {\n BILLING_ADDRESS_DATA_KEY,\n BILLING_FORM_NAME,\n LOGIN_FORM_NAME,\n PURCHASE_ORDER_FORM_NAME,\n SHIPPING_ADDRESS_DATA_KEY,\n SHIPPING_FORM_NAME,\n TERMS_AND_CONDITIONS_FORM_NAME,\n} from './constants.js';\n\nimport { rootLink } from '../../scripts/commerce.js';\n\n// Initializers\nimport '../../scripts/initializers/account.js';\nimport '../../scripts/initializers/checkout.js';\nimport '../../scripts/initializers/order.js';\nimport '../../scripts/initializers/payment-services.js';\n\n// Checkout success block import and CSS preload\nimport { renderCheckoutSuccess, preloadCheckoutSuccess } from '../commerce-checkout-success/commerce-checkout-success.js';\n\npreloadCheckoutSuccess();\n\nfunction redirectToCartIfEmpty(cartData) {\n const isOrderPlaced = events.lastPayload('order/placed') !== undefined;\n\n if (!isOrderPlaced && (cartData === null || cartData?.items?.length === 0)) {\n window.location.href = rootLink('/cart');\n }\n}\n\nexport default async function decorate(block) {\n setMetaTags('Checkout');\n document.title = 'Checkout';\n\n const cartData = events.lastPayload('cart/initialized');\n redirectToCartIfEmpty(cartData);\n\n // Container and component references\n let shippingForm;\n let billingForm;\n let shippingAddresses;\n let billingAddresses;\n\n const shippingFormRef = { current: null };\n const billingFormRef = { current: null };\n const creditCardFormRef = { current: null };\n const loaderRef = { current: null };\n\n events.on('order/placed', () => {\n setMetaTags('Order Confirmation');\n document.title = 'Order Confirmation';\n });\n\n // Create the checkout layout using fragments\n const checkoutFragment = createCheckoutFragment();\n\n // Create scoped selector for the checkout fragment\n const getElement = createScopedSelector(checkoutFragment);\n\n // Get all checkout elements using centralized selectors\n const $content = getElement(selectors.checkout.content);\n const $loader = getElement(selectors.checkout.loader);\n const $mergedCartBanner = getElement(selectors.checkout.mergedCartBanner);\n const $heading = getElement(selectors.checkout.heading);\n const $serverError = getElement(selectors.checkout.serverError);\n const $outOfStock = getElement(selectors.checkout.outOfStock);\n const $login = getElement(selectors.checkout.login);\n const $shippingForm = getElement(selectors.checkout.shippingForm);\n const $billToShipping = getElement(selectors.checkout.billToShipping);\n const $delivery = getElement(selectors.checkout.delivery);\n const $paymentMethods = getElement(selectors.checkout.paymentMethods);\n const $billingForm = getElement(selectors.checkout.billingForm);\n const $orderSummary = getElement(selectors.checkout.orderSummary);\n const $cartSummary = getElement(selectors.checkout.cartSummary);\n const $placeOrder = getElement(selectors.checkout.placeOrder);\n const $giftOptions = getElement(selectors.checkout.giftOptions);\n const $termsAndConditions = getElement(selectors.checkout.termsAndConditions);\n\n block.appendChild(checkoutFragment);\n\n const handleValidation = () => validateForms([\n { name: LOGIN_FORM_NAME },\n { name: SHIPPING_FORM_NAME, ref: shippingFormRef },\n { name: BILLING_FORM_NAME, ref: billingFormRef },\n { name: PURCHASE_ORDER_FORM_NAME },\n { name: TERMS_AND_CONDITIONS_FORM_NAME },\n ]);\n\n const handlePlaceOrder = async ({ cartId, code }) => {\n await displayOverlaySpinner(loaderRef, $loader);\n try {\n // Payment Services credit card\n if (code === PaymentMethodCode.CREDIT_CARD) {\n if (!creditCardFormRef.current) {\n console.error('Credit card form not rendered.');\n return;\n }\n if (!creditCardFormRef.current.validate()) {\n // Credit card form invalid; abort order placement\n return;\n }\n // Submit Payment Services credit card form\n await creditCardFormRef.current.submit();\n }\n // Place order\n await orderApi.placeOrder(cartId);\n } catch (error) {\n console.error(error);\n throw error;\n } finally {\n removeOverlaySpinner(loaderRef, $loader);\n }\n };\n\n // First, render the place order component\n await renderPlaceOrder($placeOrder, { handleValidation, handlePlaceOrder });\n\n // Render the remaining containers\n const [\n _mergedCartBanner,\n _header,\n _serverError,\n _outOfStock,\n _loginForm,\n shippingFormSkeleton,\n _billToShipping,\n _shippingMethods,\n _paymentMethods,\n billingFormSkeleton,\n _orderSummary,\n _cartSummary,\n _termsAndConditions,\n _giftOptions,\n ] = await Promise.all([\n renderMergedCartBanner($mergedCartBanner),\n\n renderCheckoutHeader($heading, 'Checkout'),\n\n renderServerError($serverError, $content),\n\n renderOutOfStock($outOfStock),\n\n renderLoginForm($login),\n\n renderShippingAddressFormSkeleton($shippingForm),\n\n renderBillToShippingAddress($billToShipping),\n\n renderShippingMethods($delivery),\n\n renderPaymentMethods($paymentMethods, creditCardFormRef),\n\n renderBillingAddressFormSkeleton($billingForm),\n\n renderOrderSummary($orderSummary),\n\n renderCartSummaryList($cartSummary),\n\n renderTermsAndConditions($termsAndConditions),\n\n renderGiftOptions($giftOptions),\n ]);\n\n async function initializeCheckout(data) {\n await initReCaptcha(0);\n if (data.isGuest) await displayGuestAddressForms(data);\n else {\n removeOverlaySpinner(loaderRef, $loader);\n await displayCustomerAddressForms(data);\n }\n }\n\n async function displayGuestAddressForms(data) {\n if (isVirtualCart(data)) {\n shippingForm?.remove();\n shippingForm = null;\n $shippingForm.innerHTML = '';\n } else if (!shippingForm) {\n shippingFormSkeleton.remove();\n\n shippingForm = await renderAddressForm($shippingForm, shippingFormRef, data, 'shipping');\n }\n\n if (!billingForm) {\n billingFormSkeleton.remove();\n\n billingForm = await renderAddressForm($billingForm, billingFormRef, data, 'billing');\n }\n }\n\n async function displayCustomerAddressForms(data) {\n if (isVirtualCart(data)) {\n shippingAddresses?.remove();\n shippingAddresses = null;\n $shippingForm.innerHTML = '';\n } else if (!shippingAddresses) {\n shippingForm?.remove();\n shippingForm = null;\n shippingFormRef.current = null;\n\n shippingAddresses = await renderCustomerShippingAddresses(\n $shippingForm,\n shippingFormRef,\n data,\n );\n }\n\n if (!billingAddresses) {\n billingForm?.remove();\n billingForm = null;\n billingFormRef.current = null;\n\n billingAddresses = await renderCustomerBillingAddresses(\n $billingForm,\n billingFormRef,\n data,\n );\n }\n }\n\n async function handleCheckoutUpdated(data) {\n if (!data) return;\n await initializeCheckout(data);\n }\n\n function handleAuthenticated(authenticated) {\n if (!authenticated) return;\n\n // When a customer creates an account on the checkout success page and then\n // signs in, they will be redirected to the order details page with the order\n // number as orderRef, allowing the order details to be displayed\n const orderData = events.lastPayload('order/placed');\n if (orderData) {\n const url = buildOrderDetailsUrl(orderData);\n window.history.pushState({}, '', url);\n }\n\n window.location.reload();\n }\n\n function handleCheckoutValues(payload) {\n const { isBillToShipping } = payload;\n $billingForm.style.display = isBillToShipping ? 'none' : 'block';\n }\n\n async function handleOrderPlaced(orderData) {\n // Clear address form data\n sessionStorage.removeItem(SHIPPING_ADDRESS_DATA_KEY);\n sessionStorage.removeItem(BILLING_ADDRESS_DATA_KEY);\n\n const url = buildOrderDetailsUrl(orderData);\n\n window.history.pushState({}, '', url);\n\n await renderCheckoutSuccess(block, { orderData });\n }\n\n events.on('authenticated', handleAuthenticated);\n events.on('checkout/initialized', handleCheckoutUpdated, { eager: true });\n events.on('checkout/updated', handleCheckoutUpdated);\n events.on('checkout/values', handleCheckoutValues);\n events.on('order/placed', handleOrderPlaced);\n events.on('cart/initialized', redirectToCartIfEmpty, { eager: true });\n events.on('cart/data', redirectToCartIfEmpty);\n}\n",
|
|
293
|
+
"commerce-checkout.css": ".checkout__content {\n display: grid;\n grid-template-columns: 1fr;\n gap: var(--spacing-big) 0;\n}\n\n.checkout__merged-cart-banner {\n display: grid;\n grid-column: 1 / -1;\n align-items: start;\n grid-template-columns: auto;\n}\n\n.checkout__main {\n display: grid;\n row-gap: var(--spacing-xbig);\n margin-top: var(--spacing-medium);\n}\n\n.checkout__aside {\n display: grid;\n gap: var(--spacing-xbig);\n}\n\n.checkout-header h1 {\n margin: 0;\n}\n\n/* Block dividers */\n.checkout__block.checkout__heading .dropin-header-container {\n gap: var(--spacing-xsmall);\n}\n\n.checkout__shipping-form {\n padding-top: var(--spacing-xbig);\n border-top: var(--shape-border-width-3) solid var(--color-neutral-400);\n}\n\n.checkout__payment-methods {\n padding-top: var(--spacing-xbig);\n border-top: var(--shape-border-width-3) solid var(--color-neutral-400);\n padding-bottom: var(--spacing-xbig);\n border-bottom: var(--shape-border-width-3) solid var(--color-neutral-400);\n}\n\n/* Server error visibility */\n.checkout__server-error {\n display: none;\n}\n\n/* Show when it contains actual error content */\n.checkout__server-error:has(.dropin-illustrated-message) {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n}\n\n/* Safari fallback: show when not empty, but this may show empty divs briefly */\n@supports not selector(:has(*)) {\n .checkout__server-error:not(:empty) {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n }\n}\n\n/* Hide empty blocks */\n.checkout__block:empty {\n display: none;\n}\n\n/* Hide main containers when there is a server error */\n.checkout__content--error .checkout__merged-cart-banner,\n.checkout__content--error .checkout__out-of-stock,\n.checkout__content--error .checkout__login,\n.checkout__content--error .checkout__shipping-form,\n.checkout__content--error .checkout__bill-to-shipping,\n.checkout__content--error .checkout__delivery,\n.checkout__content--error .checkout__payment-methods,\n.checkout__content--error .checkout__billing-form,\n.checkout__content--error .checkout__terms-and-conditions {\n display: none !important;\n}\n\n/* Hide blocks with empty divs */\n.checkout__out-of-stock:has(> :empty),\n.checkout__merged-cart-banner:has(> :empty),\n.checkout__delivery:has(> div:first-child:empty),\n.checkout__bill-to-shipping:has(> :empty),\n.checkout__gift-options:has(.cart-gift-options-view--readonly:empty) {\n display: none;\n}\n\n/* Hide aside containers when there is a server error */\n.checkout__content--error .checkout__aside {\n display: none;\n}\n\n/* Integrate place order button into Order Summary - mobile */\n.checkout__place-order {\n grid-column: unset;\n justify-items: unset;\n margin-top: calc(var(--spacing-big) * -1);\n}\n\n/* Hide the place order button when there is a server error */\n.checkout__content--error .checkout__place-order {\n display: none;\n}\n\n.checkout__loader {\n align-items: center;\n background: var(--color-neutral-50);\n display: flex;\n height: 100vh;\n justify-content: center;\n left: 0;\n opacity: 0.5;\n position: fixed;\n top: 0;\n width: 100%;\n z-index: 9999;\n}\n\n.checkout__loader:empty {\n display: none;\n}\n\n/* remove margin from the heading divider */\n.checkout__heading .dropin-divider {\n margin: 0;\n}\n\n/* Cart Summary */\n.checkout__block .cart-cart-summary-list {\n padding: var(--spacing-medium);\n}\n\n/* Order Summary Coupon */\n.dropin-accordion-section__heading {\n margin: var(--spacing-medium) auto;\n}\n\n.cart-coupons__accordion {\n margin-top: var(--spacing-xsmall);\n}\n\n/* temporary fix to hide the default cart heading */\n[data-testid=\"default-cart-heading\"] {\n display: none;\n}\n\n.cart-summary-list__heading {\n display: flex;\n justify-content: space-between;\n align-items: flex-end;\n}\n\n.cart-summary-list__heading-text {\n font: var(--type-headline-2-strong-font);\n letter-spacing: var(--type-headline-2-strong-letter-spacing);\n color: var(--color-neutral-800);\n}\n\n.cart-cart-summary-list__heading {\n row-gap: var(--spacing-small);\n padding-top: 0;\n}\n\n.cart-cart-summary-list__heading-text {\n font: var(--type-headline-2-strong-font);\n letter-spacing: var(--type-headline-2-strong-letter-spacing);\n color: var(--color-neutral-800);\n}\n\n.cart-summary-list__edit {\n font: var(--type-body-2-strong-font);\n letter-spacing: var(--type-body-2-strong-letter-spacing);\n}\n\n.checkout__block\n.cart-cart-summary-list\n.cart-cart-summary-list__footer-divider {\n margin: var(--spacing-small) 0;\n}\n\n/* Sign-in modal */\n#modal {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-color: rgb(0 0 0 / 50%);\n display: flex;\n justify-content: center;\n align-items: center;\n z-index: 2;\n}\n\n#modal-form {\n width: 800px;\n}\n\n/* Address form */\n.checkout__shipping-form .account-address-form-wrapper__title,\n.checkout__shipping-form .dropin-header-container__title,\n.checkout__billing-form .account-address-form-wrapper__title,\n.checkout__billing-form .dropin-header-container__title {\n font: var(--type-headline-2-default-font);\n letter-spacing: var(--type-headline-2-default-letter-spacing);\n color: var(--color-neutral-800);\n margin: 0 0 var(--spacing-medium) 0;\n}\n\n.checkout__shipping-form .dropin-header-container .dropin-divider,\n.checkout__billing-form .dropin-header-container .dropin-divider {\n display: none;\n}\n\n@media only screen and (min-width: 320px) and (max-width: 768px) {\n .checkout__main,\n .checkout__aside {\n display: contents;\n }\n\n .checkout__block {\n order: 3;\n }\n\n .checkout__heading {\n order: 1;\n }\n\n .checkout__cart-summary {\n order: 2;\n }\n\n .checkout__place-order {\n order: 4;\n }\n}\n\n@media only screen and (min-width: 768px) {\n .checkout__content {\n display: grid;\n align-items: start;\n grid-template-columns: repeat(var(--grid-4-columns), 1fr);\n gap: var(--spacing-big) var(--grid-4-gutters);\n }\n\n .checkout__content--error {\n display: grid;\n grid-template-columns: 1fr;\n }\n\n .checkout__main {\n grid-column: 1 / span 7;\n row-gap: var(--spacing-xbig);\n }\n\n .checkout__aside {\n grid-column: 9 / span 4;\n gap: var(--spacing-xbig);\n }\n\n .checkout__place-order {\n margin-top: 0;\n }\n}\n",
|
|
294
|
+
"README.md": "# Commerce Checkout Block\n\n## Overview\n\nThe Commerce Checkout block provides a comprehensive **one-page checkout** experience with dynamic form handling, payment processing, address management, and order placement. It integrates multiple dropin containers for authentication, cart management, payment services, and order processing with dynamic UI state management and validation.\n\n## Integration\n\n<!-- ### Block Configuration\n\nNo block configuration is read via `readBlockConfig()`. -->\n\n### URL Parameters\n\nNo URL parameters are directly read, but the block uses `window.location.href` for meta tag management and page title updates.\n\n<!-- ### Local Storage\n\nNo localStorage keys are used by this block. -->\n\n### Events\n\n#### Event Listeners\n\n- `events.on('authenticated', callback)` - Handles user authentication state changes\n- `events.on('cart/initialized', callback)` - Handles cart initialization with eager loading\n- `events.on('checkout/initialized', callback)` - Handles checkout initialization with eager loading\n- `events.on('checkout/updated', callback)` - Handles checkout data updates\n- `events.on('checkout/values', callback)` - Handles checkout form value changes\n- `events.on('order/placed', callback)` - Handles successful order placement\n\n#### Event Emitters\n\n- `events.emit('checkout/addresses/shipping', values)` - Emits shipping address form values with debouncing\n- `events.emit('checkout/addresses/billing', values)` - Emits billing address form values with debouncing\n\n## Behavior Patterns\n\n### Page Context Detection\n\n- **Checkout Flow**: Renders full checkout interface with shipping, billing, payment, and order summary\n- **Empty Cart**: When cart is empty, redirects to the cart page\n- **Server Errors**: When server errors occur, shows error state and hides checkout forms\n- **Out of Stock**: When items are out of stock, shows out of stock message with cart update options\n- **Order Confirmation**: After successful order placement, transitions to order confirmation view\n\n### User Interaction Flows\n\n1. **Initialization**: Block sets up meta tags, renders checkout layout, and initializes all containers\n2. **Authentication**: Users can sign in/out via modal with form validation and success callbacks\n3. **Address Management**: Users can enter shipping/billing addresses with real-time validation and cart updates\n4. **Payment Processing**: Users can select payment methods and enter credit card information with validation\n5. **Order Placement**: Users can place orders with comprehensive form validation and payment processing\n6. **Error Handling**: Block shows appropriate error states and recovery options for various failure scenarios\n\n### Error Handling\n\n- **Form Validation Errors**: Individual form validation with scroll-to-error functionality\n- **Payment Processing Errors**: Credit card validation and payment service error handling\n- **Server Errors**: Server error display with retry functionality\n- **Cart Errors**: Empty cart and out-of-stock item handling\n- **Network Errors**: Graceful handling of network failures with user feedback\n- **Fallback Behavior**: Always falls back to appropriate error states with recovery options",
|
|
295
|
+
"constants.js": "// Form and address constants\nconst BILLING_FORM_NAME = 'selectedBillingAddress';\nconst BILLING_ADDRESS_DATA_KEY = `${BILLING_FORM_NAME}_addressData`;\nconst LOGIN_FORM_NAME = 'login-form';\nconst PURCHASE_ORDER_FORM_NAME = 'purchase-order';\nconst SHIPPING_FORM_NAME = 'selectedShippingAddress';\nconst SHIPPING_ADDRESS_DATA_KEY = `${SHIPPING_FORM_NAME}_addressData`;\nconst TERMS_AND_CONDITIONS_FORM_NAME = 'checkout-terms-and-conditions__form';\n\n// Timing constants\nconst DEBOUNCE_TIME = 1000;\nconst ADDRESS_INPUT_DEBOUNCE_TIME = 500;\n\n// Block and styling constants\nconst CHECKOUT_BLOCK = 'checkout__block';\nconst CHECKOUT_ERROR_CLASS = 'checkout__content--error';\nconst CHECKOUT_HEADER_CLASS = 'checkout-header';\nconst ORDER_CONFIRMATION_BLOCK = 'order-confirmation__block';\n\nexport {\n ADDRESS_INPUT_DEBOUNCE_TIME,\n BILLING_ADDRESS_DATA_KEY,\n BILLING_FORM_NAME,\n LOGIN_FORM_NAME,\n PURCHASE_ORDER_FORM_NAME,\n SHIPPING_ADDRESS_DATA_KEY,\n SHIPPING_FORM_NAME,\n TERMS_AND_CONDITIONS_FORM_NAME,\n\n // Timing constants\n DEBOUNCE_TIME,\n\n // Block and styling constants\n CHECKOUT_BLOCK,\n CHECKOUT_ERROR_CLASS,\n CHECKOUT_HEADER_CLASS,\n ORDER_CONFIRMATION_BLOCK,\n};\n",
|
|
296
|
+
"containers.js": "/* eslint-disable max-len */\n/* eslint-disable import/no-unresolved */\n/* eslint-disable no-unused-vars */\n/* eslint-disable no-shadow */\n/* eslint-disable no-use-before-define */\n/* eslint-disable prefer-const */\n\n// Checkout Dropin\nimport * as checkoutApi from '@dropins/storefront-checkout/api.js';\nimport BillToShippingAddress from '@dropins/storefront-checkout/containers/BillToShippingAddress.js';\nimport EstimateShipping from '@dropins/storefront-checkout/containers/EstimateShipping.js';\nimport LoginForm from '@dropins/storefront-checkout/containers/LoginForm.js';\nimport MergedCartBanner from '@dropins/storefront-checkout/containers/MergedCartBanner.js';\nimport OutOfStock from '@dropins/storefront-checkout/containers/OutOfStock.js';\nimport PaymentMethods from '@dropins/storefront-checkout/containers/PaymentMethods.js';\nimport PlaceOrder from '@dropins/storefront-checkout/containers/PlaceOrder.js';\nimport ServerError from '@dropins/storefront-checkout/containers/ServerError.js';\nimport ShippingMethods from '@dropins/storefront-checkout/containers/ShippingMethods.js';\nimport TermsAndConditions from '@dropins/storefront-checkout/containers/TermsAndConditions.js';\nimport { render as CheckoutProvider } from '@dropins/storefront-checkout/render.js';\n\n// Auth Dropin\nimport * as authApi from '@dropins/storefront-auth/api.js';\nimport AuthCombine from '@dropins/storefront-auth/containers/AuthCombine.js';\nimport { render as AuthProvider } from '@dropins/storefront-auth/render.js';\n\n// Account Dropin\nimport Addresses from '@dropins/storefront-account/containers/Addresses.js';\nimport AddressForm from '@dropins/storefront-account/containers/AddressForm.js';\nimport { render as AccountProvider } from '@dropins/storefront-account/render.js';\n\n// Cart Dropin\nimport * as cartApi from '@dropins/storefront-cart/api.js';\nimport CartSummaryList from '@dropins/storefront-cart/containers/CartSummaryList.js';\nimport Coupons from '@dropins/storefront-cart/containers/Coupons.js';\nimport GiftCards from '@dropins/storefront-cart/containers/GiftCards.js';\nimport GiftOptions from '@dropins/storefront-cart/containers/GiftOptions.js';\nimport OrderSummary from '@dropins/storefront-cart/containers/OrderSummary.js';\nimport { render as CartProvider } from '@dropins/storefront-cart/render.js';\n\n// Payment Services Dropin\nimport { PaymentMethodCode } from '@dropins/storefront-payment-services/api.js';\nimport CreditCard from '@dropins/storefront-payment-services/containers/CreditCard.js';\nimport { render as PaymentServices } from '@dropins/storefront-payment-services/render.js';\n\n// Tools\nimport {\n Header,\n provider as UI,\n} from '@dropins/tools/components.js';\nimport { events } from '@dropins/tools/event-bus.js';\nimport { debounce } from '@dropins/tools/lib.js';\nimport { tryRenderAemAssetsImage } from '@dropins/tools/lib/aem/assets.js';\n\n// Checkout Dropin Libs\nimport {\n estimateShippingCost,\n setAddressOnCart,\n getCartAddress,\n transformCartAddressToFormValues,\n} from '@dropins/storefront-checkout/lib/utils.js';\n\nimport { showModal, swatchImageSlot } from './utils.js';\n\n// External dependencies\nimport {\n authPrivacyPolicyConsentSlot,\n fetchPlaceholders,\n rootLink,\n} from '../../scripts/commerce.js';\n\n// Constants\nimport {\n ADDRESS_INPUT_DEBOUNCE_TIME,\n BILLING_ADDRESS_DATA_KEY,\n BILLING_FORM_NAME,\n CHECKOUT_ERROR_CLASS,\n CHECKOUT_HEADER_CLASS,\n DEBOUNCE_TIME,\n LOGIN_FORM_NAME,\n SHIPPING_ADDRESS_DATA_KEY,\n SHIPPING_FORM_NAME,\n} from './constants.js';\n\n/**\n * Container IDs for registry management\n * @enum {string}\n */\nexport const CONTAINERS = Object.freeze({\n // Static containers (rendered in Promise.all)\n MERGED_CART_BANNER: 'mergedCartBanner',\n CHECKOUT_HEADER: 'checkoutHeader',\n SERVER_ERROR: 'serverError',\n OUT_OF_STOCK: 'outOfStock',\n LOGIN_FORM: 'loginForm',\n SHIPPING_ADDRESS_FORM_SKELETON: 'shippingAddressFormSkeleton',\n BILL_TO_SHIPPING_ADDRESS: 'billToShippingAddress',\n SHIPPING_METHODS: 'shippingMethods',\n PAYMENT_METHODS: 'paymentMethods',\n BILLING_ADDRESS_FORM_SKELETON: 'billingAddressFormSkeleton',\n ORDER_SUMMARY: 'orderSummary',\n CART_SUMMARY_LIST: 'cartSummaryList',\n TERMS_AND_CONDITIONS: 'termsAndConditions',\n PLACE_ORDER_BUTTON: 'placeOrderButton',\n GIFT_OPTIONS: 'giftOptions',\n CUSTOMER_SHIPPING_ADDRESSES: 'customerShippingAddresses',\n CUSTOMER_BILLING_ADDRESSES: 'customerBillingAddresses',\n\n // Dynamic containers (conditional rendering)\n SHIPPING_ADDRESS_FORM: 'shippingAddressForm',\n BILLING_ADDRESS_FORM: 'billingAddressForm',\n\n // Slot/Sub-containers (nested within other containers)\n ESTIMATE_SHIPPING: 'estimateShipping',\n CART_COUPONS: 'cartCoupons',\n GIFT_CARDS: 'giftCards',\n CART_GIFT_OPTIONS: 'cartGiftOptions',\n});\n\n/**\n * A Map to store the API of rendered containers.\n * The key is a unique string ID, and the value is the containers's API object.\n * (e.g., { setProps: (props) => {...}, remove: () => {...} })\n */\nconst registry = new Map();\n\n/**\n * Checks if a container with the given ID has been rendered.\n * This is used to prevent multiple instances of the same container from being rendered.\n * @param {string} id - The unique ID of the container to check.\n * @returns {boolean} - Returns true if the container has been rendered, false otherwise.\n */\nexport const hasContainer = (id) => registry.has(id);\n\n/**\n * Helper to get a container from the registry or render and register it if not present.\n * @async\n * @param {string} id - Unique identifier for the container.\n * @param {Function} renderFn - Async function that renders the container.\n * @returns {Promise<Object>} - The rendered container API.\n */\nconst renderContainer = async (id, renderFn) => {\n if (registry.has(id)) {\n return registry.get(id);\n }\n\n try {\n const container = await renderFn();\n registry.set(id, container);\n return container;\n } catch (error) {\n console.error(`Error rendering container ${id}:`, error);\n throw error;\n }\n};\n\n/**\n * Unmounts and removes a container from the registry.\n * This function checks if the container is registered, removes it from the DOM,\n * and deletes its reference from the registry.\n * @param {string} id - The unique ID of the container to unmount.\n * @return {void}\n */\nexport const unmountContainer = (id) => {\n if (!registry.has(id)) {\n return;\n }\n\n const containerApi = registry.get(id);\n containerApi.remove();\n registry.delete(id);\n};\n\n/**\n * Renders the merged cart banner notification for authenticated users\n * @param {HTMLElement} container - DOM element to render the banner in\n * @returns {Promise<Object>} - The rendered merged cart banner component\n */\nexport const renderMergedCartBanner = async (container) => renderContainer(\n CONTAINERS.MERGED_CART_BANNER,\n async () => CheckoutProvider.render(MergedCartBanner)(container),\n);\n\n/**\n * Renders the checkout page header with title and styling\n * @param {HTMLElement} container - DOM element to render the header in\n * @param {string} title - The title to display in the header\n * @returns {Promise<Object>} - The rendered checkout header component\n */\nexport const renderCheckoutHeader = async (container, title) => renderContainer(\n CONTAINERS.CHECKOUT_HEADER,\n async () => UI.render(Header, {\n className: CHECKOUT_HEADER_CLASS,\n divider: true,\n level: 1,\n size: 'large',\n title,\n })(container),\n);\n\n/**\n * Renders server error handling with retry functionality and error state management\n * @param {HTMLElement} container - DOM element to render the error component in\n * @param {HTMLElement} contentElement - Main content element to add error styling to\n * @returns {Promise<Object>} - The rendered server error component\n */\nexport const renderServerError = async (container, contentElement) => renderContainer(\n CONTAINERS.SERVER_ERROR,\n async () => CheckoutProvider.render(ServerError, {\n autoScroll: true,\n onRetry: (error) => {\n if (error.code === 'PERMISSION_DENIED') {\n document.location.reload();\n return;\n }\n\n contentElement.classList.remove(CHECKOUT_ERROR_CLASS);\n },\n onServerError: () => {\n contentElement.classList.add(CHECKOUT_ERROR_CLASS);\n },\n })(container),\n);\n\n/**\n * Renders out of stock handling with cart navigation and product update options\n * @param {HTMLElement} container - DOM element to render the component in\n * @returns {Promise<Object>} - The rendered out-of-stock component\n */\nexport const renderOutOfStock = async (container) => renderContainer(\n CONTAINERS.OUT_OF_STOCK,\n async () => CheckoutProvider.render(OutOfStock, {\n routeCart: () => rootLink('/cart'),\n onCartProductsUpdate: (items) => {\n cartApi.updateProductsFromCart(items).catch(console.error);\n },\n })(container),\n);\n\n/**\n * Renders the login form for guest checkout with authentication options\n * Uses the existing 'authenticated' event system for decoupled communication\n * @param {HTMLElement} container - DOM element to render the login form in\n * @returns {Promise<Object>} - The rendered login form component\n */\nexport const renderLoginForm = async (container) => renderContainer(\n CONTAINERS.LOGIN_FORM,\n async () => CheckoutProvider.render(LoginForm, {\n name: LOGIN_FORM_NAME,\n onSignInClick: async (initialEmailValue) => {\n const signInForm = document.createElement('div');\n\n AuthProvider.render(AuthCombine, {\n signInFormConfig: {\n renderSignUpLink: true,\n initialEmailValue,\n // No onSuccessCallback needed - the 'authenticated' event will be fired automatically\n },\n signUpFormConfig: {\n slots: {\n ...authPrivacyPolicyConsentSlot,\n },\n },\n resetPasswordFormConfig: {},\n })(signInForm);\n\n await showModal(signInForm);\n },\n onSignOutClick: () => {\n authApi.revokeCustomerToken();\n },\n })(container),\n);\n\n/**\n * Renders the shipping address form skeleton (initial placeholder)\n * @param {HTMLElement} container - DOM element to render the form in\n * @returns {Promise<Object>} - The rendered shipping address form skeleton\n */\nexport const renderShippingAddressFormSkeleton = async (container) => renderContainer(\n CONTAINERS.SHIPPING_ADDRESS_FORM_SKELETON,\n async () => AccountProvider.render(AddressForm, {\n fieldIdPrefix: 'shipping',\n isOpen: true,\n showFormLoader: true,\n })(container),\n);\n\n/**\n * Renders the billing address form skeleton (initial placeholder)\n * @param {HTMLElement} container - DOM element to render the form in\n * @returns {Promise<Object>} - The rendered billing address form skeleton\n */\nexport const renderBillingAddressFormSkeleton = async (container) => renderContainer(\n CONTAINERS.BILLING_ADDRESS_FORM_SKELETON,\n async () => AccountProvider.render(AddressForm, {\n fieldIdPrefix: 'billing',\n isOpen: true,\n showFormLoader: true,\n })(container),\n);\n\n/**\n * Renders checkbox to set billing address same as shipping address - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render the checkbox in\n * @returns {Promise<Object>} - The rendered bill to shipping address component\n */\nexport const renderBillToShippingAddress = async (container) => renderContainer(\n CONTAINERS.BILL_TO_SHIPPING_ADDRESS,\n async () => {\n const setBillingAddressOnCart = setAddressOnCart({ type: 'billing' });\n\n return CheckoutProvider.render(BillToShippingAddress, {\n onChange: (checked) => {\n const billingFormValues = events.lastPayload('checkout/addresses/billing');\n\n if (!checked && billingFormValues) {\n setBillingAddressOnCart(billingFormValues);\n }\n },\n })(container);\n },\n);\n\n/**\n * Renders available shipping methods with selection interface\n * @param {HTMLElement} container - DOM element to render shipping methods in\n * @returns {Promise<Object>} - The rendered shipping methods component\n */\nexport const renderShippingMethods = async (container) => renderContainer(\n CONTAINERS.SHIPPING_METHODS,\n async () => CheckoutProvider.render(ShippingMethods)(container),\n);\n\n/**\n * Renders payment methods with credit card integration - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render payment methods in\n * @param {Object} creditCardFormRef - React-style ref for credit card form\n * @returns {Promise<Object>} - The rendered payment methods component\n */\nexport const renderPaymentMethods = async (container, creditCardFormRef) => renderContainer(\n CONTAINERS.PAYMENT_METHODS,\n async () => CheckoutProvider.render(PaymentMethods, {\n slots: {\n Methods: {\n [PaymentMethodCode.CREDIT_CARD]: {\n render: (ctx) => {\n const $creditCard = document.createElement('div');\n\n PaymentServices.render(CreditCard, {\n getCartId: () => ctx.cartId,\n creditCardFormRef,\n })($creditCard);\n\n ctx.replaceHTML($creditCard);\n },\n },\n [PaymentMethodCode.SMART_BUTTONS]: {\n enabled: false,\n },\n [PaymentMethodCode.APPLE_PAY]: {\n enabled: false,\n },\n [PaymentMethodCode.GOOGLE_PAY]: {\n enabled: false,\n },\n [PaymentMethodCode.VAULT]: {\n enabled: false,\n },\n [PaymentMethodCode.FASTLANE]: {\n enabled: false,\n },\n },\n },\n })(container),\n);\n\n/**\n * Renders terms and conditions with agreement slots and manual consent mode\n * @param {HTMLElement} container - DOM element to render the terms in\n * @returns {Promise<Object>} - The rendered terms and conditions component\n */\nexport const renderTermsAndConditions = async (container) => renderContainer(\n CONTAINERS.TERMS_AND_CONDITIONS,\n async () => CheckoutProvider.render(TermsAndConditions, {\n slots: {\n Agreements: (ctx) => {\n ctx.appendAgreement(() => ({\n name: 'default',\n mode: 'manual',\n translationId: 'Checkout.TermsAndConditions.label',\n }));\n },\n },\n })(container),\n);\n\n/**\n * Renders estimate shipping form for order summary slot\n * @param {HTMLElement} ctx - The slot context element\n * @returns {void}\n */\nexport const renderEstimateShipping = (ctx) => {\n const estimateShippingForm = document.createElement('div');\n CheckoutProvider.render(EstimateShipping)(estimateShippingForm);\n ctx.appendChild(estimateShippingForm);\n};\n\n/**\n * Renders cart coupons for order summary slot\n * @param {HTMLElement} ctx - The slot context element\n * @returns {void}\n */\nexport const renderCartCoupons = (ctx) => {\n const coupons = document.createElement('div');\n CartProvider.render(Coupons)(coupons);\n ctx.appendChild(coupons);\n};\n\n/**\n * Renders gift cards for order summary slot\n * @param {HTMLElement} ctx - The slot context element\n * @returns {void}\n */\nexport const renderGiftCards = (ctx) => {\n const giftCards = document.createElement('div');\n CartProvider.render(GiftCards)(giftCards);\n ctx.appendChild(giftCards);\n};\n\n/**\n * Renders gift options for cart summary list footer slot\n * @param {HTMLElement} ctx - The slot context element\n * @returns {void}\n */\nexport const renderCartGiftOptions = (ctx) => {\n const giftOptions = document.createElement('div');\n\n CartProvider.render(GiftOptions, {\n item: ctx.item,\n view: 'product',\n dataSource: 'cart',\n isEditable: false,\n handleItemsLoading: ctx.handleItemsLoading,\n handleItemsError: ctx.handleItemsError,\n onItemUpdate: ctx.onItemUpdate,\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(giftOptions);\n\n ctx.appendChild(giftOptions);\n};\n\n// ============================================================================\n// SUMMARY CONTAINERS\n// ============================================================================\n\n/**\n * Renders order summary with estimate shipping, coupons, and gift cards slots\n * @param {HTMLElement} container - DOM element to render order summary in\n * @returns {Promise<Object>} - The rendered order summary component\n */\nexport const renderOrderSummary = async (container) => renderContainer(\n CONTAINERS.ORDER_SUMMARY,\n async () => CartProvider.render(OrderSummary, {\n slots: {\n EstimateShipping: renderEstimateShipping,\n Coupons: renderCartCoupons,\n GiftCards: renderGiftCards,\n },\n })(container),\n);\n\n/**\n * Renders cart summary list with custom heading, thumbnail and gift options slots\n * @param {HTMLElement} container - DOM element to render cart summary list in\n * @returns {Promise<Object>} - The rendered cart summary list component\n */\nexport const renderCartSummaryList = async (container) => renderContainer(\n CONTAINERS.CART_SUMMARY_LIST,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n return CartProvider.render(CartSummaryList, {\n variant: 'secondary',\n slots: {\n Heading: (headingCtx) => {\n const title = placeholders?.Checkout?.Summary?.heading;\n\n const cartSummaryListHeading = document.createElement('div');\n cartSummaryListHeading.classList.add('cart-summary-list__heading');\n\n const cartSummaryListHeadingText = document.createElement('div');\n cartSummaryListHeadingText.classList.add(\n 'cart-summary-list__heading-text',\n );\n\n cartSummaryListHeadingText.innerText = title?.replace(\n '({count})',\n headingCtx.count ? `(${headingCtx.count})` : '',\n );\n const editCartLink = document.createElement('a');\n editCartLink.classList.add('cart-summary-list__edit');\n editCartLink.href = rootLink('/cart');\n editCartLink.rel = 'noreferrer';\n editCartLink.innerText = placeholders?.Checkout?.Summary?.Edit;\n\n cartSummaryListHeading.appendChild(cartSummaryListHeadingText);\n cartSummaryListHeading.appendChild(editCartLink);\n headingCtx.appendChild(cartSummaryListHeading);\n\n headingCtx.onChange((nextHeadingCtx) => {\n cartSummaryListHeadingText.innerText = title?.replace(\n '({count})',\n nextHeadingCtx.count ? `(${nextHeadingCtx.count})` : '',\n );\n });\n },\n Thumbnail: (ctx) => {\n const { item, defaultImageProps } = ctx;\n tryRenderAemAssetsImage(ctx, {\n alias: item.sku,\n imageProps: defaultImageProps,\n\n params: {\n width: defaultImageProps.width,\n height: defaultImageProps.height,\n },\n });\n },\n Footer: renderCartGiftOptions,\n },\n })(container);\n },\n);\n\n/**\n * Renders place order button with handler functions - follows multi-step pattern\n * @param {HTMLElement} container - DOM element to render the place order button in\n * @param {Object} options - Configuration object with handler functions\n * @param {Function} options.handleValidation - Validation handler function\n * @param {Function} options.handlePlaceOrder - Place order handler function\n * @returns {Promise<Object>} - The rendered place order component\n */\nexport const renderPlaceOrder = async (container, options = {}) => renderContainer(\n CONTAINERS.PLACE_ORDER_BUTTON,\n async () => CheckoutProvider.render(PlaceOrder, {\n handleValidation: options.handleValidation,\n handlePlaceOrder: options.handlePlaceOrder,\n })(container),\n);\n\n/**\n * Renders customer shipping addresses selector/form for authenticated users - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render shipping addresses in\n * @param {Object} formRef - React-style ref for form reference\n * @param {Object} data - Cart data containing shipping address information\n * @returns {Promise<Object>} - The rendered customer shipping addresses component\n */\nexport const renderCustomerShippingAddresses = async (container, formRef, data) => renderContainer(\n CONTAINERS.CUSTOMER_SHIPPING_ADDRESSES,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n const cartShippingAddress = getCartAddress(data, 'shipping');\n\n const shippingAddressId = cartShippingAddress\n ? cartShippingAddress?.id ?? 0\n : undefined;\n\n const shippingAddressCache = sessionStorage.getItem(SHIPPING_ADDRESS_DATA_KEY);\n\n // Clear persisted shipping address if cart has a shipping address\n if (cartShippingAddress && shippingAddressCache) {\n sessionStorage.removeItem(SHIPPING_ADDRESS_DATA_KEY);\n }\n\n const storeConfig = checkoutApi.getStoreConfigCache();\n\n const inputsDefaultValueSet = cartShippingAddress && cartShippingAddress.id === undefined\n ? transformCartAddressToFormValues(cartShippingAddress)\n : { countryCode: storeConfig.defaultCountry };\n\n const hasCartShippingAddress = Boolean(data.shippingAddresses?.[0]);\n let isFirstRenderShipping = true;\n\n // Create address setters with constants moved inside\n const setShippingAddressOnCart = setAddressOnCart({\n type: 'shipping',\n debounceMs: DEBOUNCE_TIME,\n });\n\n const estimateShippingCostOnCart = estimateShippingCost({\n debounceMs: DEBOUNCE_TIME,\n });\n\n const notifyShippingValues = debounce((values) => {\n events.emit('checkout/addresses/shipping', values);\n }, ADDRESS_INPUT_DEBOUNCE_TIME);\n\n return AccountProvider.render(Addresses, {\n addressFormTitle: placeholders?.Checkout?.Addresses?.shippingAddressTitle,\n defaultSelectAddressId: shippingAddressId,\n fieldIdPrefix: 'shipping',\n formName: SHIPPING_FORM_NAME,\n forwardFormRef: formRef,\n inputsDefaultValueSet,\n minifiedView: false,\n onAddressData: (values) => {\n const canSetShippingAddressOnCart = !isFirstRenderShipping || !hasCartShippingAddress;\n if (canSetShippingAddressOnCart) setShippingAddressOnCart(values);\n if (!hasCartShippingAddress) estimateShippingCostOnCart(values);\n if (isFirstRenderShipping) isFirstRenderShipping = false;\n notifyShippingValues(values);\n },\n selectable: true,\n selectShipping: true,\n showBillingCheckBox: false,\n showSaveCheckBox: true,\n showShippingCheckBox: false,\n title: placeholders?.Checkout?.Addresses?.shippingAddressTitle,\n })(container);\n },\n);\n\n/**\n * Renders customer billing addresses selector/form for authenticated users - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render billing addresses in\n * @param {Object} formRef - React-style ref for form reference\n * @param {Object} data - Cart data containing billing address information\n * @returns {Promise<Object>} - The rendered customer billing addresses component\n */\nexport const renderCustomerBillingAddresses = async (container, formRef, data) => renderContainer(\n CONTAINERS.CUSTOMER_BILLING_ADDRESSES,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n const cartBillingAddress = getCartAddress(data, 'billing');\n\n const billingAddressId = cartBillingAddress\n ? cartBillingAddress?.id ?? 0\n : undefined;\n\n const billingAddressCache = sessionStorage.getItem(BILLING_ADDRESS_DATA_KEY);\n\n // Clear persisted billing address if cart has a billing address\n if (cartBillingAddress && billingAddressCache) {\n sessionStorage.removeItem(BILLING_ADDRESS_DATA_KEY);\n }\n\n const storeConfig = checkoutApi.getStoreConfigCache();\n\n const inputsDefaultValueSet = cartBillingAddress && cartBillingAddress.id === undefined\n ? transformCartAddressToFormValues(cartBillingAddress)\n : { countryCode: storeConfig.defaultCountry };\n\n const hasCartBillingAddress = Boolean(data.billingAddress);\n let isFirstRenderBilling = true;\n\n // Create address setter with constants moved inside\n const setBillingAddressOnCart = setAddressOnCart({\n type: 'billing',\n debounceMs: DEBOUNCE_TIME,\n });\n\n const notifyBillingValues = debounce((values) => {\n events.emit('checkout/addresses/billing', values);\n }, ADDRESS_INPUT_DEBOUNCE_TIME);\n\n return AccountProvider.render(Addresses, {\n addressFormTitle: placeholders?.Checkout?.Addresses?.billToNewAddress,\n defaultSelectAddressId: billingAddressId,\n formName: BILLING_FORM_NAME,\n forwardFormRef: formRef,\n inputsDefaultValueSet,\n minifiedView: false,\n onAddressData: (values) => {\n const canSetBillingAddressOnCart = !isFirstRenderBilling || !hasCartBillingAddress;\n if (canSetBillingAddressOnCart) setBillingAddressOnCart(values);\n if (isFirstRenderBilling) isFirstRenderBilling = false;\n notifyBillingValues(values);\n },\n selectable: true,\n selectBilling: true,\n showBillingCheckBox: false,\n showSaveCheckBox: true,\n showShippingCheckBox: false,\n title: placeholders?.Checkout?.Addresses?.billingAddressTitle,\n })(container);\n },\n);\n\n/**\n * Renders address form for guest users (shipping or billing) - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render address form in\n * @param {Object} formRef - React-style ref for form reference\n * @param {Object} data - Cart data containing address information\n * @param {string} addressType - Type of address form ('shipping' or 'billing')\n * @returns {Promise<Object>} - The rendered address form component\n */\nexport const renderAddressForm = async (container, formRef, data, addressType) => {\n const isShipping = addressType === 'shipping';\n const containerKey = isShipping ? CONTAINERS.SHIPPING_ADDRESS_FORM : CONTAINERS.BILLING_ADDRESS_FORM;\n\n return renderContainer(\n containerKey,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n // Get address type specific configurations\n const cartAddress = getCartAddress(data, addressType);\n const addressDataKey = isShipping ? SHIPPING_ADDRESS_DATA_KEY : BILLING_ADDRESS_DATA_KEY;\n const addressCache = sessionStorage.getItem(addressDataKey);\n\n // Clear persisted address if cart has an address\n if (cartAddress && addressCache) {\n sessionStorage.removeItem(addressDataKey);\n }\n\n let isFirstRender = true;\n const hasCartAddress = Boolean(isShipping ? data.shippingAddresses?.[0] : data.billingAddress);\n\n // Create address setter with appropriate API\n const setAddressOnCartFn = setAddressOnCart({\n type: addressType,\n debounceMs: DEBOUNCE_TIME,\n });\n\n // Create shipping cost estimator (only for shipping addresses)\n const estimateShippingCostOnCart = isShipping ? estimateShippingCost({\n debounceMs: DEBOUNCE_TIME,\n }) : null;\n\n const notifyValues = debounce((values) => {\n const eventType = isShipping ? 'checkout/addresses/shipping' : 'checkout/addresses/billing';\n events.emit(eventType, values);\n }, ADDRESS_INPUT_DEBOUNCE_TIME);\n\n const storeConfig = checkoutApi.getStoreConfigCache();\n\n // Address type specific configurations\n const formName = isShipping ? SHIPPING_FORM_NAME : BILLING_FORM_NAME;\n const addressTitle = isShipping\n ? placeholders?.Checkout?.Addresses?.shippingAddressTitle\n : placeholders?.Checkout?.Addresses?.billingAddressTitle;\n const className = isShipping\n ? 'checkout-shipping-form__address-form'\n : 'checkout-billing-form__address-form';\n\n const inputsDefaultValueSet = cartAddress\n ? transformCartAddressToFormValues(cartAddress)\n : { countryCode: storeConfig.defaultCountry };\n\n return AccountProvider.render(AddressForm, {\n addressesFormTitle: addressTitle,\n className,\n fieldIdPrefix: addressType,\n formName,\n forwardFormRef: formRef,\n hideActionFormButtons: true,\n inputsDefaultValueSet,\n isOpen: true,\n onChange: (values) => {\n const canSetAddressOnCart = !isFirstRender || !hasCartAddress;\n if (canSetAddressOnCart) setAddressOnCartFn(values);\n\n // Only estimate shipping cost for shipping addresses when no cart address exists\n if (isShipping && !hasCartAddress && estimateShippingCostOnCart) {\n estimateShippingCostOnCart(values);\n }\n\n if (isFirstRender) isFirstRender = false;\n\n notifyValues(values);\n },\n showBillingCheckBox: false,\n showFormLoader: false,\n showShippingCheckBox: false,\n })(container);\n },\n );\n};\n\n/**\n * Renders order-level gift options with swatch image integration\n * @param {HTMLElement} container - DOM element to render gift options in\n * @returns {Promise<Object>} - The rendered gift options component\n */\nexport const renderGiftOptions = async (container) => renderContainer(\n CONTAINERS.GIFT_OPTIONS,\n async () => CartProvider.render(GiftOptions, {\n view: 'order',\n dataSource: 'cart',\n isEditable: false,\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(container),\n);\n",
|
|
297
|
+
"fragments.js": "// eslint-disable-next-line import/no-unresolved\nimport { createFragment } from '@dropins/storefront-checkout/lib/utils.js';\n\nimport { CHECKOUT_BLOCK } from './constants.js';\n\n/**\n * A frozen, nested object of CSS selectors\n * @readonly\n */\nexport const selectors = Object.freeze({\n checkout: {\n content: '.checkout__content',\n loader: '.checkout__loader',\n mergedCartBanner: '.checkout__merged-cart-banner',\n heading: '.checkout__heading',\n serverError: '.checkout__server-error',\n outOfStock: '.checkout__out-of-stock',\n login: '.checkout__login',\n shippingForm: '.checkout__shipping-form',\n billToShipping: '.checkout__bill-to-shipping',\n delivery: '.checkout__delivery',\n paymentMethods: '.checkout__payment-methods',\n billingForm: '.checkout__billing-form',\n orderSummary: '.checkout__order-summary',\n cartSummary: '.checkout__cart-summary',\n placeOrder: '.checkout__place-order',\n giftOptions: '.checkout__gift-options',\n termsAndConditions: '.checkout__terms-and-conditions',\n main: '.checkout__main',\n aside: '.checkout__aside',\n },\n});\n\n// =============================================================================\n// CHECKOUT\n// =============================================================================\n\n/**\n * Creates the main checkout fragment with all checkout blocks.\n * @returns {DocumentFragment} The complete checkout fragment.\n */\nexport function createCheckoutFragment() {\n return createFragment(`\n <div class=\"checkout__wrapper\">\n <div class=\"checkout__loader\"></div>\n <div class=\"checkout__content\">\n <div class=\"checkout__merged-cart-banner\"></div>\n <div class=\"checkout__main\">\n <div class=\"checkout__heading ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__server-error ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__out-of-stock ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__login ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__shipping-form ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__bill-to-shipping ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__delivery ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__payment-methods ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__billing-form ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__terms-and-conditions ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__place-order ${CHECKOUT_BLOCK}\"></div>\n </div>\n <div class=\"checkout__aside\">\n <div class=\"checkout__order-summary ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__gift-options ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__cart-summary ${CHECKOUT_BLOCK}\"></div>\n </div>\n </div>\n </div>\n `);\n}\n",
|
|
298
|
+
"utils.js": "/* eslint-disable import/no-unresolved */\nimport { ProgressSpinner, provider as UI } from '@dropins/tools/components.js';\nimport { tryRenderAemAssetsImage } from '@dropins/tools/lib/aem/assets.js';\nimport { ORDER_DETAILS_PATH, rootLink } from '../../scripts/commerce.js';\nimport { getUserTokenCookie } from '../../scripts/initializers/index.js';\nimport createModal from '../modal/modal.js';\n\n/**\n * Displays an overlay spinner in the specified container\n * @param {Object} loaderRef - Ref object to store the spinner component\n * @param {HTMLElement} $loader - DOM element to render the spinner in\n */\nexport const displayOverlaySpinner = async (loaderRef, $loader) => {\n if (loaderRef.current) return;\n\n loaderRef.current = await UI.render(ProgressSpinner, {\n className: '.checkout__overlay-spinner',\n })($loader);\n};\n\n/**\n * Removes the overlay spinner and cleans up references\n * @param {Object} loaderRef - Ref object containing the spinner component\n * @param {HTMLElement} $loader - DOM element containing the spinner\n */\nexport const removeOverlaySpinner = (loaderRef, $loader) => {\n if (!loaderRef.current) return;\n\n loaderRef.current.remove();\n loaderRef.current = null;\n $loader.innerHTML = '';\n};\n\n// Modal state management\nlet modal;\n\n/**\n * Shows a modal with the specified content\n * @param {HTMLElement} content - DOM element to display in the modal\n */\nexport const showModal = async (content) => {\n modal = await createModal([content]);\n modal.showModal();\n};\n\n/**\n * Removes the currently displayed modal and cleans up references\n */\nexport const removeModal = () => {\n if (!modal) return;\n modal.removeModal();\n modal = null;\n};\n\n/**\n * Renders AEM asset images for gift option swatches\n * @param {Object} ctx - The context object containing imageSwatchContext and defaultImageProps\n */\nexport function swatchImageSlot(ctx) {\n const { imageSwatchContext, defaultImageProps } = ctx;\n tryRenderAemAssetsImage(ctx, {\n alias: imageSwatchContext.label,\n imageProps: defaultImageProps,\n wrapper: document.createElement('span'),\n params: {\n width: defaultImageProps.width,\n height: defaultImageProps.height,\n },\n });\n}\n\n/**\n * Builds the order details URL based on authentication status\n * @param {Object} orderData - Order data containing number and token\n * @param {string} orderDetailsPath - Path to the order details page\n * @returns {string} The constructed order details URL\n */\nexport function buildOrderDetailsUrl(orderData, orderDetailsPath = ORDER_DETAILS_PATH) {\n const token = getUserTokenCookie();\n const orderRef = token ? orderData.number : orderData.token;\n const orderNumber = orderData.number;\n const encodedOrderRef = encodeURIComponent(orderRef);\n const encodedOrderNumber = encodeURIComponent(orderNumber);\n\n return token\n ? rootLink(`${orderDetailsPath}?orderRef=${encodedOrderRef}`)\n : rootLink(`${orderDetailsPath}?orderRef=${encodedOrderRef}&orderNumber=${encodedOrderNumber}`);\n}\n"
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"name": "commerce-checkout-adyen",
|
|
303
|
+
"description": "Example commerce-checkout-adyen block for storefront-checkout",
|
|
304
|
+
"files": {
|
|
305
|
+
"commerce-checkout-adyen.js": "/* eslint-disable import/no-unresolved */\n/* eslint-disable no-unused-vars */\n\n// Dropin Tools\nimport { getConfigValue } from '@dropins/tools/lib/aem/configs.js';\nimport { events } from '@dropins/tools/event-bus.js';\nimport { initReCaptcha } from '@dropins/tools/recaptcha.js';\n\n// Checkout Dropin Libraries\nimport {\n createScopedSelector,\n isVirtualCart,\n setMetaTags,\n validateForms,\n} from '@dropins/storefront-checkout/lib/utils.js';\n\nimport * as orderApi from '@dropins/storefront-order/api.js';\n\n// Dropin components for Adyen-specific payment methods\nimport PaymentMethods from '@dropins/storefront-checkout/containers/PaymentMethods.js';\nimport { render as CheckoutProvider } from '@dropins/storefront-checkout/render.js';\n\n// Payment Services Dropin\nimport { PaymentMethodCode } from '@dropins/storefront-payment-services/api.js';\nimport CreditCard from '@dropins/storefront-payment-services/containers/CreditCard.js';\nimport { render as PaymentServices } from '@dropins/storefront-payment-services/render.js';\nimport { getUserTokenCookie } from '../../scripts/initializers/index.js';\nimport { loadCSS, loadScript } from '../../scripts/aem.js';\n\n// Block-level imports from local utils (functions not available in dropin lib)\nimport {\n buildOrderDetailsUrl,\n displayOverlaySpinner,\n removeOverlaySpinner,\n} from './utils.js';\n\n// Fragment functions\nimport {\n createCheckoutFragment,\n selectors,\n} from './fragments.js';\n\n// Container functions\nimport {\n renderAddressForm,\n renderBillingAddressFormSkeleton,\n renderBillToShippingAddress,\n renderCartSummaryList,\n renderCheckoutHeader,\n renderCustomerBillingAddresses,\n renderCustomerShippingAddresses,\n renderGiftOptions,\n renderLoginForm,\n renderMergedCartBanner,\n renderOrderSummary,\n renderOutOfStock,\n renderPlaceOrder,\n renderServerError,\n renderShippingAddressFormSkeleton,\n renderShippingMethods,\n renderTermsAndConditions,\n} from './containers.js';\n\n// Constants\nimport {\n BILLING_ADDRESS_DATA_KEY,\n BILLING_FORM_NAME,\n LOGIN_FORM_NAME,\n PURCHASE_ORDER_FORM_NAME,\n SHIPPING_ADDRESS_DATA_KEY,\n SHIPPING_FORM_NAME,\n TERMS_AND_CONDITIONS_FORM_NAME,\n} from './constants.js';\n\nimport { rootLink } from '../../scripts/commerce.js';\n\n// Initializers\nimport '../../scripts/initializers/account.js';\nimport '../../scripts/initializers/checkout.js';\nimport '../../scripts/initializers/order.js';\n\n// Checkout success block import and CSS preload\nimport {\n preloadCheckoutSuccess,\n renderCheckoutSuccess,\n} from '../commerce-checkout-success/commerce-checkout-success.js';\n\npreloadCheckoutSuccess();\n\nfunction redirectToCartIfEmpty(cartData) {\n const isOrderPlaced = events.lastPayload('order/placed') !== undefined;\n\n if (!isOrderPlaced && (cartData === null || cartData?.items?.length === 0)) {\n window.location.href = rootLink('/cart');\n }\n}\n\nexport default async function decorate(block) {\n setMetaTags('Checkout');\n document.title = 'Checkout';\n\n const cartData = events.lastPayload('cart/initialized');\n redirectToCartIfEmpty(cartData);\n\n // Adobe Commerce GraphQL endpoint\n const commerceCoreEndpoint = getConfigValue('commerce-core-endpoint') || getConfigValue('commerce-endpoint');\n\n // Adyen-specific variable\n let adyenCard;\n\n // Container and component references\n let shippingForm;\n let billingForm;\n let shippingAddresses;\n let billingAddresses;\n\n const shippingFormRef = { current: null };\n const billingFormRef = { current: null };\n const creditCardFormRef = { current: null };\n const loaderRef = { current: null };\n\n events.on('order/placed', () => {\n setMetaTags('Order Confirmation');\n document.title = 'Order Confirmation';\n });\n\n // Create the checkout layout using shared fragments\n const checkoutFragment = createCheckoutFragment();\n\n // Create scoped selector for the checkout fragment\n const getElement = createScopedSelector(checkoutFragment);\n\n // Get all checkout elements using centralized selectors\n const $content = getElement(selectors.checkout.content);\n const $loader = getElement(selectors.checkout.loader);\n const $mergedCartBanner = getElement(selectors.checkout.mergedCartBanner);\n const $heading = getElement(selectors.checkout.heading);\n const $serverError = getElement(selectors.checkout.serverError);\n const $outOfStock = getElement(selectors.checkout.outOfStock);\n const $login = getElement(selectors.checkout.login);\n const $shippingForm = getElement(selectors.checkout.shippingForm);\n const $billToShipping = getElement(selectors.checkout.billToShipping);\n const $delivery = getElement(selectors.checkout.delivery);\n const $paymentMethods = getElement(selectors.checkout.paymentMethods);\n const $billingForm = getElement(selectors.checkout.billingForm);\n const $orderSummary = getElement(selectors.checkout.orderSummary);\n const $cartSummary = getElement(selectors.checkout.cartSummary);\n const $placeOrder = getElement(selectors.checkout.placeOrder);\n const $giftOptions = getElement(selectors.checkout.giftOptions);\n const $termsAndConditions = getElement(selectors.checkout.termsAndConditions);\n\n block.appendChild(checkoutFragment);\n\n // Create validation and place order handlers\n const handleValidation = () => validateForms([\n { name: LOGIN_FORM_NAME },\n { name: SHIPPING_FORM_NAME, ref: shippingFormRef },\n { name: BILLING_FORM_NAME, ref: billingFormRef },\n { name: PURCHASE_ORDER_FORM_NAME },\n { name: TERMS_AND_CONDITIONS_FORM_NAME },\n ]);\n\n // eslint-disable-next-line consistent-return\n const handlePlaceOrder = async ({ cartId, code }) => {\n await displayOverlaySpinner(loaderRef, $loader);\n try {\n const isAdyen = code === 'adyen_cc';\n\n // Validate Adyen component before any network activity\n if (isAdyen) {\n if (!adyenCard) {\n console.error('Adyen card not rendered.');\n return false;\n }\n\n if (!adyenCard.state?.isValid) {\n adyenCard.showValidation?.();\n return false;\n }\n }\n\n // Adyen-specific payment handling\n if (isAdyen) {\n return new Promise((resolve, reject) => {\n // eslint-disable-next-line no-underscore-dangle\n adyenCard._orderPromise = { resolve, reject };\n adyenCard.submit();\n });\n }\n\n // Default payment handling\n await orderApi.placeOrder(cartId);\n } catch (error) {\n console.error(error);\n throw error;\n } finally {\n removeOverlaySpinner(loaderRef, $loader);\n }\n };\n\n // Adyen-specific payment methods renderer\n async function renderAdyenPaymentMethods(container) {\n return CheckoutProvider.render(PaymentMethods, {\n slots: {\n Methods: {\n adyen_cc: {\n autoSync: false,\n render: async (ctx) => {\n // Create container in the slot render\n const $adyenCardContainer = document.createElement('div');\n $adyenCardContainer.className = 'adyen-card-container';\n\n // Append the container to the slot\n ctx.appendChild($adyenCardContainer);\n\n // Initialize Adyen each time the slot renders\n ctx.onRender(async () => {\n // Check if Adyen is already mounted to this specific container\n if ($adyenCardContainer.hasChildNodes()) {\n return;\n }\n\n // Clear any previous adyenCard reference since we're mounting to a new container\n adyenCard = null;\n\n try {\n // Dynamically import Adyen Web v6.x as an ES module\n await loadScript('https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/6.16.0/adyen.js', {});\n // Load Adyen CSS from CDN if not already loaded\n await loadCSS('https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/6.16.0/adyen.css');\n\n // Access AdyenWeb safely without optional-chaining to satisfy ESLint\n const { AdyenCheckout, Card } = (window.AdyenWeb) || {};\n\n if (!AdyenCheckout) {\n console.error('AdyenCheckout not available after import.');\n return;\n }\n\n const checkout = await AdyenCheckout({\n clientKey: 'test_UJLHEXDC5JDOZBLAHE7EB4XCAEANSI6H',\n locale: 'en_US',\n environment: 'test',\n countryCode: 'US',\n paymentMethodsResponse: {\n paymentMethods: [\n {\n name: 'Cards',\n type: 'scheme',\n brand: null,\n brands: [\n 'visa',\n 'mc',\n 'amex',\n 'discover',\n 'cup',\n 'diners',\n ],\n configuration: null,\n },\n ],\n },\n onSubmit: async (state, component) => {\n const additionalData = {\n stateData: JSON.stringify(state.data),\n };\n try {\n const paymentMethod = {\n code: 'adyen_cc',\n adyen_additional_data_cc: additionalData,\n };\n\n const currentCartId = ctx.cartId;\n await orderApi.setPaymentMethodAndPlaceOrder(currentCartId, paymentMethod);\n\n // Resolve the promise in handlePlaceOrder\n // eslint-disable-next-line no-underscore-dangle\n adyenCard._orderPromise.resolve();\n } catch (error) {\n // Reject the promise in handlePlaceOrder\n component.setStatus('ready');\n // eslint-disable-next-line no-underscore-dangle\n adyenCard._orderPromise.reject(error);\n }\n },\n });\n\n // Create and mount Adyen card\n adyenCard = new Card(checkout, {\n showPayButton: false,\n });\n adyenCard.mount($adyenCardContainer);\n } catch (error) {\n console.error('Failed to initialize Adyen:', error);\n }\n });\n },\n },\n [PaymentMethodCode.CREDIT_CARD]: {\n render: (ctx) => {\n const $creditCard = document.createElement('div');\n\n PaymentServices.render(CreditCard, {\n apiUrl: commerceCoreEndpoint,\n getCustomerToken: getUserTokenCookie,\n getCartId: () => ctx.cartId,\n creditCardFormRef,\n })($creditCard);\n\n ctx.replaceHTML($creditCard);\n },\n },\n [PaymentMethodCode.SMART_BUTTONS]: {\n enabled: false,\n },\n [PaymentMethodCode.APPLE_PAY]: {\n enabled: false,\n },\n [PaymentMethodCode.GOOGLE_PAY]: {\n enabled: false,\n },\n [PaymentMethodCode.VAULT]: {\n enabled: false,\n },\n [PaymentMethodCode.FASTLANE]: {\n enabled: false,\n },\n },\n },\n })(container);\n }\n\n // First, render the place order component\n await renderPlaceOrder($placeOrder, { handleValidation, handlePlaceOrder });\n\n // Render the remaining containers\n const [\n _mergedCartBanner,\n _header,\n _serverError,\n _outOfStock,\n _loginForm,\n shippingFormSkeleton,\n _billToShipping,\n _shippingMethods,\n _paymentMethods,\n billingFormSkeleton,\n _orderSummary,\n _cartSummary,\n _termsAndConditions,\n _giftOptions,\n ] = await Promise.all([\n renderMergedCartBanner($mergedCartBanner),\n\n renderCheckoutHeader($heading, 'Adyen Checkout'),\n\n renderServerError($serverError, $content),\n\n renderOutOfStock($outOfStock),\n\n renderLoginForm($login),\n\n renderShippingAddressFormSkeleton($shippingForm),\n\n renderBillToShippingAddress($billToShipping),\n\n renderShippingMethods($delivery),\n\n renderAdyenPaymentMethods($paymentMethods),\n\n renderBillingAddressFormSkeleton($billingForm),\n\n renderOrderSummary($orderSummary),\n\n renderCartSummaryList($cartSummary),\n\n renderTermsAndConditions($termsAndConditions),\n\n renderGiftOptions($giftOptions),\n ]);\n\n async function initializeCheckout(data) {\n await initReCaptcha(0);\n if (data.isGuest) await displayGuestAddressForms(data);\n else {\n removeOverlaySpinner(loaderRef, $loader);\n await displayCustomerAddressForms(data);\n }\n }\n\n async function displayGuestAddressForms(data) {\n if (isVirtualCart(data)) {\n shippingForm?.remove();\n shippingForm = null;\n $shippingForm.innerHTML = '';\n } else if (!shippingForm) {\n shippingFormSkeleton.remove();\n\n shippingForm = await renderAddressForm($shippingForm, shippingFormRef, data, 'shipping');\n }\n\n if (!billingForm) {\n billingFormSkeleton.remove();\n\n billingForm = await renderAddressForm($billingForm, billingFormRef, data, 'billing');\n }\n }\n\n async function displayCustomerAddressForms(data) {\n if (isVirtualCart(data)) {\n shippingAddresses?.remove();\n shippingAddresses = null;\n $shippingForm.innerHTML = '';\n } else if (!shippingAddresses) {\n shippingForm?.remove();\n shippingForm = null;\n shippingFormRef.current = null;\n\n shippingAddresses = await renderCustomerShippingAddresses(\n $shippingForm,\n shippingFormRef,\n data,\n );\n }\n\n if (!billingAddresses) {\n billingForm?.remove();\n billingForm = null;\n billingFormRef.current = null;\n\n billingAddresses = await renderCustomerBillingAddresses(\n $billingForm,\n billingFormRef,\n data,\n );\n }\n }\n\n async function handleCheckoutUpdated(data) {\n if (!data) return;\n await initializeCheckout(data);\n }\n\n function handleAuthenticated(authenticated) {\n if (!authenticated) return;\n\n // When a customer creates an account on the checkout success page and then\n // signs in, they will be redirected to the order details page with the order\n // number as orderRef, allowing the order details to be displayed\n const orderData = events.lastPayload('order/placed');\n if (orderData) {\n const url = buildOrderDetailsUrl(orderData);\n window.history.pushState({}, '', url);\n }\n\n window.location.reload();\n }\n\n function handleCheckoutValues(payload) {\n const { isBillToShipping } = payload;\n $billingForm.style.display = isBillToShipping ? 'none' : 'block';\n }\n\n async function handleOrderPlaced(orderData) {\n // Clear address form data\n sessionStorage.removeItem(SHIPPING_ADDRESS_DATA_KEY);\n sessionStorage.removeItem(BILLING_ADDRESS_DATA_KEY);\n\n const url = buildOrderDetailsUrl(orderData);\n\n window.history.pushState({}, '', url);\n\n await renderCheckoutSuccess(block, { orderData });\n }\n\n events.on('authenticated', handleAuthenticated);\n events.on('checkout/initialized', handleCheckoutUpdated, { eager: true });\n events.on('checkout/updated', handleCheckoutUpdated);\n events.on('checkout/values', handleCheckoutValues);\n events.on('order/placed', handleOrderPlaced);\n events.on('cart/initialized', redirectToCartIfEmpty, { eager: true });\n events.on('cart/data', redirectToCartIfEmpty);\n}\n",
|
|
306
|
+
"commerce-checkout-adyen.css": ".checkout__content {\n display: grid;\n grid-template-columns: 1fr;\n gap: var(--spacing-big) 0;\n}\n\n.checkout__merged-cart-banner {\n display: grid;\n grid-column: 1 / -1;\n align-items: start;\n grid-template-columns: auto;\n}\n\n.checkout__main {\n display: grid;\n row-gap: var(--spacing-xbig);\n margin-top: var(--spacing-medium);\n}\n\n.checkout__aside {\n display: grid;\n gap: var(--spacing-xbig);\n}\n\n.checkout-header h1 {\n margin: 0;\n}\n\n/* Block dividers */\n.checkout__block.checkout__heading .dropin-header-container {\n gap: var(--spacing-xsmall);\n}\n\n.checkout__shipping-form {\n padding-top: var(--spacing-xbig);\n border-top: var(--shape-border-width-3) solid var(--color-neutral-400);\n}\n\n.checkout__payment-methods {\n padding-top: var(--spacing-xbig);\n border-top: var(--shape-border-width-3) solid var(--color-neutral-400);\n padding-bottom: var(--spacing-xbig);\n border-bottom: var(--shape-border-width-3) solid var(--color-neutral-400);\n}\n\n/* Server error visibility */\n.checkout__server-error {\n display: none;\n}\n\n/* Show when it contains actual error content */\n.checkout__server-error:has(.dropin-illustrated-message) {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n}\n\n/* Safari fallback: show when not empty, but this may show empty divs briefly */\n@supports not selector(:has(*)) {\n .checkout__server-error:not(:empty) {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n }\n}\n\n/* Hide empty blocks */\n.checkout__block:empty {\n display: none;\n}\n\n/* Hide main containers when there is a server error */\n.checkout__content--error .checkout__merged-cart-banner,\n.checkout__content--error .checkout__out-of-stock,\n.checkout__content--error .checkout__login,\n.checkout__content--error .checkout__shipping-form,\n.checkout__content--error .checkout__bill-to-shipping,\n.checkout__content--error .checkout__delivery,\n.checkout__content--error .checkout__payment-methods,\n.checkout__content--error .checkout__billing-form,\n.checkout__content--error .checkout__terms-and-conditions {\n display: none !important;\n}\n\n/* Hide blocks with empty divs */\n.checkout__out-of-stock:has(> :empty),\n.checkout__merged-cart-banner:has(> :empty),\n.checkout__delivery:has(> div:first-child:empty),\n.checkout__bill-to-shipping:has(> :empty),\n.checkout__gift-options:has(.cart-gift-options-view--readonly:empty) {\n display: none;\n}\n\n/* Hide aside containers when there is a server error */\n.checkout__content--error .checkout__aside {\n display: none;\n}\n\n/* Integrate place order button into Order Summary - mobile */\n.checkout__place-order {\n grid-column: unset;\n justify-items: unset;\n margin-top: calc(var(--spacing-big) * -1);\n}\n\n/* Hide the place order button when there is a server error */\n.checkout__content--error .checkout__place-order {\n display: none;\n}\n\n.checkout__loader {\n align-items: center;\n background: var(--color-neutral-50);\n display: flex;\n height: 100vh;\n justify-content: center;\n left: 0;\n opacity: 0.5;\n position: fixed;\n top: 0;\n width: 100%;\n z-index: 9999;\n}\n\n.checkout__loader:empty {\n display: none;\n}\n\n/* remove margin from the heading divider */\n.checkout__heading .dropin-divider {\n margin: 0;\n}\n\n/* Cart Summary */\n.checkout__block .cart-cart-summary-list {\n padding: var(--spacing-medium);\n}\n\n/* Order Summary Coupon */\n.dropin-accordion-section__heading {\n margin: var(--spacing-medium) auto;\n}\n\n.cart-coupons__accordion {\n margin-top: var(--spacing-xsmall);\n}\n\n/* temporary fix to hide the default cart heading */\n[data-testid=\"default-cart-heading\"] {\n display: none;\n}\n\n.cart-summary-list__heading {\n display: flex;\n justify-content: space-between;\n align-items: flex-end;\n}\n\n.cart-summary-list__heading-text {\n font: var(--type-headline-2-strong-font);\n letter-spacing: var(--type-headline-2-strong-letter-spacing);\n color: var(--color-neutral-800);\n}\n\n.cart-cart-summary-list__heading {\n row-gap: var(--spacing-small);\n padding-top: 0;\n}\n\n.cart-cart-summary-list__heading-text {\n font: var(--type-headline-2-strong-font);\n letter-spacing: var(--type-headline-2-strong-letter-spacing);\n color: var(--color-neutral-800);\n}\n\n.cart-summary-list__edit {\n font: var(--type-body-2-strong-font);\n letter-spacing: var(--type-body-2-strong-letter-spacing);\n}\n\n.checkout__block\n.cart-cart-summary-list\n.cart-cart-summary-list__footer-divider {\n margin: var(--spacing-small) 0;\n}\n\n/* Sign-in modal */\n#modal {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-color: rgb(0 0 0 / 50%);\n display: flex;\n justify-content: center;\n align-items: center;\n z-index: 2;\n}\n\n#modal-form {\n width: 800px;\n}\n\n/* Address form */\n.checkout__shipping-form .account-address-form-wrapper__title,\n.checkout__shipping-form .dropin-header-container__title,\n.checkout__billing-form .account-address-form-wrapper__title,\n.checkout__billing-form .dropin-header-container__title {\n font: var(--type-headline-2-default-font);\n letter-spacing: var(--type-headline-2-default-letter-spacing);\n color: var(--color-neutral-800);\n margin: 0 0 var(--spacing-medium) 0;\n}\n\n.checkout__shipping-form .dropin-header-container .dropin-divider,\n.checkout__billing-form .dropin-header-container .dropin-divider {\n display: none;\n}\n\n/* Order confirmation */\n.order-confirmation {\n display: grid;\n align-items: start;\n grid-template-columns: repeat(var(--grid-4-columns), 1fr);\n grid-template-areas: \"main aside\";\n grid-column-gap: var(--grid-4-gutters);\n margin-bottom: var(--spacing-xbig);\n padding-top: var(--spacing-xxlarge);\n}\n\n.order-confirmation__main {\n display: grid;\n grid-row-gap: var(--spacing-xbig);\n grid-column: 1 / span 7;\n}\n\n.order-confirmation__aside {\n display: grid;\n grid-row-gap: var(--spacing-xbig);\n grid-column: 9 / span 4;\n}\n\n.order-confirmation__footer {\n display: grid;\n gap: var(--spacing-small);\n text-align: center;\n}\n\n.order-confirmation__footer p {\n margin: 0;\n}\n\n.order-confirmation__footer .order-confirmation-footer__continue-button {\n margin: 0 auto;\n text-align: center;\n display: inline-block;\n}\n\n.order-confirmation-footer__contact-support {\n font: var(--type-body-2-default-font);\n letter-spacing: var(--type-body-2-default-letter-spacing);\n color: var(--color-neutral-700);\n}\n\n.order-confirmation-footer__contact-support a {\n font: var(--type-body-2-strong-font);\n letter-spacing: var(--type-body-2-strong-letter-spacing);\n color: var(--color-brand-500);\n cursor: pointer;\n}\n\n/* Hide empty blocks */\n.order-confirmation__block:empty {\n display: none;\n}\n\n@media only screen and (min-width: 320px) and (max-width: 768px) {\n .checkout__main,\n .checkout__aside {\n display: contents;\n }\n\n .checkout__block {\n order: 3;\n }\n\n .checkout__heading {\n order: 1;\n }\n\n .checkout__cart-summary {\n order: 2;\n }\n\n .checkout__place-order {\n order: 4;\n }\n\n .order-confirmation {\n grid-template-columns: repeat(var(--grid-1-columns), 1fr);\n padding-top: 0;\n }\n\n .order-confirmation__main,\n .order-confirmation__aside {\n grid-row-gap: var(--spacing-medium);\n }\n\n .order-confirmation > div {\n grid-column: 1 / span 4;\n }\n\n .order-confirmation__block .dropin-card {\n border: 0;\n }\n}\n\n@media only screen and (min-width: 768px) {\n .checkout__content {\n display: grid;\n align-items: start;\n grid-template-columns: repeat(var(--grid-4-columns), 1fr);\n gap: var(--spacing-big) var(--grid-4-gutters);\n }\n\n .checkout__content--error {\n display: grid;\n grid-template-columns: 1fr;\n }\n\n .checkout__main {\n grid-column: 1 / span 7;\n row-gap: var(--spacing-xbig);\n }\n\n .checkout__aside {\n grid-column: 9 / span 4;\n gap: var(--spacing-xbig);\n }\n\n .checkout__place-order {\n margin-top: 0;\n }\n}\n",
|
|
307
|
+
"README.md": "# Adyen Integration Guide\n\nThis guide walks you through integrating Adyen payments with Adobe Commerce and AEM Commerce Checkout. The integration covers both backend Commerce configuration and frontend checkout implementation.\n\n## Step 1: Install and Setup Adyen Extension in the Commerce Instance\n\n```bash\ncomposer require adyen/module-payment\n```\n\n> **Note**: At the time of this integration, the installed version was 9.18.1.\n\n## Step 2: Verify Payment Method\n\n1. Navigate to your Commerce admin panel\n2. Configure the Adyen payment method settings\n3. Verify installation by visiting `/checkout` - Adyen should appear in the payment methods list if enabled.\n\n## Step 3: Install Checkout Dropin\n\nInstall the Adobe Commerce Checkout dropin `@dropins/storefront-checkout` in your AEM site. The recommended version for this integration is **v2.0.0**:\n\n```bash\nnpm install @dropins/storefront-checkout@2.0.0 --save\n```\n\n> This dropin provides the core checkout containers and infrastructure that the Adyen payment method will plug into.\n\n## Step 4: Verify Integration\n\n> **Note**: This step assumes your AEM boilerplate has a `commerce-checkout.js` block already implemented and configured.\n\n1. Navigate to `/checkout`\n2. Check the browser console for the `checkout/initialized` event\n3. Verify that `adyen_cc` appears in the `availablePaymentMethods` object\n\n## Step 5: Load the Adyen Assets, Styles and Configure the Payment Method slot\n\nFollow the [official Adyen documentation](https://docs.adyen.com/online-payments/build-your-integration/sessions-flow/?platform=Web&integration=Drop-in&version=6.16.0#install-adyen-web) for full SDK details. The Checkout block loads the Adyen Drop-in assets directly from Adyen's CDN.\n\nIn your checkout block, locate the `CheckoutProvider.render(PaymentMethods, {...})` call and add the Adyen payment method (`adyen_cc`) to the slots structure:\n\n```javascript\nCheckoutProvider.render(PaymentMethods, {\n slots: {\n Methods: {\n adyen_cc: {\n autoSync: false,\n render: async (ctx) => {\n // Load the Adyen JS and CSS when the payment-method slot is rendered\n await loadScript('https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/6.16.0/adyen.js', {});\n await loadCSS('https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/6.16.0/adyen.css');\n\n const { AdyenCheckout, Card } = (window.AdyenWeb) || {};\n\n if (!AdyenCheckout) {\n console.error('AdyenCheckout not available after import.');\n return;\n }\n // Adyen implementation goes here\n },\n },\n // ... other payment methods\n },\n },\n})\n```\n\nThe assets are fetched directly from the CDN at runtime via `loadScript`/`loadCSS`, keeping your bundle lean and always on the specified SDK version.\n\n**Key points:**\n\n- **Path**: `slots > Methods > adyen_cc > render`\n- **autoSync: false**: Prevents the payment method from automatically syncing its state with the backend when selected.\n- **render function**: Where you implement the Adyen Card component (detailed in Step 8)\n\n## Step 6: Setup Adyen Global Configuration\n\n**Create Global Configuration**: Follow the [Adyen documentation to set up a global configuration object](https://docs.adyen.com/online-payments/build-your-integration/sessions-flow/?platform=Web&integration=Drop-in&version=6.16.0#id552021099)\n\n## Step 7: Important - 3rd Party Component Integration Pattern\n\nWhen integrating 3rd party components like Adyen within dropin slots, you **must** follow this specific pattern:\n\nFirst, declare the `adyenCard` variable at the block level so it can be accessed by both the payment method slot and the `handlePlaceOrder` function:\n\n```javascript\n// Add this declaration at the block level\nlet adyenCard;\n```\n\nLocate the `CheckoutProvider.render(PaymentMethods, {...})` and mount the Adyen component once the slot is in the DOM:\n\n```javascript\nadyen_cc: {\n render: async (ctx) => {\n // 1. Create the container element\n const $container = document.createElement('div');\n \n // 2. Add container to slot (this is asynchronous/queued)\n ctx.appendChild($container);\n \n // 3. Use onRender to wait for DOM update before mounting 3rd party component\n ctx.onRender(async () => {\n // Check if component is already mounted to prevent duplicates\n if ($container.hasChildNodes()) {\n return;\n }\n \n // Clear any previous reference when mounting to a new container\n adyenCard = null;\n \n // 4. Now mount the 3rd party component to the DOM-connected element\n const checkout = await AdyenCheckout(config);\n adyenCard = new Card(checkout, options);\n adyenCard.mount($container);\n });\n }\n}\n```\n\n### Why This Pattern is Required\n\n- **Slot methods are asynchronous**: `ctx.appendChild()`, `ctx.replaceWith()`, etc. don't immediately update the DOM - they queue changes\n- **3rd party components need DOM-connected elements**: Adyen (and most other components) require mounting to elements that are already in the document\n- **`ctx.onRender()` ensures proper timing**: This callback runs after the slot's DOM has been updated\n- **Duplicate prevention**: The `hasChildNodes()` check prevents remounting the same component multiple times\n- **Framework handles cleanup automatically**: When users switch payment methods, the slot system automatically cleans up and re-renders\n\n#### ❌ Don't Do This (Won't Work)\n\n```javascript\nctx.appendChild($container);\nnew Card(checkout).mount($container); // ← Container not in DOM yet!\n```\n\n#### ✅ Do This (Correct)\n\n```javascript\nctx.appendChild($container);\nctx.onRender(() => {\n // Check if already mounted to prevent duplicates\n if ($container.hasChildNodes()) {\n return;\n }\n \n // Clear previous reference for new container\n adyenCard = null;\n \n adyenCard = new Card(checkout, options);\n adyenCard.mount($container); // ← Container is now in DOM\n});\n```\n\n## Step 8: Handle Async Payment Processing\n\nAdyen uses an asynchronous callback pattern that requires special handling to integrate with the synchronous `handlePlaceOrder` flow. You need to create a Promise bridge:\n\n```javascript\n// In the Adyen onSubmit callback\nonSubmit: async (state, component) => {\n try {\n // ... payment processing logic ...\n \n // Resolve the promise in handlePlaceOrder\n adyenCard._orderPromise.resolve();\n } catch (error) {\n component.setStatus('ready');\n \n // Reject the promise in handlePlaceOrder\n adyenCard._orderPromise.reject(error);\n }\n}\n\n// In handlePlaceOrder function of the PlaceOrder container\nconst isAdyen = code === 'adyen_cc';\n\ntry {\n if (isAdyen) {\n // Create a promise that resolves/rejects based on the onSubmit callback\n await new Promise((resolve, reject) => {\n // Store the resolve/reject functions so onSubmit can call them\n adyenCard._orderPromise = { resolve, reject };\n \n adyenCard.submit();\n });\n }\n} catch () {\n // Catch error\n}\n```\n\n### Why This Promise Bridge is Required\n\n**The Problem**: Adyen's payment flow is fundamentally different from other payment methods:\n\n- **Other payment methods**: Synchronous flow where `handlePlaceOrder` can `await` the payment directly\n- **Adyen payment method**: Asynchronous callback flow where `adyenCard.submit()` returns immediately, but the actual payment processing happens later in the `onSubmit` callback\n\n**Without the Promise bridge**:\n\n```javascript\n// This doesn't work properly:\nadyenCard.submit(); // Returns immediately\nreturn; // handlePlaceOrder exits immediately\n// ... later, onSubmit callback runs but handlePlaceOrder is long gone\n```\n\n**Result**:\n\n- ❌ Spinner gets removed too early\n- ❌ Errors don't bubble up to the main error handler\n- ❌ Inconsistent user experience\n\n**With the Promise bridge**:\n\n```javascript\n// This works correctly:\nawait new Promise((resolve, reject) => {\n adyenCard._orderPromise = { resolve, reject };\n adyenCard.submit();\n});\n// handlePlaceOrder waits here until onSubmit calls resolve/reject\n```\n\n**Result**:\n\n- ✅ Spinner stays visible during payment processing\n- ✅ Errors bubble up to the main error handler\n- ✅ Consistent user experience with other payment methods\n- ✅ Proper async flow coordination\n\n## Step 9: Implement the onSubmit Callback\n\nThe `onSubmit` callback is where the actual payment processing happens. Here's the complete sample implementation you need:\n\n```javascript\nconst checkout = await AdyenCheckout({\n ...globalConfiguration,\n onSubmit: async (state, component) => {\n const additionalData = {\n stateData: JSON.stringify(state.data),\n };\n try {\n const paymentMethod = {\n code: 'adyen_cc',\n adyen_additional_data_cc: additionalData,\n };\n\n await orderApi.setPaymentMethodAndPlaceOrder(ctx.cartId, paymentMethod);\n\n // Resolve the promise in handlePlaceOrder\n adyenCard._orderPromise.resolve();\n } catch (error) {\n component.setStatus('ready');\n // Reject the promise in handlePlaceOrder\n adyenCard._orderPromise.reject(error);\n }\n },\n});\n\n// Mount the Adyen Card component\nadyenCard = new Card(checkout, { showPayButton: false });\nadyenCard.mount($adyenCardContainer);\n```\n\n### What This Implementation Does\n\n1. **Extracts Payment Data** from the Drop-in (`state.data`).\n2. **Prepares Backend Payload** as `adyen_additional_data_cc`.\n3. **Sets Payment Method & Places Order** via `orderApi.setPaymentMethodAndPlaceOrder()`.\n4. **Bridges Async Flow** by resolving / rejecting the Promise stored in `adyenCard._orderPromise`.\n5. **Handles Errors** cleanly—any exception rejects the bridge promise so `handlePlaceOrder` can react.\n6. **Mounts the Component** with `showPayButton: false` so the primary Checkout button controls submission.\n\n## Step 10: Configure PlaceOrder Container\n\n```javascript\nCheckoutProvider.render(PlaceOrder, {\n handlePlaceOrder: async ({ cartId, code }) => {\n const isAdyen = code === 'adyen_cc';\n\n if (isAdyen) {\n if (!adyenCard) {\n console.error('Adyen card not rendered.');\n return;\n }\n\n if (!adyenCard.state?.isValid) {\n adyenCard.showValidation?.();\n return;\n }\n }\n\n await displayOverlaySpinner();\n \n try {\n if (isAdyen) {\n await new Promise((resolve, reject) => {\n adyenCard._orderPromise = { resolve, reject };\n adyenCard.submit();\n });\n return;\n }\n } finally {\n removeOverlaySpinner();\n }\n },\n})($placeOrder);\n```\n\n### Key Configuration Points\n\n1. **Early Validation**: Ensures the shopper can't proceed until the card fields are valid.\n2. **Promise Bridge**: Keeps the spinner up while Adyen's async `onSubmit` finishes.\n3. **Single Spinner**: Shown once for the whole sequence, hidden in `finally`.\n4. **Error Safety**: Any error re-throws after the spinner is dismissed.\n\n#### Important Notes\n\n- **Order Creation** happens in the `onSubmit` callback via `orderApi.setPaymentMethodAndPlaceOrder()`.\n- **Spinner Management**: The overlay spinner is displayed only when the flow is ready to perform network operations and is removed regardless of success/failure.\n- **Validation UX**: `adyenCard.showValidation()` highlights any missing or invalid fields for the shopper.\n\nThat completes the Adyen Drop-in integration steps.\n",
|
|
308
|
+
"constants.js": "// Form and address constants\nconst BILLING_FORM_NAME = 'selectedBillingAddress';\nconst BILLING_ADDRESS_DATA_KEY = `${BILLING_FORM_NAME}_addressData`;\nconst LOGIN_FORM_NAME = 'login-form';\nconst PURCHASE_ORDER_FORM_NAME = 'purchase-order';\nconst SHIPPING_FORM_NAME = 'selectedShippingAddress';\nconst SHIPPING_ADDRESS_DATA_KEY = `${SHIPPING_FORM_NAME}_addressData`;\nconst TERMS_AND_CONDITIONS_FORM_NAME = 'checkout-terms-and-conditions__form';\n\n// Timing constants\nconst DEBOUNCE_TIME = 1000;\nconst ADDRESS_INPUT_DEBOUNCE_TIME = 500;\n\n// Block and styling constants\nconst CHECKOUT_BLOCK = 'checkout__block';\nconst CHECKOUT_ERROR_CLASS = 'checkout__content--error';\nconst CHECKOUT_HEADER_CLASS = 'checkout-header';\nconst ORDER_CONFIRMATION_BLOCK = 'order-confirmation__block';\n\n// Default values\nconst USER_TOKEN_COOKIE_NAME = 'auth_dropin_user_token';\n\nexport {\n // Form and address constants\n BILLING_ADDRESS_DATA_KEY,\n BILLING_FORM_NAME,\n LOGIN_FORM_NAME,\n PURCHASE_ORDER_FORM_NAME,\n SHIPPING_ADDRESS_DATA_KEY,\n SHIPPING_FORM_NAME,\n TERMS_AND_CONDITIONS_FORM_NAME,\n\n // Timing constants\n ADDRESS_INPUT_DEBOUNCE_TIME,\n DEBOUNCE_TIME,\n\n // Block and styling constants\n CHECKOUT_BLOCK,\n CHECKOUT_ERROR_CLASS,\n CHECKOUT_HEADER_CLASS,\n ORDER_CONFIRMATION_BLOCK,\n\n // Default values\n USER_TOKEN_COOKIE_NAME,\n};\n",
|
|
309
|
+
"containers.js": "/* eslint-disable max-len */\n/* eslint-disable import/no-unresolved */\n/* eslint-disable no-unused-vars */\n/* eslint-disable no-shadow */\n/* eslint-disable no-use-before-define */\n/* eslint-disable prefer-const */\n\n// Checkout Dropin\nimport * as checkoutApi from '@dropins/storefront-checkout/api.js';\nimport BillToShippingAddress from '@dropins/storefront-checkout/containers/BillToShippingAddress.js';\nimport EstimateShipping from '@dropins/storefront-checkout/containers/EstimateShipping.js';\nimport LoginForm from '@dropins/storefront-checkout/containers/LoginForm.js';\nimport MergedCartBanner from '@dropins/storefront-checkout/containers/MergedCartBanner.js';\nimport OutOfStock from '@dropins/storefront-checkout/containers/OutOfStock.js';\nimport PaymentMethods from '@dropins/storefront-checkout/containers/PaymentMethods.js';\nimport PlaceOrder from '@dropins/storefront-checkout/containers/PlaceOrder.js';\nimport ServerError from '@dropins/storefront-checkout/containers/ServerError.js';\nimport ShippingMethods from '@dropins/storefront-checkout/containers/ShippingMethods.js';\nimport TermsAndConditions from '@dropins/storefront-checkout/containers/TermsAndConditions.js';\nimport { render as CheckoutProvider } from '@dropins/storefront-checkout/render.js';\n\n// Auth Dropin\nimport * as authApi from '@dropins/storefront-auth/api.js';\nimport AuthCombine from '@dropins/storefront-auth/containers/AuthCombine.js';\nimport SignUp from '@dropins/storefront-auth/containers/SignUp.js';\nimport { render as AuthProvider } from '@dropins/storefront-auth/render.js';\n\n// Account Dropin\nimport Addresses from '@dropins/storefront-account/containers/Addresses.js';\nimport AddressForm from '@dropins/storefront-account/containers/AddressForm.js';\nimport { render as AccountProvider } from '@dropins/storefront-account/render.js';\n\n// Cart Dropin\nimport * as cartApi from '@dropins/storefront-cart/api.js';\nimport CartSummaryList from '@dropins/storefront-cart/containers/CartSummaryList.js';\nimport Coupons from '@dropins/storefront-cart/containers/Coupons.js';\nimport GiftCards from '@dropins/storefront-cart/containers/GiftCards.js';\nimport GiftOptions from '@dropins/storefront-cart/containers/GiftOptions.js';\nimport OrderSummary from '@dropins/storefront-cart/containers/OrderSummary.js';\nimport { render as CartProvider } from '@dropins/storefront-cart/render.js';\n\n// Payment Services Dropin\nimport { PaymentMethodCode } from '@dropins/storefront-payment-services/api.js';\nimport CreditCard from '@dropins/storefront-payment-services/containers/CreditCard.js';\nimport { render as PaymentServices } from '@dropins/storefront-payment-services/render.js';\n\n// Order Dropin\nimport CustomerDetails from '@dropins/storefront-order/containers/CustomerDetails.js';\nimport OrderCostSummary from '@dropins/storefront-order/containers/OrderCostSummary.js';\nimport OrderHeader from '@dropins/storefront-order/containers/OrderHeader.js';\nimport OrderProductList from '@dropins/storefront-order/containers/OrderProductList.js';\nimport OrderStatus from '@dropins/storefront-order/containers/OrderStatus.js';\nimport ShippingStatus from '@dropins/storefront-order/containers/ShippingStatus.js';\nimport { render as OrderProvider } from '@dropins/storefront-order/render.js';\n\n// Tools\nimport { debounce, getCookie } from '@dropins/tools/lib.js';\nimport { tryRenderAemAssetsImage } from '@dropins/tools/lib/aem/assets.js';\nimport { getConfigValue } from '@dropins/tools/lib/aem/configs.js';\nimport {\n Button,\n Header,\n provider as UI,\n} from '@dropins/tools/components.js';\nimport { events } from '@dropins/tools/event-bus.js';\n\n// Checkout Dropin Libraries\nimport {\n estimateShippingCost,\n getCartAddress,\n setAddressOnCart,\n transformCartAddressToFormValues,\n} from '@dropins/storefront-checkout/lib/utils.js';\n\n// Utils\nimport {\n showModal,\n swatchImageSlot,\n} from './utils.js';\n\n// External dependencies\nimport {\n authPrivacyPolicyConsentSlot,\n fetchPlaceholders,\n rootLink,\n} from '../../scripts/commerce.js';\n\n// Constants\nimport {\n ADDRESS_INPUT_DEBOUNCE_TIME,\n BILLING_ADDRESS_DATA_KEY,\n BILLING_FORM_NAME,\n CHECKOUT_ERROR_CLASS,\n CHECKOUT_HEADER_CLASS,\n DEBOUNCE_TIME,\n LOGIN_FORM_NAME,\n SHIPPING_ADDRESS_DATA_KEY,\n SHIPPING_FORM_NAME,\n USER_TOKEN_COOKIE_NAME,\n} from './constants.js';\n\n/**\n * Container IDs for registry management\n * @enum {string}\n */\nexport const CONTAINERS = Object.freeze({\n // Static containers (rendered in Promise.all)\n MERGED_CART_BANNER: 'mergedCartBanner',\n CHECKOUT_HEADER: 'checkoutHeader',\n SERVER_ERROR: 'serverError',\n OUT_OF_STOCK: 'outOfStock',\n LOGIN_FORM: 'loginForm',\n SHIPPING_ADDRESS_FORM_SKELETON: 'shippingAddressFormSkeleton',\n BILL_TO_SHIPPING_ADDRESS: 'billToShippingAddress',\n SHIPPING_METHODS: 'shippingMethods',\n PAYMENT_METHODS: 'paymentMethods',\n BILLING_ADDRESS_FORM_SKELETON: 'billingAddressFormSkeleton',\n ORDER_SUMMARY: 'orderSummary',\n CART_SUMMARY_LIST: 'cartSummaryList',\n TERMS_AND_CONDITIONS: 'termsAndConditions',\n PLACE_ORDER_BUTTON: 'placeOrderButton',\n GIFT_OPTIONS: 'giftOptions',\n CUSTOMER_SHIPPING_ADDRESSES: 'customerShippingAddresses',\n CUSTOMER_BILLING_ADDRESSES: 'customerBillingAddresses',\n\n // Dynamic containers (conditional rendering)\n SHIPPING_ADDRESS_FORM: 'shippingAddressForm',\n BILLING_ADDRESS_FORM: 'billingAddressForm',\n\n // Order confirmation containers\n ORDER_HEADER: 'orderHeader',\n ORDER_STATUS: 'orderStatus',\n SHIPPING_STATUS: 'shippingStatus',\n CUSTOMER_DETAILS: 'customerDetails',\n ORDER_COST_SUMMARY: 'orderCostSummary',\n ORDER_GIFT_OPTIONS: 'orderGiftOptions',\n ORDER_PRODUCT_LIST: 'orderProductList',\n ORDER_CONFIRMATION_FOOTER_BUTTON: 'orderConfirmationFooterButton',\n\n // Slot/Sub-containers (nested within other containers)\n ESTIMATE_SHIPPING: 'estimateShipping',\n CART_COUPONS: 'cartCoupons',\n GIFT_CARDS: 'giftCards',\n CART_GIFT_OPTIONS: 'cartGiftOptions',\n});\n\n/**\n * A Map to store the API of rendered containers.\n * The key is a unique string ID, and the value is the containers's API object.\n * (e.g., { setProps: (props) => {...}, remove: () => {...} })\n */\nconst registry = new Map();\n\n/**\n * Checks if a container with the given ID has been rendered.\n * This is used to prevent multiple instances of the same container from being rendered.\n * @param {string} id - The unique ID of the container to check.\n * @returns {boolean} - Returns true if the container has been rendered, false otherwise.\n */\nexport const hasContainer = (id) => registry.has(id);\n\n/**\n * Helper to get a container from the registry or render and register it if not present.\n * @async\n * @param {string} id - Unique identifier for the container.\n * @param {Function} renderFn - Async function that renders the container.\n * @returns {Promise<Object>} - The rendered container API.\n */\nconst renderContainer = async (id, renderFn) => {\n if (registry.has(id)) {\n return registry.get(id);\n }\n\n try {\n const container = await renderFn();\n registry.set(id, container);\n return container;\n } catch (error) {\n console.error(`Error rendering container ${id}:`, error);\n throw error;\n }\n};\n\n/**\n * Unmounts and removes a container from the registry.\n * This function checks if the container is registered, removes it from the DOM,\n * and deletes its reference from the registry.\n * @param {string} id - The unique ID of the container to unmount.\n * @return {void}\n */\nexport const unmountContainer = (id) => {\n if (!registry.has(id)) {\n return;\n }\n\n const containerApi = registry.get(id);\n containerApi.remove();\n registry.delete(id);\n};\n\n/**\n * Renders the merged cart banner notification for authenticated users\n * @param {HTMLElement} container - DOM element to render the banner in\n * @returns {Promise<Object>} - The rendered merged cart banner component\n */\nexport const renderMergedCartBanner = async (container) => renderContainer(\n CONTAINERS.MERGED_CART_BANNER,\n async () => CheckoutProvider.render(MergedCartBanner)(container),\n);\n\n/**\n * Renders the checkout page header with title and styling\n * @param {HTMLElement} container - DOM element to render the header in\n * @param {string} title - The title to display in the header\n * @returns {Promise<Object>} - The rendered checkout header component\n */\nexport const renderCheckoutHeader = async (container, title) => renderContainer(\n CONTAINERS.CHECKOUT_HEADER,\n async () => UI.render(Header, {\n className: CHECKOUT_HEADER_CLASS,\n divider: true,\n level: 1,\n size: 'large',\n title,\n })(container),\n);\n\n/**\n * Renders server error handling with retry functionality and error state management\n * @param {HTMLElement} container - DOM element to render the error component in\n * @param {HTMLElement} contentElement - Main content element to add error styling to\n * @returns {Promise<Object>} - The rendered server error component\n */\nexport const renderServerError = async (container, contentElement) => renderContainer(\n CONTAINERS.SERVER_ERROR,\n async () => CheckoutProvider.render(ServerError, {\n autoScroll: true,\n onRetry: () => {\n contentElement.classList.remove(CHECKOUT_ERROR_CLASS);\n },\n onServerError: () => {\n contentElement.classList.add(CHECKOUT_ERROR_CLASS);\n },\n })(container),\n);\n\n/**\n * Renders out of stock handling with cart navigation and product update options\n * @param {HTMLElement} container - DOM element to render the component in\n * @returns {Promise<Object>} - The rendered out-of-stock component\n */\nexport const renderOutOfStock = async (container) => renderContainer(\n CONTAINERS.OUT_OF_STOCK,\n async () => CheckoutProvider.render(OutOfStock, {\n routeCart: () => rootLink('/cart'),\n onCartProductsUpdate: (items) => {\n cartApi.updateProductsFromCart(items).catch(console.error);\n },\n })(container),\n);\n\n/**\n * Renders the login form for guest checkout with authentication options\n * Uses the existing 'authenticated' event system for decoupled communication\n * @param {HTMLElement} container - DOM element to render the login form in\n * @returns {Promise<Object>} - The rendered login form component\n */\nexport const renderLoginForm = async (container) => renderContainer(\n CONTAINERS.LOGIN_FORM,\n async () => CheckoutProvider.render(LoginForm, {\n name: LOGIN_FORM_NAME,\n onSignInClick: async (initialEmailValue) => {\n const signInForm = document.createElement('div');\n\n AuthProvider.render(AuthCombine, {\n signInFormConfig: {\n renderSignUpLink: true,\n initialEmailValue,\n // No onSuccessCallback needed - the 'authenticated' event will be fired automatically\n },\n signUpFormConfig: {\n slots: {\n ...authPrivacyPolicyConsentSlot,\n },\n },\n resetPasswordFormConfig: {},\n })(signInForm);\n\n await showModal(signInForm);\n },\n onSignOutClick: () => {\n authApi.revokeCustomerToken();\n },\n })(container),\n);\n\n/**\n * Renders the shipping address form skeleton (initial placeholder)\n * @param {HTMLElement} container - DOM element to render the form in\n * @returns {Promise<Object>} - The rendered shipping address form skeleton\n */\nexport const renderShippingAddressFormSkeleton = async (container) => renderContainer(\n CONTAINERS.SHIPPING_ADDRESS_FORM_SKELETON,\n async () => AccountProvider.render(AddressForm, {\n fieldIdPrefix: 'shipping',\n isOpen: true,\n showFormLoader: true,\n })(container),\n);\n\n/**\n * Renders the billing address form skeleton (initial placeholder)\n * @param {HTMLElement} container - DOM element to render the form in\n * @returns {Promise<Object>} - The rendered billing address form skeleton\n */\nexport const renderBillingAddressFormSkeleton = async (container) => renderContainer(\n CONTAINERS.BILLING_ADDRESS_FORM_SKELETON,\n async () => AccountProvider.render(AddressForm, {\n fieldIdPrefix: 'billing',\n isOpen: true,\n showFormLoader: true,\n })(container),\n);\n\n/**\n * Renders checkbox to set billing address same as shipping address - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render the checkbox in\n * @returns {Promise<Object>} - The rendered bill to shipping address component\n */\nexport const renderBillToShippingAddress = async (container) => renderContainer(\n CONTAINERS.BILL_TO_SHIPPING_ADDRESS,\n async () => {\n const setBillingAddressOnCart = setAddressOnCart({ type: 'billing' });\n\n return CheckoutProvider.render(BillToShippingAddress, {\n onChange: (checked) => {\n const billingFormValues = events.lastPayload('checkout/addresses/billing');\n\n if (!checked && billingFormValues) {\n setBillingAddressOnCart(billingFormValues);\n }\n },\n })(container);\n },\n);\n\n/**\n * Renders available shipping methods with selection interface\n * @param {HTMLElement} container - DOM element to render shipping methods in\n * @returns {Promise<Object>} - The rendered shipping methods component\n */\nexport const renderShippingMethods = async (container) => renderContainer(\n CONTAINERS.SHIPPING_METHODS,\n async () => CheckoutProvider.render(ShippingMethods)(container),\n);\n\n/**\n * Renders payment methods with credit card integration - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render payment methods in\n * @param {Object} creditCardFormRef - React-style ref for credit card form\n * @returns {Promise<Object>} - The rendered payment methods component\n */\nexport const renderPaymentMethods = async (container, creditCardFormRef) => renderContainer(\n CONTAINERS.PAYMENT_METHODS,\n async () => {\n // Retrieve constants internally to minimize parameters\n const commerceCoreEndpoint = getConfigValue('commerce-core-endpoint') || getConfigValue('commerce-endpoint');\n const getUserTokenCookie = () => getCookie(USER_TOKEN_COOKIE_NAME);\n\n return CheckoutProvider.render(PaymentMethods, {\n slots: {\n Methods: {\n [PaymentMethodCode.CREDIT_CARD]: {\n render: (ctx) => {\n const $creditCard = document.createElement('div');\n\n PaymentServices.render(CreditCard, {\n apiUrl: commerceCoreEndpoint,\n getCustomerToken: getUserTokenCookie,\n getCartId: () => ctx.cartId,\n creditCardFormRef,\n })($creditCard);\n\n ctx.replaceHTML($creditCard);\n },\n },\n [PaymentMethodCode.SMART_BUTTONS]: {\n enabled: false,\n },\n [PaymentMethodCode.APPLE_PAY]: {\n enabled: false,\n },\n [PaymentMethodCode.GOOGLE_PAY]: {\n enabled: false,\n },\n [PaymentMethodCode.VAULT]: {\n enabled: false,\n },\n [PaymentMethodCode.FASTLANE]: {\n enabled: false,\n },\n },\n },\n })(container);\n },\n);\n\n/**\n * Renders terms and conditions with agreement slots and manual consent mode\n * @param {HTMLElement} container - DOM element to render the terms in\n * @returns {Promise<Object>} - The rendered terms and conditions component\n */\nexport const renderTermsAndConditions = async (container) => renderContainer(\n CONTAINERS.TERMS_AND_CONDITIONS,\n async () => CheckoutProvider.render(TermsAndConditions, {\n slots: {\n Agreements: (ctx) => {\n ctx.appendAgreement(() => ({\n name: 'default',\n mode: 'manual',\n translationId: 'Checkout.TermsAndConditions.label',\n }));\n },\n },\n })(container),\n);\n\n/**\n * Renders estimate shipping form for order summary slot\n * @param {HTMLElement} ctx - The slot context element\n * @returns {void}\n */\nexport const renderEstimateShipping = (ctx) => {\n const estimateShippingForm = document.createElement('div');\n CheckoutProvider.render(EstimateShipping)(estimateShippingForm);\n ctx.appendChild(estimateShippingForm);\n};\n\n/**\n * Renders cart coupons for order summary slot\n * @param {HTMLElement} ctx - The slot context element\n * @returns {void}\n */\nexport const renderCartCoupons = (ctx) => {\n const coupons = document.createElement('div');\n CartProvider.render(Coupons)(coupons);\n ctx.appendChild(coupons);\n};\n\n/**\n * Renders gift cards for order summary slot\n * @param {HTMLElement} ctx - The slot context element\n * @returns {void}\n */\nexport const renderGiftCards = (ctx) => {\n const giftCards = document.createElement('div');\n CartProvider.render(GiftCards)(giftCards);\n ctx.appendChild(giftCards);\n};\n\n/**\n * Renders gift options for cart summary list footer slot\n * @param {HTMLElement} ctx - The slot context element\n * @returns {void}\n */\nexport const renderCartGiftOptions = (ctx) => {\n const giftOptions = document.createElement('div');\n\n CartProvider.render(GiftOptions, {\n item: ctx.item,\n view: 'product',\n dataSource: 'cart',\n isEditable: false,\n handleItemsLoading: ctx.handleItemsLoading,\n handleItemsError: ctx.handleItemsError,\n onItemUpdate: ctx.onItemUpdate,\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(giftOptions);\n\n ctx.appendChild(giftOptions);\n};\n\n// ============================================================================\n// SUMMARY CONTAINERS\n// ============================================================================\n\n/**\n * Renders order summary with estimate shipping, coupons, and gift cards slots\n * @param {HTMLElement} container - DOM element to render order summary in\n * @returns {Promise<Object>} - The rendered order summary component\n */\nexport const renderOrderSummary = async (container) => renderContainer(\n CONTAINERS.ORDER_SUMMARY,\n async () => CartProvider.render(OrderSummary, {\n slots: {\n EstimateShipping: renderEstimateShipping,\n Coupons: renderCartCoupons,\n GiftCards: renderGiftCards,\n },\n })(container),\n);\n\n/**\n * Creates the cart summary heading with item count and edit link\n * @param {Object} headingCtx - The heading context with count and DOM methods\n * @returns {void}\n */\nconst createCartSummaryHeading = (headingCtx, placeholders) => {\n const title = placeholders?.Checkout?.Summary?.heading;\n\n // Create main heading container\n const heading = document.createElement('div');\n heading.classList.add('cart-summary-list__heading');\n\n // Create heading text element\n const headingText = document.createElement('div');\n headingText.classList.add('cart-summary-list__heading-text');\n\n // Create edit cart link\n const editCartLink = document.createElement('a');\n editCartLink.classList.add('cart-summary-list__edit');\n editCartLink.href = rootLink('/cart');\n editCartLink.rel = 'noreferrer';\n editCartLink.innerText = placeholders?.Checkout?.Summary?.Edit;\n\n // Helper function to update count text\n const updateCountText = (count) => {\n headingText.innerText = title?.replace(\n '({count})',\n count ? `(${count})` : '',\n );\n };\n\n // Set initial count\n updateCountText(headingCtx.count);\n\n // Assemble heading\n heading.appendChild(headingText);\n heading.appendChild(editCartLink);\n headingCtx.appendChild(heading);\n\n // Listen for count changes\n headingCtx.onChange((nextCtx) => {\n updateCountText(nextCtx.count);\n });\n};\n\n/**\n * Renders cart summary list with custom heading, thumbnail and gift options slots\n * @param {HTMLElement} container - DOM element to render cart summary list in\n * @returns {Promise<Object>} - The rendered cart summary list component\n */\nexport const renderCartSummaryList = async (container) => renderContainer(\n CONTAINERS.CART_SUMMARY_LIST,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n return CartProvider.render(CartSummaryList, {\n variant: 'secondary',\n slots: {\n Heading: (headingCtx) => createCartSummaryHeading(headingCtx, placeholders),\n Thumbnail: (ctx) => {\n const { item, defaultImageProps } = ctx;\n tryRenderAemAssetsImage(ctx, {\n alias: item.sku,\n imageProps: defaultImageProps,\n params: {\n width: defaultImageProps.width,\n height: defaultImageProps.height,\n },\n });\n },\n Footer: renderCartGiftOptions,\n },\n })(container);\n },\n);\n\n/**\n * Renders place order button with handler functions - follows multi-step pattern\n * @param {HTMLElement} container - DOM element to render the place order button in\n * @param {Object} options - Configuration object with handler functions\n * @param {Function} options.handleValidation - Validation handler function\n * @param {Function} options.handlePlaceOrder - Place order handler function\n * @returns {Promise<Object>} - The rendered place order component\n */\nexport const renderPlaceOrder = async (container, options = {}) => renderContainer(\n CONTAINERS.PLACE_ORDER_BUTTON,\n async () => CheckoutProvider.render(PlaceOrder, {\n handleValidation: options.handleValidation,\n handlePlaceOrder: options.handlePlaceOrder,\n })(container),\n);\n\n/**\n * Renders customer shipping addresses selector/form for authenticated users - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render shipping addresses in\n * @param {Object} formRef - React-style ref for form reference\n * @param {Object} data - Cart data containing shipping address information\n * @returns {Promise<Object>} - The rendered customer shipping addresses component\n */\nexport const renderCustomerShippingAddresses = async (container, formRef, data) => renderContainer(\n CONTAINERS.CUSTOMER_SHIPPING_ADDRESSES,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n const cartShippingAddress = getCartAddress(data, 'shipping');\n\n const shippingAddressId = cartShippingAddress\n ? cartShippingAddress?.id ?? 0\n : undefined;\n\n const shippingAddressCache = sessionStorage.getItem(SHIPPING_ADDRESS_DATA_KEY);\n\n // Clear persisted shipping address if cart has a shipping address\n if (cartShippingAddress && shippingAddressCache) {\n sessionStorage.removeItem(SHIPPING_ADDRESS_DATA_KEY);\n }\n\n const storeConfig = checkoutApi.getStoreConfigCache();\n\n const inputsDefaultValueSet = cartShippingAddress && cartShippingAddress.id === undefined\n ? transformCartAddressToFormValues(cartShippingAddress)\n : { countryCode: storeConfig.defaultCountry };\n\n const hasCartShippingAddress = Boolean(data.shippingAddresses?.[0]);\n let isFirstRenderShipping = true;\n\n const setShippingAddressOnCart = setAddressOnCart({\n type: 'shipping',\n debounceMs: DEBOUNCE_TIME,\n });\n\n const estimateShippingCostOnCart = estimateShippingCost({\n api: checkoutApi.estimateShippingMethods,\n debounceMs: DEBOUNCE_TIME,\n });\n\n const notifyShippingValues = debounce((values) => {\n events.emit('checkout/addresses/shipping', values);\n }, ADDRESS_INPUT_DEBOUNCE_TIME);\n\n return AccountProvider.render(Addresses, {\n addressFormTitle: placeholders?.Checkout?.Addresses?.shippingAddressTitle,\n defaultSelectAddressId: shippingAddressId,\n fieldIdPrefix: 'shipping',\n formName: SHIPPING_FORM_NAME,\n forwardFormRef: formRef,\n inputsDefaultValueSet,\n minifiedView: false,\n onAddressData: (values) => {\n const canSetShippingAddressOnCart = !isFirstRenderShipping || !hasCartShippingAddress;\n if (canSetShippingAddressOnCart) setShippingAddressOnCart(values);\n if (!hasCartShippingAddress) estimateShippingCostOnCart(values);\n if (isFirstRenderShipping) isFirstRenderShipping = false;\n notifyShippingValues(values);\n },\n selectable: true,\n selectShipping: true,\n showBillingCheckBox: false,\n showSaveCheckBox: true,\n showShippingCheckBox: false,\n title: placeholders?.Checkout?.Addresses?.shippingAddressTitle,\n })(container);\n },\n);\n\n/**\n * Renders customer billing addresses selector/form for authenticated users - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render billing addresses in\n * @param {Object} formRef - React-style ref for form reference\n * @param {Object} data - Cart data containing billing address information\n * @returns {Promise<Object>} - The rendered customer billing addresses component\n */\nexport const renderCustomerBillingAddresses = async (container, formRef, data) => renderContainer(\n CONTAINERS.CUSTOMER_BILLING_ADDRESSES,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n const cartBillingAddress = getCartAddress(data, 'billing');\n\n const billingAddressId = cartBillingAddress\n ? cartBillingAddress?.id ?? 0\n : undefined;\n\n const billingAddressCache = sessionStorage.getItem(BILLING_ADDRESS_DATA_KEY);\n\n // Clear persisted billing address if cart has a billing address\n if (cartBillingAddress && billingAddressCache) {\n sessionStorage.removeItem(BILLING_ADDRESS_DATA_KEY);\n }\n\n const storeConfig = checkoutApi.getStoreConfigCache();\n\n const inputsDefaultValueSet = cartBillingAddress && cartBillingAddress.id === undefined\n ? transformCartAddressToFormValues(cartBillingAddress)\n : { countryCode: storeConfig.defaultCountry };\n\n const hasCartBillingAddress = Boolean(data.billingAddress);\n let isFirstRenderBilling = true;\n\n const setBillingAddressOnCart = setAddressOnCart({\n type: 'billing',\n debounceMs: DEBOUNCE_TIME,\n });\n\n const notifyBillingValues = debounce((values) => {\n events.emit('checkout/addresses/billing', values);\n }, ADDRESS_INPUT_DEBOUNCE_TIME);\n\n return AccountProvider.render(Addresses, {\n addressFormTitle: placeholders?.Checkout?.Addresses?.billToNewAddress,\n defaultSelectAddressId: billingAddressId,\n formName: BILLING_FORM_NAME,\n forwardFormRef: formRef,\n inputsDefaultValueSet,\n minifiedView: false,\n onAddressData: (values) => {\n const canSetBillingAddressOnCart = !isFirstRenderBilling || !hasCartBillingAddress;\n if (canSetBillingAddressOnCart) setBillingAddressOnCart(values);\n if (isFirstRenderBilling) isFirstRenderBilling = false;\n notifyBillingValues(values);\n },\n selectable: true,\n selectBilling: true,\n showBillingCheckBox: false,\n showSaveCheckBox: true,\n showShippingCheckBox: false,\n title: placeholders?.Checkout?.Addresses?.billingAddressTitle,\n })(container);\n },\n);\n\n/**\n * Renders address form for guest users (shipping or billing) - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render address form in\n * @param {Object} formRef - React-style ref for form reference\n * @param {Object} data - Cart data containing address information\n * @param {string} addressType - Type of address form ('shipping' or 'billing')\n * @returns {Promise<Object>} - The rendered address form component\n */\nexport const renderAddressForm = async (container, formRef, data, addressType) => {\n const isShipping = addressType === 'shipping';\n const containerKey = isShipping ? CONTAINERS.SHIPPING_ADDRESS_FORM : CONTAINERS.BILLING_ADDRESS_FORM;\n\n return renderContainer(\n containerKey,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n // Get address type specific configurations\n const cartAddress = getCartAddress(data, addressType);\n const addressDataKey = isShipping ? SHIPPING_ADDRESS_DATA_KEY : BILLING_ADDRESS_DATA_KEY;\n const addressCache = sessionStorage.getItem(addressDataKey);\n\n // Clear persisted address if cart has an address\n if (cartAddress && addressCache) {\n sessionStorage.removeItem(addressDataKey);\n }\n\n let isFirstRender = true;\n const hasCartAddress = Boolean(isShipping ? data.shippingAddresses?.[0] : data.billingAddress);\n\n const setAddressOnCartFn = setAddressOnCart({\n type: addressType,\n debounceMs: DEBOUNCE_TIME,\n });\n\n // Create shipping cost estimator (only for shipping addresses)\n const estimateShippingCostOnCart = isShipping ? estimateShippingCost({\n api: checkoutApi.estimateShippingMethods,\n debounceMs: DEBOUNCE_TIME,\n }) : null;\n\n const notifyValues = debounce((values) => {\n const eventType = isShipping ? 'checkout/addresses/shipping' : 'checkout/addresses/billing';\n events.emit(eventType, values);\n }, ADDRESS_INPUT_DEBOUNCE_TIME);\n\n const storeConfig = checkoutApi.getStoreConfigCache();\n\n // Address type specific configurations\n const formName = isShipping ? SHIPPING_FORM_NAME : BILLING_FORM_NAME;\n const addressTitle = isShipping\n ? placeholders?.Checkout?.Addresses?.shippingAddressTitle\n : placeholders?.Checkout?.Addresses?.billingAddressTitle;\n const className = isShipping\n ? 'checkout-shipping-form__address-form'\n : 'checkout-billing-form__address-form';\n\n const inputsDefaultValueSet = cartAddress\n ? transformCartAddressToFormValues(cartAddress)\n : { countryCode: storeConfig.defaultCountry };\n\n return AccountProvider.render(AddressForm, {\n addressesFormTitle: addressTitle,\n className,\n fieldIdPrefix: addressType,\n formName,\n forwardFormRef: formRef,\n hideActionFormButtons: true,\n inputsDefaultValueSet,\n isOpen: true,\n onChange: (values) => {\n const canSetAddressOnCart = !isFirstRender || !hasCartAddress;\n if (canSetAddressOnCart) setAddressOnCartFn(values);\n\n // Only estimate shipping cost for shipping addresses when no cart address exists\n if (isShipping && !hasCartAddress && estimateShippingCostOnCart) {\n estimateShippingCostOnCart(values);\n }\n\n if (isFirstRender) isFirstRender = false;\n\n notifyValues(values);\n },\n showBillingCheckBox: false,\n showFormLoader: false,\n showShippingCheckBox: false,\n })(container);\n },\n );\n};\n\n/**\n * Renders order-level gift options with swatch image integration\n * @param {HTMLElement} container - DOM element to render gift options in\n * @returns {Promise<Object>} - The rendered gift options component\n */\nexport const renderGiftOptions = async (container) => renderContainer(\n CONTAINERS.GIFT_OPTIONS,\n async () => CartProvider.render(GiftOptions, {\n view: 'order',\n dataSource: 'cart',\n isEditable: false,\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(container),\n);\n\n/**\n * Renders order confirmation header with email check and sign up integration\n * @param {HTMLElement} container - DOM element to render the order header in\n * @param {Object} options - Configuration object with handlers and order data\n * @returns {Promise<Object>} - The rendered order header component\n */\nexport const renderOrderHeader = async (container, options = {}) => renderContainer(\n CONTAINERS.ORDER_HEADER,\n async () => {\n const handleSignUpClick = async ({\n inputsDefaultValueSet,\n addressesData,\n }) => {\n const signUpForm = document.createElement('div');\n\n AuthProvider.render(SignUp, {\n inputsDefaultValueSet,\n addressesData,\n routeSignIn: () => rootLink('/customer/login'),\n routeRedirectOnEmailConfirmationClose: () => rootLink('/customer/account'),\n slots: {\n ...authPrivacyPolicyConsentSlot,\n },\n })(signUpForm);\n\n await showModal(signUpForm);\n };\n\n return OrderProvider.render(OrderHeader, {\n handleEmailAvailability: checkoutApi.isEmailAvailable,\n handleSignUpClick,\n ...options,\n })(container);\n },\n);\n\n/**\n * Renders the order status component\n * @param {HTMLElement} container - The DOM element to render the order status in\n * @returns {Promise<Object>} - The rendered order status component\n */\nexport const renderOrderStatus = async (container) => renderContainer(\n CONTAINERS.ORDER_STATUS,\n async () => OrderProvider.render(OrderStatus, { slots: { OrderActions: () => null } })(container),\n);\n\n/**\n * Renders the shipping status component\n * @param {HTMLElement} container - The DOM element to render the shipping status in\n * @returns {Promise<Object>} - The rendered shipping status component\n */\nexport const renderShippingStatus = async (container) => renderContainer(\n CONTAINERS.SHIPPING_STATUS,\n async () => OrderProvider.render(ShippingStatus)(container),\n);\n\n/**\n * Renders the customer details component\n * @param {HTMLElement} container - The DOM element to render the customer details in\n * @returns {Promise<Object>} - The rendered customer details component\n */\nexport const renderCustomerDetails = async (container) => renderContainer(\n CONTAINERS.CUSTOMER_DETAILS,\n async () => OrderProvider.render(CustomerDetails)(container),\n);\n\n/**\n * Renders the order cost summary component\n * @param {HTMLElement} container - The DOM element to render the order cost summary in\n * @returns {Promise<Object>} - The rendered order cost summary component\n */\nexport const renderOrderCostSummary = async (container) => renderContainer(\n CONTAINERS.ORDER_COST_SUMMARY,\n async () => OrderProvider.render(OrderCostSummary)(container),\n);\n\n/**\n * Renders the order product list component with image slots and gift options\n * @param {HTMLElement} container - The DOM element to render the order product list in\n * @returns {Promise<Object>} - The rendered order product list component\n */\nexport const renderOrderProductList = async (container) => renderContainer(\n CONTAINERS.ORDER_PRODUCT_LIST,\n async () => OrderProvider.render(OrderProductList, {\n slots: {\n Footer: (ctx) => {\n const giftOptions = document.createElement('div');\n CartProvider.render(GiftOptions, {\n item: ctx.item,\n view: 'product',\n dataSource: 'order',\n isEditable: false,\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(giftOptions);\n ctx.appendChild(giftOptions);\n },\n CartSummaryItemImage: (ctx) => {\n const { data, defaultImageProps } = ctx;\n tryRenderAemAssetsImage(ctx, {\n alias: data.product.sku,\n imageProps: defaultImageProps,\n params: {\n width: defaultImageProps.width,\n height: defaultImageProps.height,\n },\n });\n },\n },\n })(container),\n);\n\n/**\n * Renders order-level gift options for order confirmation\n * @param {HTMLElement} container - DOM element to render order gift options in\n * @returns {Promise<Object>} - The rendered order gift options component\n */\nexport const renderOrderGiftOptions = async (container) => renderContainer(\n CONTAINERS.ORDER_GIFT_OPTIONS,\n async () => CartProvider.render(GiftOptions, {\n view: 'order',\n dataSource: 'order',\n isEditable: false,\n readOnlyFormOrderView: 'secondary',\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(container),\n);\n\n/**\n * Renders the continue shopping button for order confirmation footer\n * @param {HTMLElement} container - DOM element to render the button in\n * @returns {Promise<Object>} - The rendered continue shopping button component\n */\nexport const renderOrderConfirmationFooterButton = async (container) => renderContainer(\n CONTAINERS.ORDER_CONFIRMATION_FOOTER_BUTTON,\n async () => UI.render(Button, {\n children: 'Continue shopping',\n 'data-testid': 'order-confirmation-footer__continue-button',\n className: 'order-confirmation-footer__continue-button',\n size: 'medium',\n variant: 'primary',\n type: 'submit',\n href: rootLink('/'),\n })(container),\n);\n",
|
|
310
|
+
"fragments.js": "// eslint-disable-next-line import/no-unresolved\nimport { createFragment } from '@dropins/storefront-checkout/lib/utils.js';\n\nimport {\n CHECKOUT_BLOCK,\n ORDER_CONFIRMATION_BLOCK,\n} from './constants.js';\n\n/**\n * A frozen, nested object of CSS selectors\n * @readonly\n */\nexport const selectors = Object.freeze({\n checkout: {\n content: '.checkout__content',\n loader: '.checkout__loader',\n mergedCartBanner: '.checkout__merged-cart-banner',\n heading: '.checkout__heading',\n serverError: '.checkout__server-error',\n outOfStock: '.checkout__out-of-stock',\n login: '.checkout__login',\n shippingForm: '.checkout__shipping-form',\n billToShipping: '.checkout__bill-to-shipping',\n delivery: '.checkout__delivery',\n paymentMethods: '.checkout__payment-methods',\n billingForm: '.checkout__billing-form',\n orderSummary: '.checkout__order-summary',\n cartSummary: '.checkout__cart-summary',\n placeOrder: '.checkout__place-order',\n giftOptions: '.checkout__gift-options',\n termsAndConditions: '.checkout__terms-and-conditions',\n main: '.checkout__main',\n aside: '.checkout__aside',\n },\n orderConfirmation: {\n header: '.order-confirmation__header',\n orderStatus: '.order-confirmation__order-status',\n shippingStatus: '.order-confirmation__shipping-status',\n customerDetails: '.order-confirmation__customer-details',\n orderCostSummary: '.order-confirmation__order-cost-summary',\n giftOptions: '.order-confirmation__gift-options',\n orderProductList: '.order-confirmation__order-product-list',\n footer: '.order-confirmation__footer',\n continueButton: '.order-confirmation-footer__continue-button',\n contactSupportLink: '.order-confirmation-footer__contact-support-link',\n },\n});\n\n// =============================================================================\n// CHECKOUT\n// =============================================================================\n\n/**\n * Creates the main checkout fragment with all checkout blocks.\n * @returns {DocumentFragment} The complete checkout fragment.\n */\nexport function createCheckoutFragment() {\n return createFragment(`\n <div class=\"checkout__wrapper\">\n <div class=\"checkout__loader\"></div>\n <div class=\"checkout__content\">\n <div class=\"checkout__merged-cart-banner\"></div>\n <div class=\"checkout__main\">\n <div class=\"checkout__heading ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__server-error ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__out-of-stock ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__login ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__shipping-form ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__bill-to-shipping ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__delivery ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__payment-methods ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__billing-form ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__terms-and-conditions ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__place-order ${CHECKOUT_BLOCK}\"></div>\n </div>\n <div class=\"checkout__aside\">\n <div class=\"checkout__order-summary ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__gift-options ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__cart-summary ${CHECKOUT_BLOCK}\"></div>\n </div>\n </div>\n </div>\n `);\n}\n\n// =============================================================================\n// ORDER CONFIRMATION\n// =============================================================================\n\n/**\n * Creates the order confirmation fragment.\n * @returns {DocumentFragment} The order confirmation fragment.\n */\nexport function createOrderConfirmationFragment() {\n return createFragment(`\n <div class=\"order-confirmation\">\n <div class=\"order-confirmation__main\">\n <div class=\"order-confirmation__header ${ORDER_CONFIRMATION_BLOCK}\"></div>\n <div class=\"order-confirmation__order-status ${ORDER_CONFIRMATION_BLOCK}\"></div>\n <div class=\"order-confirmation__shipping-status ${ORDER_CONFIRMATION_BLOCK}\"></div>\n <div class=\"order-confirmation__customer-details ${ORDER_CONFIRMATION_BLOCK}\"></div>\n </div>\n <div class=\"order-confirmation__aside\">\n <div class=\"order-confirmation__order-cost-summary ${ORDER_CONFIRMATION_BLOCK}\"></div>\n <div class=\"order-confirmation__gift-options ${ORDER_CONFIRMATION_BLOCK}\"></div>\n <div class=\"order-confirmation__order-product-list ${ORDER_CONFIRMATION_BLOCK}\"></div>\n <div class=\"order-confirmation__footer ${ORDER_CONFIRMATION_BLOCK}\"></div>\n </div>\n </div>\n `);\n}\n\n/**\n * Creates the order confirmation footer content with support link.\n * @param {string} supportPath - The support page path for the contact link\n * @returns {string} The footer HTML content\n */\nexport function createOrderConfirmationFooter(supportPath) {\n return `\n <div class=\"order-confirmation-footer__continue-button\"></div>\n <div class=\"order-confirmation-footer__contact-support\">\n <p>\n Need help?\n <a\n href=\"${supportPath}\"\n rel=\"noreferrer\"\n class=\"order-confirmation-footer__contact-support-link\"\n data-testid=\"order-confirmation-footer__contact-support-link\"\n >\n Contact us\n </a>\n </p>\n </div>\n `;\n}\n",
|
|
311
|
+
"utils.js": "/* eslint-disable import/no-unresolved */\nimport { ProgressSpinner, provider as UI } from '@dropins/tools/components.js';\nimport { tryRenderAemAssetsImage } from '@dropins/tools/lib/aem/assets.js';\nimport { ORDER_DETAILS_PATH, rootLink } from '../../scripts/commerce.js';\nimport { getUserTokenCookie } from '../../scripts/initializers/index.js';\nimport createModal from '../modal/modal.js';\n\n/**\n * Displays an overlay spinner in the specified container\n * @param {Object} loaderRef - Ref object to store the spinner component\n * @param {HTMLElement} $loader - DOM element to render the spinner in\n */\nexport const displayOverlaySpinner = async (loaderRef, $loader) => {\n if (loaderRef.current) return;\n\n loaderRef.current = await UI.render(ProgressSpinner, {\n className: '.checkout__overlay-spinner',\n })($loader);\n};\n\n/**\n * Removes the overlay spinner and cleans up references\n * @param {Object} loaderRef - Ref object containing the spinner component\n * @param {HTMLElement} $loader - DOM element containing the spinner\n */\nexport const removeOverlaySpinner = (loaderRef, $loader) => {\n if (!loaderRef.current) return;\n\n loaderRef.current.remove();\n loaderRef.current = null;\n $loader.innerHTML = '';\n};\n\n// Modal state management\nlet modal;\n\n/**\n * Shows a modal with the specified content\n * @param {HTMLElement} content - DOM element to display in the modal\n */\nexport const showModal = async (content) => {\n modal = await createModal([content]);\n modal.showModal();\n};\n\n/**\n * Removes the currently displayed modal and cleans up references\n */\nexport const removeModal = () => {\n if (!modal) return;\n modal.removeModal();\n modal = null;\n};\n\n/**\n * Renders AEM asset images for gift option swatches\n * @param {Object} ctx - The context object containing imageSwatchContext and defaultImageProps\n */\nexport function swatchImageSlot(ctx) {\n const { imageSwatchContext, defaultImageProps } = ctx;\n tryRenderAemAssetsImage(ctx, {\n alias: imageSwatchContext.label,\n imageProps: defaultImageProps,\n wrapper: document.createElement('span'),\n params: {\n width: defaultImageProps.width,\n height: defaultImageProps.height,\n },\n });\n}\n\n/**\n * Builds the order details URL based on authentication status\n * @param {Object} orderData - Order data containing number and token\n * @param {string} orderDetailsPath - Path to the order details page\n * @returns {string} The constructed order details URL\n */\nexport function buildOrderDetailsUrl(orderData, orderDetailsPath = ORDER_DETAILS_PATH) {\n const token = getUserTokenCookie();\n const orderRef = token ? orderData.number : orderData.token;\n const orderNumber = orderData.number;\n const encodedOrderRef = encodeURIComponent(orderRef);\n const encodedOrderNumber = encodeURIComponent(orderNumber);\n\n return token\n ? rootLink(`${orderDetailsPath}?orderRef=${encodedOrderRef}`)\n : rootLink(`${orderDetailsPath}?orderRef=${encodedOrderRef}&orderNumber=${encodedOrderNumber}`);\n}\n"
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
"name": "commerce-checkout-bopis",
|
|
316
|
+
"description": "Example commerce-checkout-bopis block for storefront-checkout",
|
|
317
|
+
"files": {
|
|
318
|
+
"commerce-checkout-bopis.js": "/* eslint-disable import/no-unresolved */\n/* eslint-disable no-unused-vars */\n\n// Dropin Tools\nimport { events } from '@dropins/tools/event-bus.js';\nimport { initReCaptcha } from '@dropins/tools/recaptcha.js';\n\n// Dropin Components for BOPIS functionality\nimport {\n RadioButton,\n ToggleButton,\n provider as UI,\n} from '@dropins/tools/components.js';\n\n// Checkout Dropin\nimport * as checkoutApi from '@dropins/storefront-checkout/api.js';\n\n// Order Dropin Modules\nimport * as orderApi from '@dropins/storefront-order/api.js';\n\n// Payment Services Dropin\nimport { PaymentMethodCode } from '@dropins/storefront-payment-services/api.js';\nimport {\n createScopedSelector,\n isVirtualCart,\n setMetaTags,\n validateForms,\n} from '@dropins/storefront-checkout/lib/utils.js';\n\n// Checkout Dropin Libraries\n\n// Local utils\nimport {\n buildOrderDetailsUrl,\n displayOverlaySpinner,\n removeOverlaySpinner,\n} from './utils.js';\n\n// Constants\nimport {\n BILLING_ADDRESS_DATA_KEY,\n BILLING_FORM_NAME,\n LOGIN_FORM_NAME,\n PURCHASE_ORDER_FORM_NAME,\n SHIPPING_ADDRESS_DATA_KEY,\n SHIPPING_FORM_NAME,\n TERMS_AND_CONDITIONS_FORM_NAME,\n} from './constants.js';\n\n// Fragment functions\nimport {\n createCheckoutFragment,\n selectors,\n} from './fragments.js';\n\n// Container functions\nimport {\n renderAddressForm,\n renderBillingAddressFormSkeleton,\n renderBillToShippingAddress,\n renderCartSummaryList,\n renderCheckoutHeader,\n renderCustomerBillingAddresses,\n renderCustomerShippingAddresses,\n renderGiftOptions,\n renderLoginForm,\n renderMergedCartBanner,\n renderOrderSummary,\n renderOutOfStock,\n renderPaymentMethods,\n renderPlaceOrder,\n renderServerError,\n renderShippingAddressFormSkeleton,\n renderShippingMethods,\n renderTermsAndConditions,\n} from './containers.js';\n\nimport { rootLink } from '../../scripts/commerce.js';\n\n// Initializers\nimport '../../scripts/initializers/account.js';\nimport '../../scripts/initializers/checkout.js';\nimport '../../scripts/initializers/order.js';\n\n// Checkout success block import and CSS preload\nimport {\n preloadCheckoutSuccess,\n renderCheckoutSuccess,\n} from '../commerce-checkout-success/commerce-checkout-success.js';\n\npreloadCheckoutSuccess();\n\nfunction redirectToCartIfEmpty(cartData) {\n const isOrderPlaced = events.lastPayload('order/placed') !== undefined;\n\n if (!isOrderPlaced && (cartData === null || cartData?.items?.length === 0)) {\n window.location.href = rootLink('/cart');\n }\n}\n\n// 4. Fetching Pickup Locations\nasync function fetchPickupLocations() {\n return checkoutApi\n .fetchGraphQl(\n `query pickupLocations {\n pickupLocations {\n items {\n name\n pickup_location_code\n }\n total_count\n }\n }`,\n { method: 'GET', cache: 'no-cache' },\n )\n .then((res) => res.data.pickupLocations.items);\n}\n\nexport default async function decorate(block) {\n setMetaTags('Checkout');\n document.title = 'Checkout';\n\n const cartData = events.lastPayload('cart/initialized');\n redirectToCartIfEmpty(cartData);\n\n events.on('order/placed', () => {\n setMetaTags('Order Confirmation');\n document.title = 'Order Confirmation';\n });\n\n // Create the checkout layout using fragments - BOPIS-specific with delivery method selection\n const checkoutFragment = createCheckoutFragment();\n\n // Create scoped selector for the checkout fragment\n const getElement = createScopedSelector(checkoutFragment);\n\n // Add BOPIS-specific delivery method blocks to checkout__main\n const deliveryMethodHTML = `\n <div class=\"checkout__block checkout__delivery-method\">\n <h2 class=\"checkout__block checkout-delivery-method__title\">Delivery Method</h2>\n <div class=\"checkout__block checkout-delivery-method__toggle-buttons\">\n <div class=\"checkout__block checkout-delivery-method__delivery-button\"></div>\n <div class=\"checkout__block checkout-delivery-method__in-store-pickup-button\"></div>\n </div>\n </div>\n <div class=\"checkout__block checkout__in-store-pickup\"></div>\n `;\n\n // Insert delivery method after login block\n const $login = getElement(selectors.checkout.login);\n $login.insertAdjacentHTML('afterend', deliveryMethodHTML);\n\n // Get all checkout elements using centralized selectors\n const $content = getElement(selectors.checkout.content);\n const $loader = getElement(selectors.checkout.loader);\n const $mergedCartBanner = getElement(selectors.checkout.mergedCartBanner);\n const $heading = getElement(selectors.checkout.heading);\n const $serverError = getElement(selectors.checkout.serverError);\n const $outOfStock = getElement(selectors.checkout.outOfStock);\n const $shippingForm = getElement(selectors.checkout.shippingForm);\n const $billToShipping = getElement(selectors.checkout.billToShipping);\n const $delivery = getElement(selectors.checkout.delivery);\n const $paymentMethods = getElement(selectors.checkout.paymentMethods);\n const $billingForm = getElement(selectors.checkout.billingForm);\n const $orderSummary = getElement(selectors.checkout.orderSummary);\n const $cartSummary = getElement(selectors.checkout.cartSummary);\n const $placeOrder = getElement(selectors.checkout.placeOrder);\n const $giftOptions = getElement(selectors.checkout.giftOptions);\n const $termsAndConditions = getElement(selectors.checkout.termsAndConditions);\n\n // BOPIS-specific elements\n const $deliveryButton = getElement('.checkout-delivery-method__delivery-button');\n const $inStorePickupButton = getElement('.checkout-delivery-method__in-store-pickup-button');\n const $inStorePickup = getElement('.checkout__in-store-pickup');\n\n block.appendChild(checkoutFragment);\n\n // Container and component references\n let shippingForm;\n let billingForm;\n let shippingAddresses;\n let billingAddresses;\n\n const shippingFormRef = { current: null };\n const billingFormRef = { current: null };\n const creditCardFormRef = { current: null };\n const loaderRef = { current: null };\n\n // Create validation and place order handlers\n const handleValidation = () => validateForms([\n { name: LOGIN_FORM_NAME },\n { name: SHIPPING_FORM_NAME, ref: shippingFormRef },\n { name: BILLING_FORM_NAME, ref: billingFormRef },\n { name: PURCHASE_ORDER_FORM_NAME },\n { name: TERMS_AND_CONDITIONS_FORM_NAME },\n ]);\n\n const handlePlaceOrder = async ({ cartId, code }) => {\n await displayOverlaySpinner(loaderRef, $loader);\n try {\n // Payment Services credit card\n if (code === PaymentMethodCode.CREDIT_CARD) {\n if (!creditCardFormRef.current) {\n console.error('Credit card form not rendered.');\n return;\n }\n if (!creditCardFormRef.current.validate()) {\n // Credit card form invalid; abort order placement\n return;\n }\n // Submit Payment Services credit card form\n await creditCardFormRef.current.submit();\n }\n // Place order\n await orderApi.placeOrder(cartId);\n } catch (error) {\n console.error(error);\n throw error;\n } finally {\n removeOverlaySpinner(loaderRef, $loader);\n }\n };\n\n // First, render the place order component\n await renderPlaceOrder($placeOrder, { handleValidation, handlePlaceOrder });\n\n // Render the initial containers\n const [\n _mergedCartBanner,\n _header,\n _serverError,\n _outOfStock,\n _loginForm,\n // 2. UI Components for Delivery and In-Store Pickup\n deliveryButton,\n inStorePickupButton,\n shippingFormSkeleton,\n _billToShipping,\n _shippingMethods,\n _paymentMethods,\n billingFormSkeleton,\n _orderSummary,\n _cartSummary,\n _termsAndConditions,\n _giftOptions,\n ] = await Promise.all([\n renderMergedCartBanner($mergedCartBanner),\n\n renderCheckoutHeader($heading, 'BOPIS Checkout'),\n\n renderServerError($serverError, $content),\n\n renderOutOfStock($outOfStock),\n\n renderLoginForm($login),\n\n // 2. UI Components for Delivery and In-Store Pickup\n UI.render(ToggleButton, {\n label: 'Delivery',\n onChange: () => onToggle('delivery'),\n })($deliveryButton),\n\n UI.render(ToggleButton, {\n label: 'In-store Pickup',\n onChange: () => onToggle('in-store-pickup'),\n })($inStorePickupButton),\n\n renderShippingAddressFormSkeleton($shippingForm),\n\n renderBillToShippingAddress($billToShipping),\n\n renderShippingMethods($delivery),\n\n renderPaymentMethods($paymentMethods, creditCardFormRef),\n\n renderBillingAddressFormSkeleton($billingForm),\n\n renderOrderSummary($orderSummary),\n\n renderCartSummaryList($cartSummary),\n\n renderTermsAndConditions($termsAndConditions),\n\n renderGiftOptions($giftOptions),\n ]);\n\n // 3. Toggle Between Delivery and In-Store Pickup\n async function onToggle(type) {\n if (type === 'delivery') {\n deliveryButton.setProps((prev) => ({ ...prev, selected: true }));\n inStorePickupButton.setProps((prev) => ({ ...prev, selected: false }));\n $shippingForm.removeAttribute('hidden');\n $delivery.removeAttribute('hidden');\n $inStorePickup.setAttribute('hidden', '');\n } else {\n inStorePickupButton.setProps((prev) => ({ ...prev, selected: true }));\n deliveryButton.setProps((prev) => ({ ...prev, selected: false }));\n $shippingForm.setAttribute('hidden', '');\n $delivery.setAttribute('hidden', '');\n $inStorePickup.removeAttribute('hidden');\n }\n }\n\n onToggle('delivery');\n\n // 5. Rendering the Pickup Location Options\n const pickupLocations = await fetchPickupLocations();\n\n pickupLocations.forEach((location) => {\n const { name, pickup_location_code: pickupLocationCode } = location;\n const locationRadiobutton = document.createElement('div');\n\n UI.render(RadioButton, {\n label: name,\n name: 'pickup-location',\n value: name,\n onChange: () => {\n checkoutApi.setShippingAddress({\n address: {},\n pickupLocationCode,\n });\n },\n })(locationRadiobutton);\n\n $inStorePickup.appendChild(locationRadiobutton);\n });\n\n async function initializeCheckout(data) {\n await initReCaptcha(0);\n if (data.isGuest) await displayGuestAddressForms(data);\n else {\n removeOverlaySpinner(loaderRef, $loader);\n await displayCustomerAddressForms(data);\n }\n }\n\n async function displayGuestAddressForms(data) {\n if (isVirtualCart(data)) {\n shippingForm?.remove();\n shippingForm = null;\n $shippingForm.innerHTML = '';\n } else if (!shippingForm) {\n shippingFormSkeleton.remove();\n\n shippingForm = await renderAddressForm($shippingForm, shippingFormRef, data, 'shipping');\n }\n\n if (!billingForm) {\n billingFormSkeleton.remove();\n\n billingForm = await renderAddressForm($billingForm, billingFormRef, data, 'billing');\n }\n }\n\n async function displayCustomerAddressForms(data) {\n if (isVirtualCart(data)) {\n shippingAddresses?.remove();\n shippingAddresses = null;\n $shippingForm.innerHTML = '';\n } else if (!shippingAddresses) {\n shippingForm?.remove();\n shippingForm = null;\n shippingFormRef.current = null;\n\n shippingAddresses = await renderCustomerShippingAddresses(\n $shippingForm,\n shippingFormRef,\n data,\n );\n }\n\n if (!billingAddresses) {\n billingForm?.remove();\n billingForm = null;\n billingFormRef.current = null;\n\n billingAddresses = await renderCustomerBillingAddresses(\n $billingForm,\n billingFormRef,\n data,\n );\n }\n }\n\n async function handleCheckoutUpdated(data) {\n if (!data) return;\n await initializeCheckout(data);\n }\n\n function handleAuthenticated(authenticated) {\n if (!authenticated) return;\n\n // When a customer creates an account on the checkout success page and then\n // signs in, they will be redirected to the order details page with the order\n // number as orderRef, allowing the order details to be displayed\n const orderData = events.lastPayload('order/placed');\n if (orderData) {\n const url = buildOrderDetailsUrl(orderData);\n window.history.pushState({}, '', url);\n }\n\n window.location.reload();\n }\n\n function handleCheckoutValues(payload) {\n const { isBillToShipping } = payload;\n $billingForm.style.display = isBillToShipping ? 'none' : 'block';\n }\n\n async function handleOrderPlaced(orderData) {\n // Clear address form data\n sessionStorage.removeItem(SHIPPING_ADDRESS_DATA_KEY);\n sessionStorage.removeItem(BILLING_ADDRESS_DATA_KEY);\n\n const url = buildOrderDetailsUrl(orderData);\n\n window.history.pushState({}, '', url);\n\n await renderCheckoutSuccess(block, { orderData });\n }\n\n events.on('authenticated', handleAuthenticated);\n events.on('checkout/initialized', handleCheckoutUpdated, { eager: true });\n events.on('checkout/updated', handleCheckoutUpdated);\n events.on('checkout/values', handleCheckoutValues);\n events.on('order/placed', handleOrderPlaced);\n events.on('cart/initialized', redirectToCartIfEmpty, { eager: true });\n events.on('cart/data', redirectToCartIfEmpty);\n}\n",
|
|
319
|
+
"commerce-checkout-bopis.css": ".checkout__content {\n display: grid;\n grid-template-columns: 1fr;\n gap: var(--spacing-big) 0;\n}\n\n.checkout__merged-cart-banner {\n display: grid;\n grid-column: 1 / -1;\n align-items: start;\n grid-template-columns: auto;\n}\n\n.checkout__main {\n display: grid;\n row-gap: var(--spacing-xbig);\n margin-top: var(--spacing-medium);\n}\n\n.checkout__aside {\n display: grid;\n gap: var(--spacing-xbig);\n}\n\n.checkout-header h1 {\n margin: 0;\n}\n\n/* Block dividers */\n.checkout__block.checkout__heading .dropin-header-container {\n gap: var(--spacing-xsmall);\n}\n\n.checkout__shipping-form {\n padding-top: var(--spacing-xbig);\n border-top: var(--shape-border-width-3) solid var(--color-neutral-400);\n}\n\n.checkout__payment-methods {\n padding-top: var(--spacing-xbig);\n border-top: var(--shape-border-width-3) solid var(--color-neutral-400);\n padding-bottom: var(--spacing-xbig);\n border-bottom: var(--shape-border-width-3) solid var(--color-neutral-400);\n}\n\n/* Hide empty blocks */\n.checkout__block:empty {\n display: none;\n}\n\n/* Hide main containers when there is a server error */\n.checkout__content--error .checkout__merged-cart-banner,\n.checkout__content--error .checkout__out-of-stock,\n.checkout__content--error .checkout__login,\n.checkout__content--error .checkout__delivery-method,\n.checkout__content--error .checkout__shipping-form,\n.checkout__content--error .checkout__bill-to-shipping,\n.checkout__content--error .checkout__delivery,\n.checkout__content--error .checkout__payment-methods,\n.checkout__content--error .checkout__billing-form,\n.checkout__content--error .checkout__terms-and-conditions {\n display: none !important;\n}\n\n/* Hide blocks with empty divs */\n.checkout__server-error:has(> :empty),\n.checkout__out-of-stock:has(> :empty),\n.checkout__merged-cart-banner:has(> :empty),\n.checkout__delivery:has(> div:first-child:empty),\n.checkout__bill-to-shipping:has(> :empty),\n.checkout__gift-options:has(.cart-gift-options-view--readonly:empty) {\n display: none;\n}\n\n/* Hide aside containers when there is a server error */\n.checkout__content--error .checkout__aside {\n display: none;\n}\n\n/* Integrate place order button into Order Summary - mobile */\n.checkout__place-order {\n grid-column: unset;\n justify-items: unset;\n margin-top: calc(var(--spacing-big) * -1);\n}\n\n/* Hide the place order button when there is a server error */\n.checkout__content--error .checkout__place-order {\n display: none;\n}\n\n.checkout__loader {\n align-items: center;\n background: var(--color-neutral-50);\n display: flex;\n height: 100vh;\n justify-content: center;\n left: 0;\n opacity: 0.5;\n position: fixed;\n top: 0;\n width: 100%;\n z-index: 9999;\n}\n\n.checkout__loader:empty {\n display: none;\n}\n\n/* remove margin from the heading divider */\n.checkout__heading .dropin-divider {\n margin: 0;\n}\n\n/* Cart Summary */\n.checkout__block .cart-cart-summary-list {\n padding: var(--spacing-medium);\n}\n\n/* Order Summary Coupon */\n.dropin-accordion-section__heading {\n margin: var(--spacing-medium) auto;\n}\n\n.cart-coupons__accordion {\n margin-top: var(--spacing-xsmall);\n}\n\n/* temporary fix to hide the default cart heading */\n[data-testid=\"default-cart-heading\"] {\n display: none;\n}\n\n.cart-summary-list__heading {\n display: flex;\n justify-content: space-between;\n align-items: flex-end;\n}\n\n.cart-summary-list__heading-text {\n font: var(--type-headline-2-strong-font);\n letter-spacing: var(--type-headline-2-strong-letter-spacing);\n color: var(--color-neutral-800);\n}\n\n.cart-cart-summary-list__heading {\n row-gap: var(--spacing-small);\n padding-top: 0;\n}\n\n.cart-cart-summary-list__heading-text {\n font: var(--type-headline-2-strong-font);\n letter-spacing: var(--type-headline-2-strong-letter-spacing);\n color: var(--color-neutral-800);\n}\n\n.cart-summary-list__edit {\n font: var(--type-body-2-strong-font);\n letter-spacing: var(--type-body-2-strong-letter-spacing);\n}\n\n.checkout__block\n.cart-cart-summary-list\n.cart-cart-summary-list__footer-divider {\n margin: var(--spacing-small) 0;\n}\n\n/* Sign-in modal */\n#modal {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-color: rgb(0 0 0 / 50%);\n display: flex;\n justify-content: center;\n align-items: center;\n z-index: 2;\n}\n\n#modal-form {\n width: 800px;\n}\n\n/* Address form */\n.checkout__shipping-form .account-address-form-wrapper__title,\n.checkout__shipping-form .dropin-header-container__title,\n.checkout__billing-form .account-address-form-wrapper__title,\n.checkout__billing-form .dropin-header-container__title {\n font: var(--type-headline-2-default-font);\n letter-spacing: var(--type-headline-2-default-letter-spacing);\n color: var(--color-neutral-800);\n margin: 0 0 var(--spacing-medium) 0;\n}\n\n.checkout__shipping-form .dropin-header-container .dropin-divider,\n.checkout__billing-form .dropin-header-container .dropin-divider {\n display: none;\n}\n\n/* BOPIS specific styles */\n.checkout__in-store-pickup {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-small);\n}\n\n.checkout__in-store-pickup[hidden] {\n display: none;\n}\n\n.checkout-delivery-method__title {\n color: var(--color-neutral-800);\n font: var(--type-headline-2-strong-font);\n letter-spacing: var(--type-headline-2-strong-letter-spacing);\n margin: 0 0 var(--spacing-medium) 0;\n}\n\n.checkout-delivery-method__toggle-buttons {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: var(--spacing-big);\n}\n\n/* Order confirmation */\n.order-confirmation {\n display: grid;\n align-items: start;\n grid-template-columns: repeat(var(--grid-4-columns), 1fr);\n grid-template-areas: \"main aside\";\n grid-column-gap: var(--grid-4-gutters);\n margin-bottom: var(--spacing-xbig);\n padding-top: var(--spacing-xxlarge);\n}\n\n.order-confirmation__main {\n display: grid;\n grid-row-gap: var(--spacing-xbig);\n grid-column: 1 / span 7;\n}\n\n.order-confirmation__aside {\n display: grid;\n grid-row-gap: var(--spacing-xbig);\n grid-column: 9 / span 4;\n}\n\n.order-confirmation__footer {\n display: grid;\n gap: var(--spacing-small);\n text-align: center;\n}\n\n.order-confirmation__footer p {\n margin: 0;\n}\n\n.order-confirmation__footer .order-confirmation-footer__continue-button {\n margin: 0 auto;\n text-align: center;\n display: inline-block;\n}\n\n.order-confirmation-footer__contact-support {\n font: var(--type-body-2-default-font);\n letter-spacing: var(--type-body-2-default-letter-spacing);\n color: var(--color-neutral-700);\n}\n\n.order-confirmation-footer__contact-support a {\n font: var(--type-body-2-strong-font);\n letter-spacing: var(--type-body-2-strong-letter-spacing);\n color: var(--color-brand-500);\n cursor: pointer;\n}\n\n/* Hide empty blocks */\n.order-confirmation__block:empty {\n display: none;\n}\n\n@media only screen and (min-width: 320px) and (max-width: 768px) {\n .checkout__main,\n .checkout__aside {\n display: contents;\n }\n\n .checkout__block {\n order: 3;\n }\n\n .checkout__heading {\n order: 1;\n }\n\n .checkout__cart-summary {\n order: 2;\n }\n\n .checkout__place-order {\n order: 4;\n }\n\n .order-confirmation {\n grid-template-columns: repeat(var(--grid-1-columns), 1fr);\n padding-top: 0;\n }\n\n .order-confirmation__main,\n .order-confirmation__aside {\n grid-row-gap: var(--spacing-medium);\n }\n\n .order-confirmation > div {\n grid-column: 1 / span 4;\n }\n\n .order-confirmation__block .dropin-card {\n border: 0;\n }\n}\n\n@media only screen and (min-width: 768px) {\n .checkout__content {\n display: grid;\n align-items: start;\n grid-template-columns: repeat(var(--grid-4-columns), 1fr);\n gap: var(--spacing-big) var(--grid-4-gutters);\n }\n\n .checkout__content--error {\n display: grid;\n grid-template-columns: 1fr;\n }\n\n .checkout__main {\n grid-column: 1 / span 7;\n row-gap: var(--spacing-xbig);\n }\n\n .checkout__aside {\n grid-column: 9 / span 4;\n gap: var(--spacing-xbig);\n }\n\n .checkout__place-order {\n margin-top: 0;\n }\n}\n",
|
|
320
|
+
"README.md": "# Adding Buy Online, Pickup In-Store (BOPIS) to Checkout\n\nThis guide will walk you through the steps to extend the Checkout to support the Buy Online, Pickup In-Store (BOPIS) functionality.\n\n## Hands on\n\nWe'll use the **commerce-checkout** block as our starting point and iteratively update it to meet the new product requirements.\n\n> [!NOTE]\n> Please note the _**commerce-checkout.js**_ block is the only one that is fully functional up-to-date with the latest Drop-ins versions.\n> Use the following guidelines just as a reference when creating a new checkout experience.\n\n## Step-by-Step Process:\n\n### 1. Update Content Fragment\n\nFor the new section, we need to add additional DOM elements, which can be done by modifying the contextual fragment.\n\n```html\n<div class=\"checkout__block checkout__delivery-method\">\n <h2 class=\"checkout__block checkout-delivery-method__title\">Delivery Method</h2>\n <div class=\"checkout__block checkout-delivery-method__toggle-buttons\">\n <div class=\"checkout__block checkout-delivery-method__delivery-button\"></div>\n <div class=\"checkout__block checkout-delivery-method__in-store-pickup-button\"></div>\n </div>\n</div>\n<div class=\"checkout__block checkout__in-store-pickup\"></div>\n```\n\nWe will also need to add new selectors, allowing us to use them later to render the required components and content.\n\n```javascript\nconst $deliveryButton = checkoutFragment.querySelector('.checkout-delivery-method__delivery-button');\nconst $inStorePickupButton = checkoutFragment.querySelector('.checkout-delivery-method__in-store-pickup-button');\nconst $inStorePickup = checkoutFragment.querySelector('.checkout__in-store-pickup');\n```\n\n### 2. UI Components for Delivery and In-Store Pickup\n\nDuring the initialization, two buttons are rendered: one for Delivery and one for In-Store Pickup. These buttons allow users to toggle between the two options.\n\n```javascript\nUI.render(ToggleButton, {\n label: 'Delivery',\n onChange: () => onToggle('delivery'),\n})($deliveryButton),\n\nUI.render(ToggleButton, {\n label: 'In-store Pickup',\n onChange: () => onToggle('in-store-pickup'),\n})($inStorePickupButton),\n```\n\n### 3. Toggle Between Delivery and In-Store Pickup\n\nThe `onToggle` function manages switching between delivery and in-store pickup. It updates the selected state of the buttons and toggles the visibility of the corresponding forms.\n\n```javascript\nasync function onToggle(type) {\n if (type === 'delivery') {\n deliveryButton.setProps((prev) => ({ ...prev, selected: true }));\n inStorePickupButton.setProps((prev) => ({ ...prev, selected: false }));\n $shippingForm.removeAttribute('hidden');\n $delivery.removeAttribute('hidden');\n $inStorePickup.setAttribute('hidden', '');\n } else {\n inStorePickupButton.setProps((prev) => ({ ...prev, selected: true }));\n deliveryButton.setProps((prev) => ({ ...prev, selected: false }));\n $shippingForm.setAttribute('hidden', '');\n $delivery.setAttribute('hidden', '');\n $inStorePickup.removeAttribute('hidden');\n }\n}\n```\n\n### 4. Fetching Pickup Locations\n\nThe function `fetchPickupLocations` retrieves the list of available pickup locations using a GraphQL query. These locations will be displayed for the user to choose where they'd like to pick up their order.\n\n```javascript\nasync function fetchPickupLocations() {\n return checkoutApi\n .fetchGraphQl(\n `query pickupLocations {\n pickupLocations {\n items {\n name\n pickup_location_code\n }\n total_count\n }\n }`,\n { method: 'GET', cache: 'no-cache' },\n )\n .then((res) => res.data.pickupLocations.items);\n}\n```\n\n### 5. Rendering the Pickup Location Options\n\nOnce the pickup locations are fetched, they are rendered as radio buttons. The user can select a location, which updates the shipping address with the corresponding pickup location code.\n\n```javascript\nconst pickupLocations = await fetchPickupLocations();\n\npickupLocations.forEach((location) => {\n const { name, pickup_location_code } = location;\n const locationRadiobutton = document.createElement('div');\n\n UI.render(RadioButton, {\n label: name,\n name: 'pickup-location',\n value: name,\n onChange: () => {\n checkoutApi.setShippingAddress({\n address: {},\n pickupLocationCode: pickup_location_code,\n });\n },\n })(locationRadiobutton);\n\n $inStorePickup.appendChild(locationRadiobutton);\n});\n```\n\n### 6. Finalizing the BOPIS Flow\n\nOnce the user selects \"In-store Pickup\" and chooses a location, the form for pickup 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.\n",
|
|
321
|
+
"constants.js": "// Form and address constants\nconst BILLING_FORM_NAME = 'selectedBillingAddress';\nconst BILLING_ADDRESS_DATA_KEY = `${BILLING_FORM_NAME}_addressData`;\nconst LOGIN_FORM_NAME = 'login-form';\nconst PURCHASE_ORDER_FORM_NAME = 'purchase-order';\nconst SHIPPING_FORM_NAME = 'selectedShippingAddress';\nconst SHIPPING_ADDRESS_DATA_KEY = `${SHIPPING_FORM_NAME}_addressData`;\nconst TERMS_AND_CONDITIONS_FORM_NAME = 'checkout-terms-and-conditions__form';\n\n// Timing constants\nconst DEBOUNCE_TIME = 1000;\nconst ADDRESS_INPUT_DEBOUNCE_TIME = 500;\n\n// Block and styling constants\nconst CHECKOUT_BLOCK = 'checkout__block';\nconst CHECKOUT_ERROR_CLASS = 'checkout__content--error';\nconst CHECKOUT_HEADER_CLASS = 'checkout-header';\nconst ORDER_CONFIRMATION_BLOCK = 'order-confirmation__block';\n\n// Default values\nconst USER_TOKEN_COOKIE_NAME = 'auth_dropin_user_token';\n\nexport {\n // Form and address constants\n BILLING_ADDRESS_DATA_KEY,\n BILLING_FORM_NAME,\n LOGIN_FORM_NAME,\n PURCHASE_ORDER_FORM_NAME,\n SHIPPING_ADDRESS_DATA_KEY,\n SHIPPING_FORM_NAME,\n TERMS_AND_CONDITIONS_FORM_NAME,\n\n // Timing constants\n ADDRESS_INPUT_DEBOUNCE_TIME,\n DEBOUNCE_TIME,\n\n // Block and styling constants\n CHECKOUT_BLOCK,\n CHECKOUT_ERROR_CLASS,\n CHECKOUT_HEADER_CLASS,\n ORDER_CONFIRMATION_BLOCK,\n\n // Default values\n USER_TOKEN_COOKIE_NAME,\n};\n",
|
|
322
|
+
"containers.js": "/* eslint-disable max-len */\n/* eslint-disable import/no-unresolved */\n/* eslint-disable no-unused-vars */\n/* eslint-disable no-shadow */\n/* eslint-disable no-use-before-define */\n/* eslint-disable prefer-const */\n\n// Checkout Dropin\nimport * as checkoutApi from '@dropins/storefront-checkout/api.js';\nimport BillToShippingAddress from '@dropins/storefront-checkout/containers/BillToShippingAddress.js';\nimport EstimateShipping from '@dropins/storefront-checkout/containers/EstimateShipping.js';\nimport LoginForm from '@dropins/storefront-checkout/containers/LoginForm.js';\nimport MergedCartBanner from '@dropins/storefront-checkout/containers/MergedCartBanner.js';\nimport OutOfStock from '@dropins/storefront-checkout/containers/OutOfStock.js';\nimport PaymentMethods from '@dropins/storefront-checkout/containers/PaymentMethods.js';\nimport PlaceOrder from '@dropins/storefront-checkout/containers/PlaceOrder.js';\nimport ServerError from '@dropins/storefront-checkout/containers/ServerError.js';\nimport ShippingMethods from '@dropins/storefront-checkout/containers/ShippingMethods.js';\nimport TermsAndConditions from '@dropins/storefront-checkout/containers/TermsAndConditions.js';\nimport { render as CheckoutProvider } from '@dropins/storefront-checkout/render.js';\n\n// Auth Dropin\nimport * as authApi from '@dropins/storefront-auth/api.js';\nimport AuthCombine from '@dropins/storefront-auth/containers/AuthCombine.js';\nimport SignUp from '@dropins/storefront-auth/containers/SignUp.js';\nimport { render as AuthProvider } from '@dropins/storefront-auth/render.js';\n\n// Account Dropin\nimport Addresses from '@dropins/storefront-account/containers/Addresses.js';\nimport AddressForm from '@dropins/storefront-account/containers/AddressForm.js';\nimport { render as AccountProvider } from '@dropins/storefront-account/render.js';\n\n// Cart Dropin\nimport * as cartApi from '@dropins/storefront-cart/api.js';\nimport CartSummaryList from '@dropins/storefront-cart/containers/CartSummaryList.js';\nimport Coupons from '@dropins/storefront-cart/containers/Coupons.js';\nimport GiftCards from '@dropins/storefront-cart/containers/GiftCards.js';\nimport GiftOptions from '@dropins/storefront-cart/containers/GiftOptions.js';\nimport OrderSummary from '@dropins/storefront-cart/containers/OrderSummary.js';\nimport { render as CartProvider } from '@dropins/storefront-cart/render.js';\n\n// Payment Services Dropin\nimport { PaymentMethodCode } from '@dropins/storefront-payment-services/api.js';\nimport CreditCard from '@dropins/storefront-payment-services/containers/CreditCard.js';\nimport { render as PaymentServices } from '@dropins/storefront-payment-services/render.js';\n\n// Order Dropin\nimport CustomerDetails from '@dropins/storefront-order/containers/CustomerDetails.js';\nimport OrderCostSummary from '@dropins/storefront-order/containers/OrderCostSummary.js';\nimport OrderHeader from '@dropins/storefront-order/containers/OrderHeader.js';\nimport OrderProductList from '@dropins/storefront-order/containers/OrderProductList.js';\nimport OrderStatus from '@dropins/storefront-order/containers/OrderStatus.js';\nimport ShippingStatus from '@dropins/storefront-order/containers/ShippingStatus.js';\nimport { render as OrderProvider } from '@dropins/storefront-order/render.js';\n\n// Tools\nimport { debounce, getCookie } from '@dropins/tools/lib.js';\nimport { tryRenderAemAssetsImage } from '@dropins/tools/lib/aem/assets.js';\nimport { getConfigValue } from '@dropins/tools/lib/aem/configs.js';\nimport {\n Button,\n Header,\n provider as UI,\n} from '@dropins/tools/components.js';\nimport { events } from '@dropins/tools/event-bus.js';\n\n// Checkout Dropin Libraries\nimport {\n estimateShippingCost,\n getCartAddress,\n setAddressOnCart,\n} from '@dropins/storefront-checkout/lib/utils.js';\n\n// Utils\nimport {\n showModal,\n swatchImageSlot,\n} from './utils.js';\n\n// External dependencies\nimport {\n authPrivacyPolicyConsentSlot,\n fetchPlaceholders,\n rootLink,\n} from '../../scripts/commerce.js';\n\n// Constants\nimport {\n ADDRESS_INPUT_DEBOUNCE_TIME,\n BILLING_ADDRESS_DATA_KEY,\n BILLING_FORM_NAME,\n CHECKOUT_ERROR_CLASS,\n CHECKOUT_HEADER_CLASS,\n DEBOUNCE_TIME,\n LOGIN_FORM_NAME,\n SHIPPING_ADDRESS_DATA_KEY,\n SHIPPING_FORM_NAME,\n USER_TOKEN_COOKIE_NAME,\n} from './constants.js';\n\n/**\n * Container IDs for registry management\n * @enum {string}\n */\nexport const CONTAINERS = Object.freeze({\n // Static containers (rendered in Promise.all)\n MERGED_CART_BANNER: 'mergedCartBanner',\n CHECKOUT_HEADER: 'checkoutHeader',\n SERVER_ERROR: 'serverError',\n OUT_OF_STOCK: 'outOfStock',\n LOGIN_FORM: 'loginForm',\n SHIPPING_ADDRESS_FORM_SKELETON: 'shippingAddressFormSkeleton',\n BILL_TO_SHIPPING_ADDRESS: 'billToShippingAddress',\n SHIPPING_METHODS: 'shippingMethods',\n PAYMENT_METHODS: 'paymentMethods',\n BILLING_ADDRESS_FORM_SKELETON: 'billingAddressFormSkeleton',\n ORDER_SUMMARY: 'orderSummary',\n CART_SUMMARY_LIST: 'cartSummaryList',\n TERMS_AND_CONDITIONS: 'termsAndConditions',\n PLACE_ORDER_BUTTON: 'placeOrderButton',\n GIFT_OPTIONS: 'giftOptions',\n CUSTOMER_SHIPPING_ADDRESSES: 'customerShippingAddresses',\n CUSTOMER_BILLING_ADDRESSES: 'customerBillingAddresses',\n\n // Dynamic containers (conditional rendering)\n SHIPPING_ADDRESS_FORM: 'shippingAddressForm',\n BILLING_ADDRESS_FORM: 'billingAddressForm',\n\n // Order confirmation containers\n ORDER_HEADER: 'orderHeader',\n ORDER_STATUS: 'orderStatus',\n SHIPPING_STATUS: 'shippingStatus',\n CUSTOMER_DETAILS: 'customerDetails',\n ORDER_COST_SUMMARY: 'orderCostSummary',\n ORDER_GIFT_OPTIONS: 'orderGiftOptions',\n ORDER_PRODUCT_LIST: 'orderProductList',\n ORDER_CONFIRMATION_FOOTER_BUTTON: 'orderConfirmationFooterButton',\n\n // Slot/Sub-containers (nested within other containers)\n ESTIMATE_SHIPPING: 'estimateShipping',\n CART_COUPONS: 'cartCoupons',\n GIFT_CARDS: 'giftCards',\n CART_GIFT_OPTIONS: 'cartGiftOptions',\n});\n\n/**\n * A Map to store the API of rendered containers.\n * The key is a unique string ID, and the value is the containers's API object.\n * (e.g., { setProps: (props) => {...}, remove: () => {...} })\n */\nconst registry = new Map();\n\n/**\n * Checks if a container with the given ID has been rendered.\n * This is used to prevent multiple instances of the same container from being rendered.\n * @param {string} id - The unique ID of the container to check.\n * @returns {boolean} - Returns true if the container has been rendered, false otherwise.\n */\nexport const hasContainer = (id) => registry.has(id);\n\n/**\n * Helper to get a container from the registry or render and register it if not present.\n * @async\n * @param {string} id - Unique identifier for the container.\n * @param {Function} renderFn - Async function that renders the container.\n * @returns {Promise<Object>} - The rendered container API.\n */\nconst renderContainer = async (id, renderFn) => {\n if (registry.has(id)) {\n return registry.get(id);\n }\n\n try {\n const container = await renderFn();\n registry.set(id, container);\n return container;\n } catch (error) {\n console.error(`Error rendering container ${id}:`, error);\n throw error;\n }\n};\n\n/**\n * Unmounts and removes a container from the registry.\n * This function checks if the container is registered, removes it from the DOM,\n * and deletes its reference from the registry.\n * @param {string} id - The unique ID of the container to unmount.\n * @return {void}\n */\nexport const unmountContainer = (id) => {\n if (!registry.has(id)) {\n return;\n }\n\n const containerApi = registry.get(id);\n containerApi.remove();\n registry.delete(id);\n};\n\n/**\n * Renders the merged cart banner notification for authenticated users\n * @param {HTMLElement} container - DOM element to render the banner in\n * @returns {Promise<Object>} - The rendered merged cart banner component\n */\nexport const renderMergedCartBanner = async (container) => renderContainer(\n CONTAINERS.MERGED_CART_BANNER,\n async () => CheckoutProvider.render(MergedCartBanner)(container),\n);\n\n/**\n * Renders the checkout page header with title and styling\n * @param {HTMLElement} container - DOM element to render the header in\n * @param {string} title - The title to display in the header\n * @returns {Promise<Object>} - The rendered checkout header component\n */\nexport const renderCheckoutHeader = async (container, title) => renderContainer(\n CONTAINERS.CHECKOUT_HEADER,\n async () => UI.render(Header, {\n className: CHECKOUT_HEADER_CLASS,\n divider: true,\n level: 1,\n size: 'large',\n title,\n })(container),\n);\n\n/**\n * Renders server error handling with retry functionality and error state management\n * @param {HTMLElement} container - DOM element to render the error component in\n * @param {HTMLElement} contentElement - Main content element to add error styling to\n * @returns {Promise<Object>} - The rendered server error component\n */\nexport const renderServerError = async (container, contentElement) => renderContainer(\n CONTAINERS.SERVER_ERROR,\n async () => CheckoutProvider.render(ServerError, {\n autoScroll: true,\n onRetry: () => {\n contentElement.classList.remove(CHECKOUT_ERROR_CLASS);\n },\n onServerError: () => {\n contentElement.classList.add(CHECKOUT_ERROR_CLASS);\n },\n })(container),\n);\n\n/**\n * Renders out of stock handling with cart navigation and product update options\n * @param {HTMLElement} container - DOM element to render the component in\n * @returns {Promise<Object>} - The rendered out-of-stock component\n */\nexport const renderOutOfStock = async (container) => renderContainer(\n CONTAINERS.OUT_OF_STOCK,\n async () => CheckoutProvider.render(OutOfStock, {\n routeCart: () => rootLink('/cart'),\n onCartProductsUpdate: (items) => {\n cartApi.updateProductsFromCart(items).catch(console.error);\n },\n })(container),\n);\n\n/**\n * Renders the login form for guest checkout with authentication options\n * Uses the existing 'authenticated' event system for decoupled communication\n * @param {HTMLElement} container - DOM element to render the login form in\n * @returns {Promise<Object>} - The rendered login form component\n */\nexport const renderLoginForm = async (container) => renderContainer(\n CONTAINERS.LOGIN_FORM,\n async () => CheckoutProvider.render(LoginForm, {\n name: LOGIN_FORM_NAME,\n onSignInClick: async (initialEmailValue) => {\n const signInForm = document.createElement('div');\n\n AuthProvider.render(AuthCombine, {\n signInFormConfig: {\n renderSignUpLink: true,\n initialEmailValue,\n // No onSuccessCallback needed - the 'authenticated' event will be fired automatically\n },\n signUpFormConfig: {\n slots: {\n ...authPrivacyPolicyConsentSlot,\n },\n },\n resetPasswordFormConfig: {},\n })(signInForm);\n\n await showModal(signInForm);\n },\n onSignOutClick: () => {\n authApi.revokeCustomerToken();\n },\n })(container),\n);\n\n/**\n * Renders the shipping address form skeleton (initial placeholder)\n * @param {HTMLElement} container - DOM element to render the form in\n * @returns {Promise<Object>} - The rendered shipping address form skeleton\n */\nexport const renderShippingAddressFormSkeleton = async (container) => renderContainer(\n CONTAINERS.SHIPPING_ADDRESS_FORM_SKELETON,\n async () => AccountProvider.render(AddressForm, {\n fieldIdPrefix: 'shipping',\n isOpen: true,\n showFormLoader: true,\n })(container),\n);\n\n/**\n * Renders the billing address form skeleton (initial placeholder)\n * @param {HTMLElement} container - DOM element to render the form in\n * @returns {Promise<Object>} - The rendered billing address form skeleton\n */\nexport const renderBillingAddressFormSkeleton = async (container) => renderContainer(\n CONTAINERS.BILLING_ADDRESS_FORM_SKELETON,\n async () => AccountProvider.render(AddressForm, {\n fieldIdPrefix: 'billing',\n isOpen: true,\n showFormLoader: true,\n })(container),\n);\n\n/**\n * Renders checkbox to set billing address same as shipping address - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render the checkbox in\n * @returns {Promise<Object>} - The rendered bill to shipping address component\n */\nexport const renderBillToShippingAddress = async (container) => renderContainer(\n CONTAINERS.BILL_TO_SHIPPING_ADDRESS,\n async () => {\n const setBillingAddressOnCart = setAddressOnCart({ type: 'billing' });\n\n return CheckoutProvider.render(BillToShippingAddress, {\n onChange: (checked) => {\n const billingFormValues = events.lastPayload('checkout/addresses/billing');\n\n if (!checked && billingFormValues) {\n setBillingAddressOnCart(billingFormValues);\n }\n },\n })(container);\n },\n);\n\n/**\n * Renders available shipping methods with selection interface\n * @param {HTMLElement} container - DOM element to render shipping methods in\n * @returns {Promise<Object>} - The rendered shipping methods component\n */\nexport const renderShippingMethods = async (container) => renderContainer(\n CONTAINERS.SHIPPING_METHODS,\n async () => CheckoutProvider.render(ShippingMethods)(container),\n);\n\n/**\n * Renders payment methods with credit card integration - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render payment methods in\n * @param {Object} creditCardFormRef - React-style ref for credit card form\n * @returns {Promise<Object>} - The rendered payment methods component\n */\nexport const renderPaymentMethods = async (container, creditCardFormRef) => renderContainer(\n CONTAINERS.PAYMENT_METHODS,\n async () => {\n // Retrieve constants internally to minimize parameters\n const commerceCoreEndpoint = getConfigValue('commerce-core-endpoint') || getConfigValue('commerce-endpoint');\n const getUserTokenCookie = () => getCookie(USER_TOKEN_COOKIE_NAME);\n\n return CheckoutProvider.render(PaymentMethods, {\n slots: {\n Methods: {\n [PaymentMethodCode.CREDIT_CARD]: {\n render: (ctx) => {\n const $creditCard = document.createElement('div');\n\n PaymentServices.render(CreditCard, {\n apiUrl: commerceCoreEndpoint,\n getCustomerToken: getUserTokenCookie,\n getCartId: () => ctx.cartId,\n creditCardFormRef,\n })($creditCard);\n\n ctx.replaceHTML($creditCard);\n },\n },\n [PaymentMethodCode.SMART_BUTTONS]: {\n enabled: false,\n },\n [PaymentMethodCode.APPLE_PAY]: {\n enabled: false,\n },\n [PaymentMethodCode.GOOGLE_PAY]: {\n enabled: false,\n },\n [PaymentMethodCode.VAULT]: {\n enabled: false,\n },\n [PaymentMethodCode.FASTLANE]: {\n enabled: false,\n },\n },\n },\n })(container);\n },\n);\n\n/**\n * Renders terms and conditions with agreement slots and manual consent mode\n * @param {HTMLElement} container - DOM element to render the terms in\n * @returns {Promise<Object>} - The rendered terms and conditions component\n */\nexport const renderTermsAndConditions = async (container) => renderContainer(\n CONTAINERS.TERMS_AND_CONDITIONS,\n async () => CheckoutProvider.render(TermsAndConditions, {\n slots: {\n Agreements: (ctx) => {\n ctx.appendAgreement(() => ({\n name: 'default',\n mode: 'manual',\n translationId: 'Checkout.TermsAndConditions.label',\n }));\n },\n },\n })(container),\n);\n\n/**\n * Renders estimate shipping form for order summary slot\n * @param {HTMLElement} ctx - The slot context element\n * @returns {void}\n */\nexport const renderEstimateShipping = (ctx) => {\n const estimateShippingForm = document.createElement('div');\n CheckoutProvider.render(EstimateShipping)(estimateShippingForm);\n ctx.appendChild(estimateShippingForm);\n};\n\n/**\n * Renders cart coupons for order summary slot\n * @param {HTMLElement} ctx - The slot context element\n * @returns {void}\n */\nexport const renderCartCoupons = (ctx) => {\n const coupons = document.createElement('div');\n CartProvider.render(Coupons)(coupons);\n ctx.appendChild(coupons);\n};\n\n/**\n * Renders gift cards for order summary slot\n * @param {HTMLElement} ctx - The slot context element\n * @returns {void}\n */\nexport const renderGiftCards = (ctx) => {\n const giftCards = document.createElement('div');\n CartProvider.render(GiftCards)(giftCards);\n ctx.appendChild(giftCards);\n};\n\n/**\n * Renders gift options for cart summary list footer slot\n * @param {HTMLElement} ctx - The slot context element\n * @returns {void}\n */\nexport const renderCartGiftOptions = (ctx) => {\n const giftOptions = document.createElement('div');\n\n CartProvider.render(GiftOptions, {\n item: ctx.item,\n view: 'product',\n dataSource: 'cart',\n isEditable: false,\n handleItemsLoading: ctx.handleItemsLoading,\n handleItemsError: ctx.handleItemsError,\n onItemUpdate: ctx.onItemUpdate,\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(giftOptions);\n\n ctx.appendChild(giftOptions);\n};\n\n// ============================================================================\n// SUMMARY CONTAINERS\n// ============================================================================\n\n/**\n * Renders order summary with estimate shipping, coupons, and gift cards slots\n * @param {HTMLElement} container - DOM element to render order summary in\n * @returns {Promise<Object>} - The rendered order summary component\n */\nexport const renderOrderSummary = async (container) => renderContainer(\n CONTAINERS.ORDER_SUMMARY,\n async () => CartProvider.render(OrderSummary, {\n slots: {\n EstimateShipping: renderEstimateShipping,\n Coupons: renderCartCoupons,\n GiftCards: renderGiftCards,\n },\n })(container),\n);\n\n/**\n * Creates the cart summary heading with item count and edit link\n * @param {Object} headingCtx - The heading context with count and DOM methods\n * @returns {void}\n */\nconst createCartSummaryHeading = (headingCtx, placeholders) => {\n const title = placeholders?.Checkout?.Summary?.heading;\n\n // Create main heading container\n const heading = document.createElement('div');\n heading.classList.add('cart-summary-list__heading');\n\n // Create heading text element\n const headingText = document.createElement('div');\n headingText.classList.add('cart-summary-list__heading-text');\n\n // Create edit cart link\n const editCartLink = document.createElement('a');\n editCartLink.classList.add('cart-summary-list__edit');\n editCartLink.href = rootLink('/cart');\n editCartLink.rel = 'noreferrer';\n editCartLink.innerText = placeholders?.Checkout?.Summary?.Edit;\n\n // Helper function to update count text\n const updateCountText = (count) => {\n headingText.innerText = title?.replace(\n '({count})',\n count ? `(${count})` : '',\n );\n };\n\n // Set initial count\n updateCountText(headingCtx.count);\n\n // Assemble heading\n heading.appendChild(headingText);\n heading.appendChild(editCartLink);\n headingCtx.appendChild(heading);\n\n // Listen for count changes\n headingCtx.onChange((nextCtx) => {\n updateCountText(nextCtx.count);\n });\n};\n\n/**\n * Renders cart summary list with custom heading, thumbnail and gift options slots\n * @param {HTMLElement} container - DOM element to render cart summary list in\n * @returns {Promise<Object>} - The rendered cart summary list component\n */\nexport const renderCartSummaryList = async (container) => renderContainer(\n CONTAINERS.CART_SUMMARY_LIST,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n return CartProvider.render(CartSummaryList, {\n variant: 'secondary',\n slots: {\n Heading: (headingCtx) => createCartSummaryHeading(headingCtx, placeholders),\n Thumbnail: (ctx) => {\n const { item, defaultImageProps } = ctx;\n tryRenderAemAssetsImage(ctx, {\n alias: item.sku,\n imageProps: defaultImageProps,\n params: {\n width: defaultImageProps.width,\n height: defaultImageProps.height,\n },\n });\n },\n Footer: renderCartGiftOptions,\n },\n })(container);\n },\n);\n\n/**\n * Renders place order button with handler functions - follows multi-step pattern\n * @param {HTMLElement} container - DOM element to render the place order button in\n * @param {Object} options - Configuration object with handler functions\n * @param {Function} options.handleValidation - Validation handler function\n * @param {Function} options.handlePlaceOrder - Place order handler function\n * @returns {Promise<Object>} - The rendered place order component\n */\nexport const renderPlaceOrder = async (container, options = {}) => renderContainer(\n CONTAINERS.PLACE_ORDER_BUTTON,\n async () => CheckoutProvider.render(PlaceOrder, {\n handleValidation: options.handleValidation,\n handlePlaceOrder: options.handlePlaceOrder,\n })(container),\n);\n\n/**\n * Renders customer shipping addresses selector/form for authenticated users - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render shipping addresses in\n * @param {Object} formRef - React-style ref for form reference\n * @param {Object} data - Cart data containing shipping address information\n * @returns {Promise<Object>} - The rendered customer shipping addresses component\n */\nexport const renderCustomerShippingAddresses = async (container, formRef, data) => renderContainer(\n CONTAINERS.CUSTOMER_SHIPPING_ADDRESSES,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n const cartShippingAddress = getCartAddress(data, 'shipping');\n\n const shippingAddressId = cartShippingAddress\n ? cartShippingAddress?.id ?? 0\n : undefined;\n\n const shippingAddressCache = sessionStorage.getItem(SHIPPING_ADDRESS_DATA_KEY);\n\n // Clear persisted shipping address if cart has a shipping address\n if (cartShippingAddress && shippingAddressCache) {\n sessionStorage.removeItem(SHIPPING_ADDRESS_DATA_KEY);\n }\n\n const storeConfig = checkoutApi.getStoreConfigCache();\n\n const inputsDefaultValueSet = cartShippingAddress && cartShippingAddress.id === undefined\n ? cartShippingAddress\n : { countryCode: storeConfig.defaultCountry };\n\n const hasCartShippingAddress = Boolean(data.shippingAddresses?.[0]);\n let isFirstRenderShipping = true;\n\n const setShippingAddressOnCart = setAddressOnCart({\n type: 'shipping',\n debounceMs: DEBOUNCE_TIME,\n });\n\n const estimateShippingCostOnCart = estimateShippingCost({\n api: checkoutApi.estimateShippingMethods,\n debounceMs: DEBOUNCE_TIME,\n });\n\n const notifyShippingValues = debounce((values) => {\n events.emit('checkout/addresses/shipping', values);\n }, ADDRESS_INPUT_DEBOUNCE_TIME);\n\n return AccountProvider.render(Addresses, {\n addressFormTitle: placeholders?.Checkout?.Addresses?.shippingAddressTitle,\n defaultSelectAddressId: shippingAddressId,\n fieldIdPrefix: 'shipping',\n formName: SHIPPING_FORM_NAME,\n forwardFormRef: formRef,\n inputsDefaultValueSet,\n minifiedView: false,\n onAddressData: (values) => {\n const canSetShippingAddressOnCart = !isFirstRenderShipping || !hasCartShippingAddress;\n if (canSetShippingAddressOnCart) setShippingAddressOnCart(values);\n if (!hasCartShippingAddress) estimateShippingCostOnCart(values);\n if (isFirstRenderShipping) isFirstRenderShipping = false;\n notifyShippingValues(values);\n },\n selectable: true,\n selectShipping: true,\n showBillingCheckBox: false,\n showSaveCheckBox: true,\n showShippingCheckBox: false,\n title: placeholders?.Checkout?.Addresses?.shippingAddressTitle,\n })(container);\n },\n);\n\n/**\n * Renders customer billing addresses selector/form for authenticated users - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render billing addresses in\n * @param {Object} formRef - React-style ref for form reference\n * @param {Object} data - Cart data containing billing address information\n * @returns {Promise<Object>} - The rendered customer billing addresses component\n */\nexport const renderCustomerBillingAddresses = async (container, formRef, data) => renderContainer(\n CONTAINERS.CUSTOMER_BILLING_ADDRESSES,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n const cartBillingAddress = getCartAddress(data, 'billing');\n\n const billingAddressId = cartBillingAddress\n ? cartBillingAddress?.id ?? 0\n : undefined;\n\n const billingAddressCache = sessionStorage.getItem(BILLING_ADDRESS_DATA_KEY);\n\n // Clear persisted billing address if cart has a billing address\n if (cartBillingAddress && billingAddressCache) {\n sessionStorage.removeItem(BILLING_ADDRESS_DATA_KEY);\n }\n\n const storeConfig = checkoutApi.getStoreConfigCache();\n\n const inputsDefaultValueSet = cartBillingAddress && cartBillingAddress.id === undefined\n ? cartBillingAddress\n : { countryCode: storeConfig.defaultCountry };\n\n const hasCartBillingAddress = Boolean(data.billingAddress);\n let isFirstRenderBilling = true;\n\n const setBillingAddressOnCart = setAddressOnCart({\n type: 'billing',\n debounceMs: DEBOUNCE_TIME,\n });\n\n const notifyBillingValues = debounce((values) => {\n events.emit('checkout/addresses/billing', values);\n }, ADDRESS_INPUT_DEBOUNCE_TIME);\n\n return AccountProvider.render(Addresses, {\n addressFormTitle: placeholders?.Checkout?.Addresses?.billToNewAddress,\n defaultSelectAddressId: billingAddressId,\n formName: BILLING_FORM_NAME,\n forwardFormRef: formRef,\n inputsDefaultValueSet,\n minifiedView: false,\n onAddressData: (values) => {\n const canSetBillingAddressOnCart = !isFirstRenderBilling || !hasCartBillingAddress;\n if (canSetBillingAddressOnCart) setBillingAddressOnCart(values);\n if (isFirstRenderBilling) isFirstRenderBilling = false;\n notifyBillingValues(values);\n },\n selectable: true,\n selectBilling: true,\n showBillingCheckBox: false,\n showSaveCheckBox: true,\n showShippingCheckBox: false,\n title: placeholders?.Checkout?.Addresses?.billingAddressTitle,\n })(container);\n },\n);\n\n/**\n * Renders address form for guest users (shipping or billing) - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render address form in\n * @param {Object} formRef - React-style ref for form reference\n * @param {Object} data - Cart data containing address information\n * @param {string} addressType - Type of address form ('shipping' or 'billing')\n * @returns {Promise<Object>} - The rendered address form component\n */\nexport const renderAddressForm = async (container, formRef, data, addressType) => {\n const isShipping = addressType === 'shipping';\n const containerKey = isShipping ? CONTAINERS.SHIPPING_ADDRESS_FORM : CONTAINERS.BILLING_ADDRESS_FORM;\n\n return renderContainer(\n containerKey,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n // Get address type specific configurations\n const cartAddress = getCartAddress(data, addressType);\n const addressDataKey = isShipping ? SHIPPING_ADDRESS_DATA_KEY : BILLING_ADDRESS_DATA_KEY;\n const addressCache = sessionStorage.getItem(addressDataKey);\n\n // Clear persisted address if cart has an address\n if (cartAddress && addressCache) {\n sessionStorage.removeItem(addressDataKey);\n }\n\n let isFirstRender = true;\n const hasCartAddress = Boolean(isShipping ? data.shippingAddresses?.[0] : data.billingAddress);\n\n const setAddressOnCartFn = setAddressOnCart({\n type: addressType,\n debounceMs: DEBOUNCE_TIME,\n });\n\n // Create shipping cost estimator (only for shipping addresses)\n const estimateShippingCostOnCart = isShipping ? estimateShippingCost({\n api: checkoutApi.estimateShippingMethods,\n debounceMs: DEBOUNCE_TIME,\n }) : null;\n\n const notifyValues = debounce((values) => {\n const eventType = isShipping ? 'checkout/addresses/shipping' : 'checkout/addresses/billing';\n events.emit(eventType, values);\n }, ADDRESS_INPUT_DEBOUNCE_TIME);\n\n const storeConfig = checkoutApi.getStoreConfigCache();\n\n // Address type specific configurations\n const formName = isShipping ? SHIPPING_FORM_NAME : BILLING_FORM_NAME;\n const addressTitle = isShipping ? 'Shipping address' : 'Billing address';\n const className = isShipping\n ? 'checkout-shipping-form__address-form'\n : 'checkout-billing-form__address-form';\n\n return AccountProvider.render(AddressForm, {\n addressesFormTitle: addressTitle,\n className,\n fieldIdPrefix: addressType,\n formName,\n forwardFormRef: formRef,\n hideActionFormButtons: true,\n inputsDefaultValueSet: cartAddress ?? {\n countryCode: storeConfig.defaultCountry,\n },\n isOpen: true,\n onChange: (values) => {\n const canSetAddressOnCart = !isFirstRender || !hasCartAddress;\n if (canSetAddressOnCart) setAddressOnCartFn(values);\n\n // Only estimate shipping cost for shipping addresses when no cart address exists\n if (isShipping && !hasCartAddress && estimateShippingCostOnCart) {\n estimateShippingCostOnCart(values);\n }\n\n if (isFirstRender) isFirstRender = false;\n\n notifyValues(values);\n },\n showBillingCheckBox: false,\n showFormLoader: false,\n showShippingCheckBox: false,\n })(container);\n },\n );\n};\n\n/**\n * Renders order-level gift options with swatch image integration\n * @param {HTMLElement} container - DOM element to render gift options in\n * @returns {Promise<Object>} - The rendered gift options component\n */\nexport const renderGiftOptions = async (container) => renderContainer(\n CONTAINERS.GIFT_OPTIONS,\n async () => CartProvider.render(GiftOptions, {\n view: 'order',\n dataSource: 'cart',\n isEditable: false,\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(container),\n);\n\n/**\n * Renders order confirmation header with email check and sign up integration\n * @param {HTMLElement} container - DOM element to render the order header in\n * @param {Object} options - Configuration object with handlers and order data\n * @returns {Promise<Object>} - The rendered order header component\n */\nexport const renderOrderHeader = async (container, options = {}) => renderContainer(\n CONTAINERS.ORDER_HEADER,\n async () => {\n const handleSignUpClick = async ({\n inputsDefaultValueSet,\n addressesData,\n }) => {\n const signUpForm = document.createElement('div');\n\n AuthProvider.render(SignUp, {\n inputsDefaultValueSet,\n addressesData,\n routeSignIn: () => rootLink('/customer/login'),\n routeRedirectOnEmailConfirmationClose: () => rootLink('/customer/account'),\n slots: {\n ...authPrivacyPolicyConsentSlot,\n },\n })(signUpForm);\n\n await showModal(signUpForm);\n };\n\n return OrderProvider.render(OrderHeader, {\n handleEmailAvailability: checkoutApi.isEmailAvailable,\n handleSignUpClick,\n ...options,\n })(container);\n },\n);\n\n/**\n * Renders the order status component\n * @param {HTMLElement} container - The DOM element to render the order status in\n * @returns {Promise<Object>} - The rendered order status component\n */\nexport const renderOrderStatus = async (container) => renderContainer(\n CONTAINERS.ORDER_STATUS,\n async () => OrderProvider.render(OrderStatus, { slots: { OrderActions: () => null } })(container),\n);\n\n/**\n * Renders the shipping status component\n * @param {HTMLElement} container - The DOM element to render the shipping status in\n * @returns {Promise<Object>} - The rendered shipping status component\n */\nexport const renderShippingStatus = async (container) => renderContainer(\n CONTAINERS.SHIPPING_STATUS,\n async () => OrderProvider.render(ShippingStatus)(container),\n);\n\n/**\n * Renders the customer details component\n * @param {HTMLElement} container - The DOM element to render the customer details in\n * @returns {Promise<Object>} - The rendered customer details component\n */\nexport const renderCustomerDetails = async (container) => renderContainer(\n CONTAINERS.CUSTOMER_DETAILS,\n async () => OrderProvider.render(CustomerDetails)(container),\n);\n\n/**\n * Renders the order cost summary component\n * @param {HTMLElement} container - The DOM element to render the order cost summary in\n * @returns {Promise<Object>} - The rendered order cost summary component\n */\nexport const renderOrderCostSummary = async (container) => renderContainer(\n CONTAINERS.ORDER_COST_SUMMARY,\n async () => OrderProvider.render(OrderCostSummary)(container),\n);\n\n/**\n * Renders the order product list component with image slots and gift options\n * @param {HTMLElement} container - The DOM element to render the order product list in\n * @returns {Promise<Object>} - The rendered order product list component\n */\nexport const renderOrderProductList = async (container) => renderContainer(\n CONTAINERS.ORDER_PRODUCT_LIST,\n async () => OrderProvider.render(OrderProductList, {\n slots: {\n Footer: (ctx) => {\n const giftOptions = document.createElement('div');\n CartProvider.render(GiftOptions, {\n item: ctx.item,\n view: 'product',\n dataSource: 'order',\n isEditable: false,\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(giftOptions);\n ctx.appendChild(giftOptions);\n },\n CartSummaryItemImage: (ctx) => {\n const { data, defaultImageProps } = ctx;\n tryRenderAemAssetsImage(ctx, {\n alias: data.product.sku,\n imageProps: defaultImageProps,\n params: {\n width: defaultImageProps.width,\n height: defaultImageProps.height,\n },\n });\n },\n },\n })(container),\n);\n\n/**\n * Renders order-level gift options for order confirmation\n * @param {HTMLElement} container - DOM element to render order gift options in\n * @returns {Promise<Object>} - The rendered order gift options component\n */\nexport const renderOrderGiftOptions = async (container) => renderContainer(\n CONTAINERS.ORDER_GIFT_OPTIONS,\n async () => CartProvider.render(GiftOptions, {\n view: 'order',\n dataSource: 'order',\n isEditable: false,\n readOnlyFormOrderView: 'secondary',\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(container),\n);\n\n/**\n * Renders the continue shopping button for order confirmation footer\n * @param {HTMLElement} container - DOM element to render the button in\n * @returns {Promise<Object>} - The rendered continue shopping button component\n */\nexport const renderOrderConfirmationFooterButton = async (container) => renderContainer(\n CONTAINERS.ORDER_CONFIRMATION_FOOTER_BUTTON,\n async () => UI.render(Button, {\n children: 'Continue shopping',\n 'data-testid': 'order-confirmation-footer__continue-button',\n className: 'order-confirmation-footer__continue-button',\n size: 'medium',\n variant: 'primary',\n type: 'submit',\n href: rootLink('/'),\n })(container),\n);\n",
|
|
323
|
+
"fragments.js": "// eslint-disable-next-line import/no-unresolved\nimport { createFragment } from '@dropins/storefront-checkout/lib/utils.js';\n\nimport {\n CHECKOUT_BLOCK,\n ORDER_CONFIRMATION_BLOCK,\n} from './constants.js';\n\n/**\n * A frozen, nested object of CSS selectors\n * @readonly\n */\nexport const selectors = Object.freeze({\n checkout: {\n content: '.checkout__content',\n loader: '.checkout__loader',\n mergedCartBanner: '.checkout__merged-cart-banner',\n heading: '.checkout__heading',\n serverError: '.checkout__server-error',\n outOfStock: '.checkout__out-of-stock',\n login: '.checkout__login',\n shippingForm: '.checkout__shipping-form',\n billToShipping: '.checkout__bill-to-shipping',\n delivery: '.checkout__delivery',\n paymentMethods: '.checkout__payment-methods',\n billingForm: '.checkout__billing-form',\n orderSummary: '.checkout__order-summary',\n cartSummary: '.checkout__cart-summary',\n placeOrder: '.checkout__place-order',\n giftOptions: '.checkout__gift-options',\n termsAndConditions: '.checkout__terms-and-conditions',\n main: '.checkout__main',\n aside: '.checkout__aside',\n },\n orderConfirmation: {\n header: '.order-confirmation__header',\n orderStatus: '.order-confirmation__order-status',\n shippingStatus: '.order-confirmation__shipping-status',\n customerDetails: '.order-confirmation__customer-details',\n orderCostSummary: '.order-confirmation__order-cost-summary',\n giftOptions: '.order-confirmation__gift-options',\n orderProductList: '.order-confirmation__order-product-list',\n footer: '.order-confirmation__footer',\n continueButton: '.order-confirmation-footer__continue-button',\n contactSupportLink: '.order-confirmation-footer__contact-support-link',\n },\n});\n\n// =============================================================================\n// CHECKOUT\n// =============================================================================\n\n/**\n * Creates the main checkout fragment with all checkout blocks.\n * @returns {DocumentFragment} The complete checkout fragment.\n */\nexport function createCheckoutFragment() {\n return createFragment(`\n <div class=\"checkout__wrapper\">\n <div class=\"checkout__loader\"></div>\n <div class=\"checkout__content\">\n <div class=\"checkout__merged-cart-banner\"></div>\n <div class=\"checkout__main\">\n <div class=\"checkout__heading ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__server-error ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__out-of-stock ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__login ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__shipping-form ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__bill-to-shipping ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__delivery ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__payment-methods ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__billing-form ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__terms-and-conditions ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__place-order ${CHECKOUT_BLOCK}\"></div>\n </div>\n <div class=\"checkout__aside\">\n <div class=\"checkout__order-summary ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__gift-options ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__cart-summary ${CHECKOUT_BLOCK}\"></div>\n </div>\n </div>\n </div>\n `);\n}\n\n// =============================================================================\n// ORDER CONFIRMATION\n// =============================================================================\n\n/**\n * Creates the order confirmation fragment.\n * @returns {DocumentFragment} The order confirmation fragment.\n */\nexport function createOrderConfirmationFragment() {\n return createFragment(`\n <div class=\"order-confirmation\">\n <div class=\"order-confirmation__main\">\n <div class=\"order-confirmation__header ${ORDER_CONFIRMATION_BLOCK}\"></div>\n <div class=\"order-confirmation__order-status ${ORDER_CONFIRMATION_BLOCK}\"></div>\n <div class=\"order-confirmation__shipping-status ${ORDER_CONFIRMATION_BLOCK}\"></div>\n <div class=\"order-confirmation__customer-details ${ORDER_CONFIRMATION_BLOCK}\"></div>\n </div>\n <div class=\"order-confirmation__aside\">\n <div class=\"order-confirmation__order-cost-summary ${ORDER_CONFIRMATION_BLOCK}\"></div>\n <div class=\"order-confirmation__gift-options ${ORDER_CONFIRMATION_BLOCK}\"></div>\n <div class=\"order-confirmation__order-product-list ${ORDER_CONFIRMATION_BLOCK}\"></div>\n <div class=\"order-confirmation__footer ${ORDER_CONFIRMATION_BLOCK}\"></div>\n </div>\n </div>\n `);\n}\n\n/**\n * Creates the order confirmation footer content with support link.\n * @param {string} supportPath - The support page path for the contact link\n * @returns {string} The footer HTML content\n */\nexport function createOrderConfirmationFooter(supportPath) {\n return `\n <div class=\"order-confirmation-footer__continue-button\"></div>\n <div class=\"order-confirmation-footer__contact-support\">\n <p>\n Need help?\n <a\n href=\"${supportPath}\"\n rel=\"noreferrer\"\n class=\"order-confirmation-footer__contact-support-link\"\n data-testid=\"order-confirmation-footer__contact-support-link\"\n >\n Contact us\n </a>\n </p>\n </div>\n `;\n}\n",
|
|
324
|
+
"utils.js": "/* eslint-disable import/no-unresolved */\nimport { ProgressSpinner, provider as UI } from '@dropins/tools/components.js';\nimport { tryRenderAemAssetsImage } from '@dropins/tools/lib/aem/assets.js';\nimport { ORDER_DETAILS_PATH, rootLink } from '../../scripts/commerce.js';\nimport { getUserTokenCookie } from '../../scripts/initializers/index.js';\nimport createModal from '../modal/modal.js';\n\n/**\n * Displays an overlay spinner in the specified container\n * @param {Object} loaderRef - Ref object to store the spinner component\n * @param {HTMLElement} $loader - DOM element to render the spinner in\n */\nexport const displayOverlaySpinner = async (loaderRef, $loader) => {\n if (loaderRef.current) return;\n\n loaderRef.current = await UI.render(ProgressSpinner, {\n className: '.checkout__overlay-spinner',\n })($loader);\n};\n\n/**\n * Removes the overlay spinner and cleans up references\n * @param {Object} loaderRef - Ref object containing the spinner component\n * @param {HTMLElement} $loader - DOM element containing the spinner\n */\nexport const removeOverlaySpinner = (loaderRef, $loader) => {\n if (!loaderRef.current) return;\n\n loaderRef.current.remove();\n loaderRef.current = null;\n $loader.innerHTML = '';\n};\n\n// Modal state management\nlet modal;\n\n/**\n * Shows a modal with the specified content\n * @param {HTMLElement} content - DOM element to display in the modal\n */\nexport const showModal = async (content) => {\n modal = await createModal([content]);\n modal.showModal();\n};\n\n/**\n * Removes the currently displayed modal and cleans up references\n */\nexport const removeModal = () => {\n if (!modal) return;\n modal.removeModal();\n modal = null;\n};\n\n/**\n * Renders AEM asset images for gift option swatches\n * @param {Object} ctx - The context object containing imageSwatchContext and defaultImageProps\n */\nexport function swatchImageSlot(ctx) {\n const { imageSwatchContext, defaultImageProps } = ctx;\n tryRenderAemAssetsImage(ctx, {\n alias: imageSwatchContext.label,\n imageProps: defaultImageProps,\n wrapper: document.createElement('span'),\n params: {\n width: defaultImageProps.width,\n height: defaultImageProps.height,\n },\n });\n}\n\n/**\n * Builds the order details URL based on authentication status\n * @param {Object} orderData - Order data containing number and token\n * @param {string} orderDetailsPath - Path to the order details page\n * @returns {string} The constructed order details URL\n */\nexport function buildOrderDetailsUrl(orderData, orderDetailsPath = ORDER_DETAILS_PATH) {\n const token = getUserTokenCookie();\n const orderRef = token ? orderData.number : orderData.token;\n const orderNumber = orderData.number;\n const encodedOrderRef = encodeURIComponent(orderRef);\n const encodedOrderNumber = encodeURIComponent(orderNumber);\n\n return token\n ? rootLink(`${orderDetailsPath}?orderRef=${encodedOrderRef}`)\n : rootLink(`${orderDetailsPath}?orderRef=${encodedOrderRef}&orderNumber=${encodedOrderNumber}`);\n}\n"
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
"name": "commerce-checkout-braintree",
|
|
329
|
+
"description": "Example commerce-checkout-braintree block for storefront-checkout",
|
|
330
|
+
"files": {
|
|
331
|
+
"commerce-checkout-braintree.js": "/* eslint-disable import/no-unresolved */\n/* eslint-disable no-unused-vars */\n\n// Dropin Tools\nimport { events } from '@dropins/tools/event-bus.js';\nimport { initReCaptcha } from '@dropins/tools/recaptcha.js';\n\n// Checkout Dropin\nimport * as checkoutApi from '@dropins/storefront-checkout/api.js';\nimport PaymentMethods from '@dropins/storefront-checkout/containers/PaymentMethods.js';\n\nimport { render as CheckoutProvider } from '@dropins/storefront-checkout/render.js';\n\n// Order Dropin Modules\nimport * as orderApi from '@dropins/storefront-order/api.js';\n\n// Checkout Dropin Libraries\nimport {\n createScopedSelector,\n isVirtualCart,\n setMetaTags,\n validateForms,\n} from '@dropins/storefront-checkout/lib/utils.js';\n\n// 1. Import Braintree Payment Gateway\nimport 'https://js.braintreegateway.com/web/dropin/1.43.0/js/dropin.min.js';\n\n// Local utils\nimport {\n buildOrderDetailsUrl,\n displayOverlaySpinner,\n removeOverlaySpinner,\n} from './utils.js';\n\n// Constants\nimport {\n BILLING_ADDRESS_DATA_KEY,\n BILLING_FORM_NAME,\n LOGIN_FORM_NAME,\n PURCHASE_ORDER_FORM_NAME,\n SHIPPING_ADDRESS_DATA_KEY,\n SHIPPING_FORM_NAME,\n TERMS_AND_CONDITIONS_FORM_NAME,\n} from './constants.js';\n\n// Fragment functions\nimport {\n createCheckoutFragment,\n selectors,\n} from './fragments.js';\n\n// Container functions\nimport {\n renderAddressForm,\n renderBillingAddressFormSkeleton,\n renderBillToShippingAddress,\n renderCartSummaryList,\n renderCheckoutHeader,\n renderCustomerBillingAddresses,\n renderCustomerShippingAddresses,\n renderGiftOptions,\n renderLoginForm,\n renderMergedCartBanner,\n renderOrderSummary,\n renderOutOfStock,\n renderPlaceOrder,\n renderServerError,\n renderShippingAddressFormSkeleton,\n renderShippingMethods,\n renderTermsAndConditions,\n} from './containers.js';\n\nimport { rootLink } from '../../scripts/commerce.js';\n\n// Initializers\nimport '../../scripts/initializers/account.js';\nimport '../../scripts/initializers/checkout.js';\nimport '../../scripts/initializers/order.js';\n\n// Checkout success block import and CSS preload\nimport {\n preloadCheckoutSuccess,\n renderCheckoutSuccess,\n} from '../commerce-checkout-success/commerce-checkout-success.js';\n\npreloadCheckoutSuccess();\n\nfunction redirectToCartIfEmpty(cartData) {\n const isOrderPlaced = events.lastPayload('order/placed') !== undefined;\n\n if (!isOrderPlaced && (cartData === null || cartData?.items?.length === 0)) {\n window.location.href = rootLink('/cart');\n }\n}\n\n// Braintree-specific constants\nconst BRAINTREE_AUTHORIZATION_TOKEN = '<YOUR_BRAINTREE_SANDBOX_TOKEN>';\n\n// Braintree-specific container renderer\nasync function renderBraintreePaymentMethods(container, braintreeInstanceRef) {\n return CheckoutProvider.render(PaymentMethods, {\n slots: {\n Methods: {\n braintree: {\n autoSync: false,\n render: async (ctx) => {\n const braintreeContainer = document.createElement('div');\n\n window.braintree.dropin.create({\n authorization: BRAINTREE_AUTHORIZATION_TOKEN,\n container: braintreeContainer,\n }, (err, dropinInstance) => {\n if (err) {\n console.error(err);\n }\n\n braintreeInstanceRef.current = dropinInstance;\n });\n\n ctx.replaceHTML(braintreeContainer);\n },\n },\n },\n },\n })(container);\n}\n\nexport default async function decorate(block) {\n setMetaTags('Checkout');\n document.title = 'Braintree Checkout';\n\n const cartData = events.lastPayload('cart/initialized');\n redirectToCartIfEmpty(cartData);\n\n // Container and component references\n let shippingForm;\n let billingForm;\n let shippingAddresses;\n let billingAddresses;\n\n // Braintree-specific variable reference\n const braintreeInstanceRef = { current: null };\n\n const shippingFormRef = { current: null };\n const billingFormRef = { current: null };\n const loaderRef = { current: null };\n\n events.on('order/placed', () => {\n setMetaTags('Order Confirmation');\n document.title = 'Braintree Order Confirmation';\n });\n\n // Create the checkout layout using fragments\n const checkoutFragment = createCheckoutFragment();\n\n // Create scoped selector for the checkout fragment\n const getElement = createScopedSelector(checkoutFragment);\n\n // Get all checkout elements using centralized selectors\n const $content = getElement(selectors.checkout.content);\n const $loader = getElement(selectors.checkout.loader);\n const $mergedCartBanner = getElement(selectors.checkout.mergedCartBanner);\n const $heading = getElement(selectors.checkout.heading);\n const $serverError = getElement(selectors.checkout.serverError);\n const $outOfStock = getElement(selectors.checkout.outOfStock);\n const $login = getElement(selectors.checkout.login);\n const $shippingForm = getElement(selectors.checkout.shippingForm);\n const $billToShipping = getElement(selectors.checkout.billToShipping);\n const $delivery = getElement(selectors.checkout.delivery);\n const $paymentMethods = getElement(selectors.checkout.paymentMethods);\n const $billingForm = getElement(selectors.checkout.billingForm);\n const $orderSummary = getElement(selectors.checkout.orderSummary);\n const $cartSummary = getElement(selectors.checkout.cartSummary);\n const $placeOrder = getElement(selectors.checkout.placeOrder);\n const $giftOptions = getElement(selectors.checkout.giftOptions);\n const $termsAndConditions = getElement(selectors.checkout.termsAndConditions);\n\n block.appendChild(checkoutFragment);\n\n // Create validation and place order handlers\n const handleValidation = () => validateForms([\n { name: LOGIN_FORM_NAME },\n { name: SHIPPING_FORM_NAME, ref: shippingFormRef },\n { name: BILLING_FORM_NAME, ref: billingFormRef },\n { name: PURCHASE_ORDER_FORM_NAME },\n { name: TERMS_AND_CONDITIONS_FORM_NAME },\n ]);\n\n const handlePlaceOrder = async ({ cartId, code }) => {\n await displayOverlaySpinner(loaderRef, $loader);\n try {\n switch (code) {\n case 'braintree': {\n braintreeInstanceRef.current.requestPaymentMethod(async (err, payload) => {\n if (err) {\n removeOverlaySpinner(loaderRef, $loader);\n console.error(err);\n return;\n }\n\n await checkoutApi.setPaymentMethod({\n code: 'braintree',\n braintree: {\n is_active_payment_token_enabler: false,\n payment_method_nonce: payload.nonce,\n },\n });\n\n await orderApi.placeOrder(cartId);\n });\n\n break;\n }\n\n default: {\n // Place order\n await orderApi.placeOrder(cartId);\n }\n }\n } catch (error) {\n console.error(error);\n throw error;\n } finally {\n removeOverlaySpinner(loaderRef, $loader);\n }\n };\n\n // First, render the place order component\n await renderPlaceOrder($placeOrder, { handleValidation, handlePlaceOrder });\n\n // Render the remaining containers\n const [\n _mergedCartBanner,\n _header,\n _serverError,\n _outOfStock,\n _loginForm,\n shippingFormSkeleton,\n _billToShipping,\n _shippingMethods,\n _paymentMethods,\n billingFormSkeleton,\n _orderSummary,\n _cartSummary,\n _termsAndConditions,\n _giftOptions,\n ] = await Promise.all([\n renderMergedCartBanner($mergedCartBanner),\n\n renderCheckoutHeader($heading, 'Braintree Checkout'),\n\n renderServerError($serverError, $content),\n\n renderOutOfStock($outOfStock),\n\n renderLoginForm($login),\n\n renderShippingAddressFormSkeleton($shippingForm),\n\n renderBillToShippingAddress($billToShipping),\n\n renderShippingMethods($delivery),\n\n renderBraintreePaymentMethods($paymentMethods, braintreeInstanceRef),\n\n renderBillingAddressFormSkeleton($billingForm),\n\n renderOrderSummary($orderSummary),\n\n renderCartSummaryList($cartSummary),\n\n renderTermsAndConditions($termsAndConditions),\n\n renderGiftOptions($giftOptions),\n ]);\n\n async function initializeCheckout(data) {\n await initReCaptcha(0);\n if (data.isGuest) await displayGuestAddressForms(data);\n else {\n removeOverlaySpinner(loaderRef, $loader);\n await displayCustomerAddressForms(data);\n }\n }\n\n async function displayGuestAddressForms(data) {\n if (isVirtualCart(data)) {\n shippingForm?.remove();\n shippingForm = null;\n $shippingForm.innerHTML = '';\n } else if (!shippingForm) {\n shippingFormSkeleton.remove();\n\n shippingForm = await renderAddressForm($shippingForm, shippingFormRef, data, 'shipping');\n }\n\n if (!billingForm) {\n billingFormSkeleton.remove();\n\n billingForm = await renderAddressForm($billingForm, billingFormRef, data, 'billing');\n }\n }\n\n async function displayCustomerAddressForms(data) {\n if (isVirtualCart(data)) {\n shippingAddresses?.remove();\n shippingAddresses = null;\n $shippingForm.innerHTML = '';\n } else if (!shippingAddresses) {\n shippingForm?.remove();\n shippingForm = null;\n shippingFormRef.current = null;\n\n shippingAddresses = await renderCustomerShippingAddresses(\n $shippingForm,\n shippingFormRef,\n data,\n );\n }\n\n if (!billingAddresses) {\n billingForm?.remove();\n billingForm = null;\n billingFormRef.current = null;\n\n billingAddresses = await renderCustomerBillingAddresses(\n $billingForm,\n billingFormRef,\n data,\n );\n }\n }\n\n async function handleCheckoutUpdated(data) {\n if (!data) return;\n await initializeCheckout(data);\n }\n\n function handleAuthenticated(authenticated) {\n if (!authenticated) return;\n\n // When a customer creates an account on the checkout success page and then\n // signs in, they will be redirected to the order details page with the order\n // number as orderRef, allowing the order details to be displayed\n const orderData = events.lastPayload('order/placed');\n if (orderData) {\n const url = buildOrderDetailsUrl(orderData);\n window.history.pushState({}, '', url);\n }\n\n window.location.reload();\n }\n\n function handleCheckoutValues(payload) {\n const { isBillToShipping } = payload;\n $billingForm.style.display = isBillToShipping ? 'none' : 'block';\n }\n\n async function handleOrderPlaced(orderData) {\n // Clear address form data\n sessionStorage.removeItem(SHIPPING_ADDRESS_DATA_KEY);\n sessionStorage.removeItem(BILLING_ADDRESS_DATA_KEY);\n\n const url = buildOrderDetailsUrl(orderData);\n\n window.history.pushState({}, '', url);\n\n await renderCheckoutSuccess(block, { orderData });\n }\n\n events.on('authenticated', handleAuthenticated);\n events.on('checkout/initialized', handleCheckoutUpdated, { eager: true });\n events.on('checkout/updated', handleCheckoutUpdated);\n events.on('checkout/values', handleCheckoutValues);\n events.on('order/placed', handleOrderPlaced);\n events.on('cart/initialized', redirectToCartIfEmpty, { eager: true });\n events.on('cart/data', redirectToCartIfEmpty);\n}\n",
|
|
332
|
+
"commerce-checkout-braintree.css": ".checkout__content {\n display: grid;\n grid-template-columns: 1fr;\n gap: var(--spacing-big) 0;\n}\n\n.checkout__merged-cart-banner {\n display: grid;\n grid-column: 1 / -1;\n align-items: start;\n grid-template-columns: auto;\n}\n\n.checkout__main {\n display: grid;\n row-gap: var(--spacing-xbig);\n margin-top: var(--spacing-medium);\n}\n\n.checkout__aside {\n display: grid;\n gap: var(--spacing-xbig);\n}\n\n.checkout-header h1 {\n margin: 0;\n}\n\n/* Block dividers */\n.checkout__block.checkout__heading .dropin-header-container {\n gap: var(--spacing-xsmall);\n}\n\n.checkout__shipping-form {\n padding-top: var(--spacing-xbig);\n border-top: var(--shape-border-width-3) solid var(--color-neutral-400);\n}\n\n.checkout__payment-methods {\n padding-top: var(--spacing-xbig);\n border-top: var(--shape-border-width-3) solid var(--color-neutral-400);\n padding-bottom: var(--spacing-xbig);\n border-bottom: var(--shape-border-width-3) solid var(--color-neutral-400);\n}\n\n/* Server error visibility */\n.checkout__server-error {\n display: none;\n}\n\n/* Show when it contains actual error content */\n.checkout__server-error:has(.dropin-illustrated-message) {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n}\n\n/* Safari fallback: show when not empty, but this may show empty divs briefly */\n@supports not selector(:has(*)) {\n .checkout__server-error:not(:empty) {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n }\n}\n\n/* Hide empty blocks */\n.checkout__block:empty {\n display: none;\n}\n\n/* Hide main containers when there is a server error */\n.checkout__content--error .checkout__merged-cart-banner,\n.checkout__content--error .checkout__out-of-stock,\n.checkout__content--error .checkout__login,\n.checkout__content--error .checkout__shipping-form,\n.checkout__content--error .checkout__bill-to-shipping,\n.checkout__content--error .checkout__delivery,\n.checkout__content--error .checkout__payment-methods,\n.checkout__content--error .checkout__billing-form,\n.checkout__content--error .checkout__terms-and-conditions {\n display: none !important;\n}\n\n/* Hide blocks with empty divs */\n.checkout__out-of-stock:has(> :empty),\n.checkout__merged-cart-banner:has(> :empty),\n.checkout__delivery:has(> div:first-child:empty),\n.checkout__bill-to-shipping:has(> :empty),\n.checkout__gift-options:has(.cart-gift-options-view--readonly:empty) {\n display: none;\n}\n\n/* Hide aside containers when there is a server error */\n.checkout__content--error .checkout__aside {\n display: none;\n}\n\n/* Integrate place order button into Order Summary - mobile */\n.checkout__place-order {\n grid-column: unset;\n justify-items: unset;\n margin-top: calc(var(--spacing-big) * -1);\n}\n\n/* Hide the place order button when there is a server error */\n.checkout__content--error .checkout__place-order {\n display: none;\n}\n\n.checkout__loader {\n align-items: center;\n background: var(--color-neutral-50);\n display: flex;\n height: 100vh;\n justify-content: center;\n left: 0;\n opacity: 0.5;\n position: fixed;\n top: 0;\n width: 100%;\n z-index: 9999;\n}\n\n.checkout__loader:empty {\n display: none;\n}\n\n/* remove margin from the heading divider */\n.checkout__heading .dropin-divider {\n margin: 0;\n}\n\n/* Cart Summary */\n.checkout__block .cart-cart-summary-list {\n padding: var(--spacing-medium);\n}\n\n/* Order Summary Coupon */\n.dropin-accordion-section__heading {\n margin: var(--spacing-medium) auto;\n}\n\n.cart-coupons__accordion {\n margin-top: var(--spacing-xsmall);\n}\n\n/* temporary fix to hide the default cart heading */\n[data-testid=\"default-cart-heading\"] {\n display: none;\n}\n\n.cart-summary-list__heading {\n display: flex;\n justify-content: space-between;\n align-items: flex-end;\n}\n\n.cart-summary-list__heading-text {\n font: var(--type-headline-2-strong-font);\n letter-spacing: var(--type-headline-2-strong-letter-spacing);\n color: var(--color-neutral-800);\n}\n\n.cart-cart-summary-list__heading {\n row-gap: var(--spacing-small);\n padding-top: 0;\n}\n\n.cart-cart-summary-list__heading-text {\n font: var(--type-headline-2-strong-font);\n letter-spacing: var(--type-headline-2-strong-letter-spacing);\n color: var(--color-neutral-800);\n}\n\n.cart-summary-list__edit {\n font: var(--type-body-2-strong-font);\n letter-spacing: var(--type-body-2-strong-letter-spacing);\n}\n\n.checkout__block\n.cart-cart-summary-list\n.cart-cart-summary-list__footer-divider {\n margin: var(--spacing-small) 0;\n}\n\n/* Sign-in modal */\n#modal {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-color: rgb(0 0 0 / 50%);\n display: flex;\n justify-content: center;\n align-items: center;\n z-index: 2;\n}\n\n#modal-form {\n width: 800px;\n}\n\n/* Address form */\n.checkout__shipping-form .account-address-form-wrapper__title,\n.checkout__shipping-form .dropin-header-container__title,\n.checkout__billing-form .account-address-form-wrapper__title,\n.checkout__billing-form .dropin-header-container__title {\n font: var(--type-headline-2-default-font);\n letter-spacing: var(--type-headline-2-default-letter-spacing);\n color: var(--color-neutral-800);\n margin: 0 0 var(--spacing-medium) 0;\n}\n\n.checkout__shipping-form .dropin-header-container .dropin-divider,\n.checkout__billing-form .dropin-header-container .dropin-divider {\n display: none;\n}\n\n/* Order confirmation */\n.order-confirmation {\n display: grid;\n align-items: start;\n grid-template-columns: repeat(var(--grid-4-columns), 1fr);\n grid-template-areas: \"main aside\";\n grid-column-gap: var(--grid-4-gutters);\n margin-bottom: var(--spacing-xbig);\n padding-top: var(--spacing-xxlarge);\n}\n\n.order-confirmation__main {\n display: grid;\n grid-row-gap: var(--spacing-xbig);\n grid-column: 1 / span 7;\n}\n\n.order-confirmation__aside {\n display: grid;\n grid-row-gap: var(--spacing-xbig);\n grid-column: 9 / span 4;\n}\n\n.order-confirmation__footer {\n display: grid;\n gap: var(--spacing-small);\n text-align: center;\n}\n\n.order-confirmation__footer p {\n margin: 0;\n}\n\n.order-confirmation__footer .order-confirmation-footer__continue-button {\n margin: 0 auto;\n text-align: center;\n display: inline-block;\n}\n\n.order-confirmation-footer__contact-support {\n font: var(--type-body-2-default-font);\n letter-spacing: var(--type-body-2-default-letter-spacing);\n color: var(--color-neutral-700);\n}\n\n.order-confirmation-footer__contact-support a {\n font: var(--type-body-2-strong-font);\n letter-spacing: var(--type-body-2-strong-letter-spacing);\n color: var(--color-brand-500);\n cursor: pointer;\n}\n\n/* Hide empty blocks */\n.order-confirmation__block:empty {\n display: none;\n}\n\n@media only screen and (min-width: 320px) and (max-width: 768px) {\n .checkout__main,\n .checkout__aside {\n display: contents;\n }\n\n .checkout__block {\n order: 3;\n }\n\n .checkout__heading {\n order: 1;\n }\n\n .checkout__cart-summary {\n order: 2;\n }\n\n .checkout__place-order {\n order: 4;\n }\n\n .order-confirmation {\n grid-template-columns: repeat(var(--grid-1-columns), 1fr);\n padding-top: 0;\n }\n\n .order-confirmation__main,\n .order-confirmation__aside {\n grid-row-gap: var(--spacing-medium);\n }\n\n .order-confirmation > div {\n grid-column: 1 / span 4;\n }\n\n .order-confirmation__block .dropin-card {\n border: 0;\n }\n}\n\n@media only screen and (min-width: 768px) {\n .checkout__content {\n display: grid;\n align-items: start;\n grid-template-columns: repeat(var(--grid-4-columns), 1fr);\n gap: var(--spacing-big) var(--grid-4-gutters);\n }\n\n .checkout__content--error {\n display: grid;\n grid-template-columns: 1fr;\n }\n\n .checkout__main {\n grid-column: 1 / span 7;\n row-gap: var(--spacing-xbig);\n }\n\n .checkout__aside {\n grid-column: 9 / span 4;\n gap: var(--spacing-xbig);\n }\n\n .checkout__place-order {\n margin-top: 0;\n }\n}\n",
|
|
333
|
+
"README.md": "# Integrating Braintree Payment Method in Checkout\n\nThis guide will walk you through the steps to integrate the Braintree payment method into the Checkout.\n\n## Hands on\n\nWe'll use the **commerce-checkout** block as our starting point and iteratively update it to meet the new product requirements.\n\n> [!NOTE]\n> Please note the _**commerce-checkout.js**_ block is the only one that is fully functional up-to-date with the latest Drop-ins versions.\n> Use the following guidelines just as a reference when creating a new checkout experience.\n\n## Step-by-Step Process:\n\n### 1. Import Braintree Payment Gateway\n\nInclude the Braintree Drop-in library in your project:\n\n```html\n<script src=\"https://js.braintreegateway.com/web/dropin/1.43.0/js/dropin.min.js\"></script>\n```\n\nOr directly in the `commerce-checkout.js` block file:\n```js\nimport 'https://js.braintreegateway.com/web/dropin/1.43.0/js/dropin.min.js';\n```\n\n### 2. Create variable to store the Braintree dropin instance\n\nDefine a `braintreeInstance` variable to manage the Braintree Drop-in instance. \n\n```js\nlet braintreeInstance;\n```\n\n### 3. Add Braintree Handler to Payment Methods Container\n\nUpdate the `PaymentMethods` container to include a custom handler for the Braintree payment method and set `autoSync` to `false` to prevent automatic calls to `setPaymentMethod` mutation on change.\n\n```js\nCheckoutProvider.render(PaymentMethods, {\n slots: {\n Methods: {\n braintree: {\n autoSync: false,\n render: async (ctx) => {\n const container = document.createElement('div');\n\n window.braintree.dropin.create({\n authorization: '<YOUR_BRAINTREE_SANDBOX_TOKEN>',\n container,\n }, (err, dropinInstance) => {\n if (err) {\n console.error(err);\n }\n\n braintreeInstance = dropinInstance;\n });\n\n ctx.replaceHTML(container);\n },\n },\n },\n },\n})($paymentMethods),\n```\n\n### 4. Handle Braintree Payment Method in `PlaceOrder` Container\n\nImplement the Braintree payment logic in the `PlaceOrder` container within the `handlePlaceOrder` handler. This includes processing the payment with the Braintree nonce.\n\n```js\nCheckoutProvider.render(PlaceOrder, {\n handlePlaceOrder: async ({ cartId, code }) => {\n await displayOverlaySpinner();\n try {\n switch (code) {\n case 'braintree': {\n braintreeInstance.requestPaymentMethod(async (err, payload) => {\n if (err) {\n removeOverlaySpinner();\n console.error(err);\n return;\n }\n\n await checkoutApi.setPaymentMethod({\n code: 'braintree',\n braintree: {\n is_active_payment_token_enabler: false,\n payment_method_nonce: payload.nonce,\n },\n });\n\n await orderApi.placeOrder(cartId);\n });\n\n break;\n }\n\n default: {\n // Place order\n await orderApi.placeOrder(cartId);\n }\n }\n } catch (error) {\n console.error(error);\n throw error;\n } finally {\n await removeOverlaySpinner();\n }\n },\n})($placeOrder),\n```\n",
|
|
334
|
+
"constants.js": "// Form and address constants\nconst BILLING_FORM_NAME = 'selectedBillingAddress';\nconst BILLING_ADDRESS_DATA_KEY = `${BILLING_FORM_NAME}_addressData`;\nconst LOGIN_FORM_NAME = 'login-form';\nconst PURCHASE_ORDER_FORM_NAME = 'purchase-order';\nconst SHIPPING_FORM_NAME = 'selectedShippingAddress';\nconst SHIPPING_ADDRESS_DATA_KEY = `${SHIPPING_FORM_NAME}_addressData`;\nconst TERMS_AND_CONDITIONS_FORM_NAME = 'checkout-terms-and-conditions__form';\n\n// Timing constants\nconst DEBOUNCE_TIME = 1000;\nconst ADDRESS_INPUT_DEBOUNCE_TIME = 500;\n\n// Block and styling constants\nconst CHECKOUT_BLOCK = 'checkout__block';\nconst CHECKOUT_ERROR_CLASS = 'checkout__content--error';\nconst CHECKOUT_HEADER_CLASS = 'checkout-header';\nconst ORDER_CONFIRMATION_BLOCK = 'order-confirmation__block';\n\n// Default values\nconst USER_TOKEN_COOKIE_NAME = 'auth_dropin_user_token';\n\nexport {\n // Form and address constants\n BILLING_ADDRESS_DATA_KEY,\n BILLING_FORM_NAME,\n LOGIN_FORM_NAME,\n PURCHASE_ORDER_FORM_NAME,\n SHIPPING_ADDRESS_DATA_KEY,\n SHIPPING_FORM_NAME,\n TERMS_AND_CONDITIONS_FORM_NAME,\n\n // Timing constants\n ADDRESS_INPUT_DEBOUNCE_TIME,\n DEBOUNCE_TIME,\n\n // Block and styling constants\n CHECKOUT_BLOCK,\n CHECKOUT_ERROR_CLASS,\n CHECKOUT_HEADER_CLASS,\n ORDER_CONFIRMATION_BLOCK,\n\n // Default values\n USER_TOKEN_COOKIE_NAME,\n};\n",
|
|
335
|
+
"containers.js": "/* eslint-disable max-len */\n/* eslint-disable import/no-unresolved */\n/* eslint-disable no-unused-vars */\n/* eslint-disable no-shadow */\n/* eslint-disable no-use-before-define */\n/* eslint-disable prefer-const */\n\n// Checkout Dropin\nimport * as checkoutApi from '@dropins/storefront-checkout/api.js';\nimport BillToShippingAddress from '@dropins/storefront-checkout/containers/BillToShippingAddress.js';\nimport EstimateShipping from '@dropins/storefront-checkout/containers/EstimateShipping.js';\nimport LoginForm from '@dropins/storefront-checkout/containers/LoginForm.js';\nimport MergedCartBanner from '@dropins/storefront-checkout/containers/MergedCartBanner.js';\nimport OutOfStock from '@dropins/storefront-checkout/containers/OutOfStock.js';\nimport PaymentMethods from '@dropins/storefront-checkout/containers/PaymentMethods.js';\nimport PlaceOrder from '@dropins/storefront-checkout/containers/PlaceOrder.js';\nimport ServerError from '@dropins/storefront-checkout/containers/ServerError.js';\nimport ShippingMethods from '@dropins/storefront-checkout/containers/ShippingMethods.js';\nimport TermsAndConditions from '@dropins/storefront-checkout/containers/TermsAndConditions.js';\nimport { render as CheckoutProvider } from '@dropins/storefront-checkout/render.js';\n\n// Auth Dropin\nimport * as authApi from '@dropins/storefront-auth/api.js';\nimport AuthCombine from '@dropins/storefront-auth/containers/AuthCombine.js';\nimport SignUp from '@dropins/storefront-auth/containers/SignUp.js';\nimport { render as AuthProvider } from '@dropins/storefront-auth/render.js';\n\n// Account Dropin\nimport Addresses from '@dropins/storefront-account/containers/Addresses.js';\nimport AddressForm from '@dropins/storefront-account/containers/AddressForm.js';\nimport { render as AccountProvider } from '@dropins/storefront-account/render.js';\n\n// Cart Dropin\nimport * as cartApi from '@dropins/storefront-cart/api.js';\nimport CartSummaryList from '@dropins/storefront-cart/containers/CartSummaryList.js';\nimport Coupons from '@dropins/storefront-cart/containers/Coupons.js';\nimport GiftCards from '@dropins/storefront-cart/containers/GiftCards.js';\nimport GiftOptions from '@dropins/storefront-cart/containers/GiftOptions.js';\nimport OrderSummary from '@dropins/storefront-cart/containers/OrderSummary.js';\nimport { render as CartProvider } from '@dropins/storefront-cart/render.js';\n\n// Payment Services Dropin\nimport { PaymentMethodCode } from '@dropins/storefront-payment-services/api.js';\nimport CreditCard from '@dropins/storefront-payment-services/containers/CreditCard.js';\nimport { render as PaymentServices } from '@dropins/storefront-payment-services/render.js';\n\n// Order Dropin\nimport CustomerDetails from '@dropins/storefront-order/containers/CustomerDetails.js';\nimport OrderCostSummary from '@dropins/storefront-order/containers/OrderCostSummary.js';\nimport OrderHeader from '@dropins/storefront-order/containers/OrderHeader.js';\nimport OrderProductList from '@dropins/storefront-order/containers/OrderProductList.js';\nimport OrderStatus from '@dropins/storefront-order/containers/OrderStatus.js';\nimport ShippingStatus from '@dropins/storefront-order/containers/ShippingStatus.js';\nimport { render as OrderProvider } from '@dropins/storefront-order/render.js';\n\n// Tools\nimport { debounce, getCookie } from '@dropins/tools/lib.js';\nimport { tryRenderAemAssetsImage } from '@dropins/tools/lib/aem/assets.js';\nimport { getConfigValue } from '@dropins/tools/lib/aem/configs.js';\nimport {\n Button,\n Header,\n provider as UI,\n} from '@dropins/tools/components.js';\nimport { events } from '@dropins/tools/event-bus.js';\n\n// Checkout Dropin Libraries\nimport {\n estimateShippingCost,\n getCartAddress,\n setAddressOnCart,\n} from '@dropins/storefront-checkout/lib/utils.js';\n\n// Utils\nimport {\n showModal,\n swatchImageSlot,\n} from './utils.js';\n\n// External dependencies\nimport {\n authPrivacyPolicyConsentSlot,\n fetchPlaceholders,\n rootLink,\n} from '../../scripts/commerce.js';\n\n// Constants\nimport {\n ADDRESS_INPUT_DEBOUNCE_TIME,\n BILLING_ADDRESS_DATA_KEY,\n BILLING_FORM_NAME,\n CHECKOUT_ERROR_CLASS,\n CHECKOUT_HEADER_CLASS,\n DEBOUNCE_TIME,\n LOGIN_FORM_NAME,\n SHIPPING_ADDRESS_DATA_KEY,\n SHIPPING_FORM_NAME,\n USER_TOKEN_COOKIE_NAME,\n} from './constants.js';\n\n/**\n * Container IDs for registry management\n * @enum {string}\n */\nexport const CONTAINERS = Object.freeze({\n // Static containers (rendered in Promise.all)\n MERGED_CART_BANNER: 'mergedCartBanner',\n CHECKOUT_HEADER: 'checkoutHeader',\n SERVER_ERROR: 'serverError',\n OUT_OF_STOCK: 'outOfStock',\n LOGIN_FORM: 'loginForm',\n SHIPPING_ADDRESS_FORM_SKELETON: 'shippingAddressFormSkeleton',\n BILL_TO_SHIPPING_ADDRESS: 'billToShippingAddress',\n SHIPPING_METHODS: 'shippingMethods',\n PAYMENT_METHODS: 'paymentMethods',\n BILLING_ADDRESS_FORM_SKELETON: 'billingAddressFormSkeleton',\n ORDER_SUMMARY: 'orderSummary',\n CART_SUMMARY_LIST: 'cartSummaryList',\n TERMS_AND_CONDITIONS: 'termsAndConditions',\n PLACE_ORDER_BUTTON: 'placeOrderButton',\n GIFT_OPTIONS: 'giftOptions',\n CUSTOMER_SHIPPING_ADDRESSES: 'customerShippingAddresses',\n CUSTOMER_BILLING_ADDRESSES: 'customerBillingAddresses',\n\n // Dynamic containers (conditional rendering)\n SHIPPING_ADDRESS_FORM: 'shippingAddressForm',\n BILLING_ADDRESS_FORM: 'billingAddressForm',\n\n // Order confirmation containers\n ORDER_HEADER: 'orderHeader',\n ORDER_STATUS: 'orderStatus',\n SHIPPING_STATUS: 'shippingStatus',\n CUSTOMER_DETAILS: 'customerDetails',\n ORDER_COST_SUMMARY: 'orderCostSummary',\n ORDER_GIFT_OPTIONS: 'orderGiftOptions',\n ORDER_PRODUCT_LIST: 'orderProductList',\n ORDER_CONFIRMATION_FOOTER_BUTTON: 'orderConfirmationFooterButton',\n\n // Slot/Sub-containers (nested within other containers)\n ESTIMATE_SHIPPING: 'estimateShipping',\n CART_COUPONS: 'cartCoupons',\n GIFT_CARDS: 'giftCards',\n CART_GIFT_OPTIONS: 'cartGiftOptions',\n});\n\n/**\n * A Map to store the API of rendered containers.\n * The key is a unique string ID, and the value is the containers's API object.\n * (e.g., { setProps: (props) => {...}, remove: () => {...} })\n */\nconst registry = new Map();\n\n/**\n * Checks if a container with the given ID has been rendered.\n * This is used to prevent multiple instances of the same container from being rendered.\n * @param {string} id - The unique ID of the container to check.\n * @returns {boolean} - Returns true if the container has been rendered, false otherwise.\n */\nexport const hasContainer = (id) => registry.has(id);\n\n/**\n * Helper to get a container from the registry or render and register it if not present.\n * @async\n * @param {string} id - Unique identifier for the container.\n * @param {Function} renderFn - Async function that renders the container.\n * @returns {Promise<Object>} - The rendered container API.\n */\nconst renderContainer = async (id, renderFn) => {\n if (registry.has(id)) {\n return registry.get(id);\n }\n\n try {\n const container = await renderFn();\n registry.set(id, container);\n return container;\n } catch (error) {\n console.error(`Error rendering container ${id}:`, error);\n throw error;\n }\n};\n\n/**\n * Unmounts and removes a container from the registry.\n * This function checks if the container is registered, removes it from the DOM,\n * and deletes its reference from the registry.\n * @param {string} id - The unique ID of the container to unmount.\n * @return {void}\n */\nexport const unmountContainer = (id) => {\n if (!registry.has(id)) {\n return;\n }\n\n const containerApi = registry.get(id);\n containerApi.remove();\n registry.delete(id);\n};\n\n/**\n * Renders the merged cart banner notification for authenticated users\n * @param {HTMLElement} container - DOM element to render the banner in\n * @returns {Promise<Object>} - The rendered merged cart banner component\n */\nexport const renderMergedCartBanner = async (container) => renderContainer(\n CONTAINERS.MERGED_CART_BANNER,\n async () => CheckoutProvider.render(MergedCartBanner)(container),\n);\n\n/**\n * Renders the checkout page header with title and styling\n * @param {HTMLElement} container - DOM element to render the header in\n * @param {string} title - The title to display in the header\n * @returns {Promise<Object>} - The rendered checkout header component\n */\nexport const renderCheckoutHeader = async (container, title) => renderContainer(\n CONTAINERS.CHECKOUT_HEADER,\n async () => UI.render(Header, {\n className: CHECKOUT_HEADER_CLASS,\n divider: true,\n level: 1,\n size: 'large',\n title,\n })(container),\n);\n\n/**\n * Renders server error handling with retry functionality and error state management\n * @param {HTMLElement} container - DOM element to render the error component in\n * @param {HTMLElement} contentElement - Main content element to add error styling to\n * @returns {Promise<Object>} - The rendered server error component\n */\nexport const renderServerError = async (container, contentElement) => renderContainer(\n CONTAINERS.SERVER_ERROR,\n async () => CheckoutProvider.render(ServerError, {\n autoScroll: true,\n onRetry: () => {\n contentElement.classList.remove(CHECKOUT_ERROR_CLASS);\n },\n onServerError: () => {\n contentElement.classList.add(CHECKOUT_ERROR_CLASS);\n },\n })(container),\n);\n\n/**\n * Renders out of stock handling with cart navigation and product update options\n * @param {HTMLElement} container - DOM element to render the component in\n * @returns {Promise<Object>} - The rendered out-of-stock component\n */\nexport const renderOutOfStock = async (container) => renderContainer(\n CONTAINERS.OUT_OF_STOCK,\n async () => CheckoutProvider.render(OutOfStock, {\n routeCart: () => rootLink('/cart'),\n onCartProductsUpdate: (items) => {\n cartApi.updateProductsFromCart(items).catch(console.error);\n },\n })(container),\n);\n\n/**\n * Renders the login form for guest checkout with authentication options\n * Uses the existing 'authenticated' event system for decoupled communication\n * @param {HTMLElement} container - DOM element to render the login form in\n * @returns {Promise<Object>} - The rendered login form component\n */\nexport const renderLoginForm = async (container) => renderContainer(\n CONTAINERS.LOGIN_FORM,\n async () => CheckoutProvider.render(LoginForm, {\n name: LOGIN_FORM_NAME,\n onSignInClick: async (initialEmailValue) => {\n const signInForm = document.createElement('div');\n\n AuthProvider.render(AuthCombine, {\n signInFormConfig: {\n renderSignUpLink: true,\n initialEmailValue,\n // No onSuccessCallback needed - the 'authenticated' event will be fired automatically\n },\n signUpFormConfig: {\n slots: {\n ...authPrivacyPolicyConsentSlot,\n },\n },\n resetPasswordFormConfig: {},\n })(signInForm);\n\n await showModal(signInForm);\n },\n onSignOutClick: () => {\n authApi.revokeCustomerToken();\n },\n })(container),\n);\n\n/**\n * Renders the shipping address form skeleton (initial placeholder)\n * @param {HTMLElement} container - DOM element to render the form in\n * @returns {Promise<Object>} - The rendered shipping address form skeleton\n */\nexport const renderShippingAddressFormSkeleton = async (container) => renderContainer(\n CONTAINERS.SHIPPING_ADDRESS_FORM_SKELETON,\n async () => AccountProvider.render(AddressForm, {\n fieldIdPrefix: 'shipping',\n isOpen: true,\n showFormLoader: true,\n })(container),\n);\n\n/**\n * Renders the billing address form skeleton (initial placeholder)\n * @param {HTMLElement} container - DOM element to render the form in\n * @returns {Promise<Object>} - The rendered billing address form skeleton\n */\nexport const renderBillingAddressFormSkeleton = async (container) => renderContainer(\n CONTAINERS.BILLING_ADDRESS_FORM_SKELETON,\n async () => AccountProvider.render(AddressForm, {\n fieldIdPrefix: 'billing',\n isOpen: true,\n showFormLoader: true,\n })(container),\n);\n\n/**\n * Renders checkbox to set billing address same as shipping address - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render the checkbox in\n * @returns {Promise<Object>} - The rendered bill to shipping address component\n */\nexport const renderBillToShippingAddress = async (container) => renderContainer(\n CONTAINERS.BILL_TO_SHIPPING_ADDRESS,\n async () => {\n const setBillingAddressOnCart = setAddressOnCart({ type: 'billing' });\n\n return CheckoutProvider.render(BillToShippingAddress, {\n onChange: (checked) => {\n const billingFormValues = events.lastPayload('checkout/addresses/billing');\n\n if (!checked && billingFormValues) {\n setBillingAddressOnCart(billingFormValues);\n }\n },\n })(container);\n },\n);\n\n/**\n * Renders available shipping methods with selection interface\n * @param {HTMLElement} container - DOM element to render shipping methods in\n * @returns {Promise<Object>} - The rendered shipping methods component\n */\nexport const renderShippingMethods = async (container) => renderContainer(\n CONTAINERS.SHIPPING_METHODS,\n async () => CheckoutProvider.render(ShippingMethods)(container),\n);\n\n/**\n * Renders payment methods with credit card integration - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render payment methods in\n * @param {Object} creditCardFormRef - React-style ref for credit card form\n * @returns {Promise<Object>} - The rendered payment methods component\n */\nexport const renderPaymentMethods = async (container, creditCardFormRef) => renderContainer(\n CONTAINERS.PAYMENT_METHODS,\n async () => {\n // Retrieve constants internally to minimize parameters\n const commerceCoreEndpoint = getConfigValue('commerce-core-endpoint') || getConfigValue('commerce-endpoint');\n const getUserTokenCookie = () => getCookie(USER_TOKEN_COOKIE_NAME);\n\n return CheckoutProvider.render(PaymentMethods, {\n slots: {\n Methods: {\n [PaymentMethodCode.CREDIT_CARD]: {\n render: (ctx) => {\n const $creditCard = document.createElement('div');\n\n PaymentServices.render(CreditCard, {\n apiUrl: commerceCoreEndpoint,\n getCustomerToken: getUserTokenCookie,\n getCartId: () => ctx.cartId,\n creditCardFormRef,\n })($creditCard);\n\n ctx.replaceHTML($creditCard);\n },\n },\n [PaymentMethodCode.SMART_BUTTONS]: {\n enabled: false,\n },\n [PaymentMethodCode.APPLE_PAY]: {\n enabled: false,\n },\n [PaymentMethodCode.GOOGLE_PAY]: {\n enabled: false,\n },\n [PaymentMethodCode.VAULT]: {\n enabled: false,\n },\n [PaymentMethodCode.FASTLANE]: {\n enabled: false,\n },\n },\n },\n })(container);\n },\n);\n\n/**\n * Renders terms and conditions with agreement slots and manual consent mode\n * @param {HTMLElement} container - DOM element to render the terms in\n * @returns {Promise<Object>} - The rendered terms and conditions component\n */\nexport const renderTermsAndConditions = async (container) => renderContainer(\n CONTAINERS.TERMS_AND_CONDITIONS,\n async () => CheckoutProvider.render(TermsAndConditions, {\n slots: {\n Agreements: (ctx) => {\n ctx.appendAgreement(() => ({\n name: 'default',\n mode: 'manual',\n translationId: 'Checkout.TermsAndConditions.label',\n }));\n },\n },\n })(container),\n);\n\n/**\n * Renders estimate shipping form for order summary slot\n * @param {HTMLElement} ctx - The slot context element\n * @returns {void}\n */\nexport const renderEstimateShipping = (ctx) => {\n const estimateShippingForm = document.createElement('div');\n CheckoutProvider.render(EstimateShipping)(estimateShippingForm);\n ctx.appendChild(estimateShippingForm);\n};\n\n/**\n * Renders cart coupons for order summary slot\n * @param {HTMLElement} ctx - The slot context element\n * @returns {void}\n */\nexport const renderCartCoupons = (ctx) => {\n const coupons = document.createElement('div');\n CartProvider.render(Coupons)(coupons);\n ctx.appendChild(coupons);\n};\n\n/**\n * Renders gift cards for order summary slot\n * @param {HTMLElement} ctx - The slot context element\n * @returns {void}\n */\nexport const renderGiftCards = (ctx) => {\n const giftCards = document.createElement('div');\n CartProvider.render(GiftCards)(giftCards);\n ctx.appendChild(giftCards);\n};\n\n/**\n * Renders gift options for cart summary list footer slot\n * @param {HTMLElement} ctx - The slot context element\n * @returns {void}\n */\nexport const renderCartGiftOptions = (ctx) => {\n const giftOptions = document.createElement('div');\n\n CartProvider.render(GiftOptions, {\n item: ctx.item,\n view: 'product',\n dataSource: 'cart',\n isEditable: false,\n handleItemsLoading: ctx.handleItemsLoading,\n handleItemsError: ctx.handleItemsError,\n onItemUpdate: ctx.onItemUpdate,\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(giftOptions);\n\n ctx.appendChild(giftOptions);\n};\n\n// ============================================================================\n// SUMMARY CONTAINERS\n// ============================================================================\n\n/**\n * Renders order summary with estimate shipping, coupons, and gift cards slots\n * @param {HTMLElement} container - DOM element to render order summary in\n * @returns {Promise<Object>} - The rendered order summary component\n */\nexport const renderOrderSummary = async (container) => renderContainer(\n CONTAINERS.ORDER_SUMMARY,\n async () => CartProvider.render(OrderSummary, {\n slots: {\n EstimateShipping: renderEstimateShipping,\n Coupons: renderCartCoupons,\n GiftCards: renderGiftCards,\n },\n })(container),\n);\n\n/**\n * Creates the cart summary heading with item count and edit link\n * @param {Object} headingCtx - The heading context with count and DOM methods\n * @returns {void}\n */\nconst createCartSummaryHeading = (headingCtx, placeholders) => {\n const title = placeholders?.Checkout?.Summary?.heading;\n\n // Create main heading container\n const heading = document.createElement('div');\n heading.classList.add('cart-summary-list__heading');\n\n // Create heading text element\n const headingText = document.createElement('div');\n headingText.classList.add('cart-summary-list__heading-text');\n\n // Create edit cart link\n const editCartLink = document.createElement('a');\n editCartLink.classList.add('cart-summary-list__edit');\n editCartLink.href = rootLink('/cart');\n editCartLink.rel = 'noreferrer';\n editCartLink.innerText = placeholders?.Checkout?.Summary?.Edit;\n\n // Helper function to update count text\n const updateCountText = (count) => {\n headingText.innerText = title?.replace(\n '({count})',\n count ? `(${count})` : '',\n );\n };\n\n // Set initial count\n updateCountText(headingCtx.count);\n\n // Assemble heading\n heading.appendChild(headingText);\n heading.appendChild(editCartLink);\n headingCtx.appendChild(heading);\n\n // Listen for count changes\n headingCtx.onChange((nextCtx) => {\n updateCountText(nextCtx.count);\n });\n};\n\n/**\n * Renders cart summary list with custom heading, thumbnail and gift options slots\n * @param {HTMLElement} container - DOM element to render cart summary list in\n * @returns {Promise<Object>} - The rendered cart summary list component\n */\nexport const renderCartSummaryList = async (container) => renderContainer(\n CONTAINERS.CART_SUMMARY_LIST,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n return CartProvider.render(CartSummaryList, {\n variant: 'secondary',\n slots: {\n Heading: (headingCtx) => createCartSummaryHeading(headingCtx, placeholders),\n Thumbnail: (ctx) => {\n const { item, defaultImageProps } = ctx;\n tryRenderAemAssetsImage(ctx, {\n alias: item.sku,\n imageProps: defaultImageProps,\n params: {\n width: defaultImageProps.width,\n height: defaultImageProps.height,\n },\n });\n },\n Footer: renderCartGiftOptions,\n },\n })(container);\n },\n);\n\n/**\n * Renders place order button with handler functions - follows multi-step pattern\n * @param {HTMLElement} container - DOM element to render the place order button in\n * @param {Object} options - Configuration object with handler functions\n * @param {Function} options.handleValidation - Validation handler function\n * @param {Function} options.handlePlaceOrder - Place order handler function\n * @returns {Promise<Object>} - The rendered place order component\n */\nexport const renderPlaceOrder = async (container, options = {}) => renderContainer(\n CONTAINERS.PLACE_ORDER_BUTTON,\n async () => CheckoutProvider.render(PlaceOrder, {\n handleValidation: options.handleValidation,\n handlePlaceOrder: options.handlePlaceOrder,\n })(container),\n);\n\n/**\n * Renders customer shipping addresses selector/form for authenticated users - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render shipping addresses in\n * @param {Object} formRef - React-style ref for form reference\n * @param {Object} data - Cart data containing shipping address information\n * @returns {Promise<Object>} - The rendered customer shipping addresses component\n */\nexport const renderCustomerShippingAddresses = async (container, formRef, data) => renderContainer(\n CONTAINERS.CUSTOMER_SHIPPING_ADDRESSES,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n const cartShippingAddress = getCartAddress(data, 'shipping');\n\n const shippingAddressId = cartShippingAddress\n ? cartShippingAddress?.id ?? 0\n : undefined;\n\n const shippingAddressCache = sessionStorage.getItem(SHIPPING_ADDRESS_DATA_KEY);\n\n // Clear persisted shipping address if cart has a shipping address\n if (cartShippingAddress && shippingAddressCache) {\n sessionStorage.removeItem(SHIPPING_ADDRESS_DATA_KEY);\n }\n\n const storeConfig = checkoutApi.getStoreConfigCache();\n\n const inputsDefaultValueSet = cartShippingAddress && cartShippingAddress.id === undefined\n ? cartShippingAddress\n : { countryCode: storeConfig.defaultCountry };\n\n const hasCartShippingAddress = Boolean(data.shippingAddresses?.[0]);\n let isFirstRenderShipping = true;\n\n const setShippingAddressOnCart = setAddressOnCart({\n type: 'shipping',\n debounceMs: DEBOUNCE_TIME,\n });\n\n const estimateShippingCostOnCart = estimateShippingCost({\n api: checkoutApi.estimateShippingMethods,\n debounceMs: DEBOUNCE_TIME,\n });\n\n const notifyShippingValues = debounce((values) => {\n events.emit('checkout/addresses/shipping', values);\n }, ADDRESS_INPUT_DEBOUNCE_TIME);\n\n return AccountProvider.render(Addresses, {\n addressFormTitle: placeholders?.Checkout?.Addresses?.shippingAddressTitle,\n defaultSelectAddressId: shippingAddressId,\n fieldIdPrefix: 'shipping',\n formName: SHIPPING_FORM_NAME,\n forwardFormRef: formRef,\n inputsDefaultValueSet,\n minifiedView: false,\n onAddressData: (values) => {\n const canSetShippingAddressOnCart = !isFirstRenderShipping || !hasCartShippingAddress;\n if (canSetShippingAddressOnCart) setShippingAddressOnCart(values);\n if (!hasCartShippingAddress) estimateShippingCostOnCart(values);\n if (isFirstRenderShipping) isFirstRenderShipping = false;\n notifyShippingValues(values);\n },\n selectable: true,\n selectShipping: true,\n showBillingCheckBox: false,\n showSaveCheckBox: true,\n showShippingCheckBox: false,\n title: placeholders?.Checkout?.Addresses?.shippingAddressTitle,\n })(container);\n },\n);\n\n/**\n * Renders customer billing addresses selector/form for authenticated users - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render billing addresses in\n * @param {Object} formRef - React-style ref for form reference\n * @param {Object} data - Cart data containing billing address information\n * @returns {Promise<Object>} - The rendered customer billing addresses component\n */\nexport const renderCustomerBillingAddresses = async (container, formRef, data) => renderContainer(\n CONTAINERS.CUSTOMER_BILLING_ADDRESSES,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n const cartBillingAddress = getCartAddress(data, 'billing');\n\n const billingAddressId = cartBillingAddress\n ? cartBillingAddress?.id ?? 0\n : undefined;\n\n const billingAddressCache = sessionStorage.getItem(BILLING_ADDRESS_DATA_KEY);\n\n // Clear persisted billing address if cart has a billing address\n if (cartBillingAddress && billingAddressCache) {\n sessionStorage.removeItem(BILLING_ADDRESS_DATA_KEY);\n }\n\n const storeConfig = checkoutApi.getStoreConfigCache();\n\n const inputsDefaultValueSet = cartBillingAddress && cartBillingAddress.id === undefined\n ? cartBillingAddress\n : { countryCode: storeConfig.defaultCountry };\n\n const hasCartBillingAddress = Boolean(data.billingAddress);\n let isFirstRenderBilling = true;\n\n const setBillingAddressOnCart = setAddressOnCart({\n type: 'billing',\n debounceMs: DEBOUNCE_TIME,\n });\n\n const notifyBillingValues = debounce((values) => {\n events.emit('checkout/addresses/billing', values);\n }, ADDRESS_INPUT_DEBOUNCE_TIME);\n\n return AccountProvider.render(Addresses, {\n addressFormTitle: placeholders?.Checkout?.Addresses?.billToNewAddress,\n defaultSelectAddressId: billingAddressId,\n formName: BILLING_FORM_NAME,\n forwardFormRef: formRef,\n inputsDefaultValueSet,\n minifiedView: false,\n onAddressData: (values) => {\n const canSetBillingAddressOnCart = !isFirstRenderBilling || !hasCartBillingAddress;\n if (canSetBillingAddressOnCart) setBillingAddressOnCart(values);\n if (isFirstRenderBilling) isFirstRenderBilling = false;\n notifyBillingValues(values);\n },\n selectable: true,\n selectBilling: true,\n showBillingCheckBox: false,\n showSaveCheckBox: true,\n showShippingCheckBox: false,\n title: placeholders?.Checkout?.Addresses?.billingAddressTitle,\n })(container);\n },\n);\n\n/**\n * Renders address form for guest users (shipping or billing) - original regular checkout functionality\n * @param {HTMLElement} container - DOM element to render address form in\n * @param {Object} formRef - React-style ref for form reference\n * @param {Object} data - Cart data containing address information\n * @param {string} addressType - Type of address form ('shipping' or 'billing')\n * @returns {Promise<Object>} - The rendered address form component\n */\nexport const renderAddressForm = async (container, formRef, data, addressType) => {\n const isShipping = addressType === 'shipping';\n const containerKey = isShipping ? CONTAINERS.SHIPPING_ADDRESS_FORM : CONTAINERS.BILLING_ADDRESS_FORM;\n\n return renderContainer(\n containerKey,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n // Get address type specific configurations\n const cartAddress = getCartAddress(data, addressType);\n const addressDataKey = isShipping ? SHIPPING_ADDRESS_DATA_KEY : BILLING_ADDRESS_DATA_KEY;\n const addressCache = sessionStorage.getItem(addressDataKey);\n\n // Clear persisted address if cart has an address\n if (cartAddress && addressCache) {\n sessionStorage.removeItem(addressDataKey);\n }\n\n let isFirstRender = true;\n const hasCartAddress = Boolean(isShipping ? data.shippingAddresses?.[0] : data.billingAddress);\n\n const setAddressOnCartFn = setAddressOnCart({\n type: addressType,\n debounceMs: DEBOUNCE_TIME,\n });\n\n // Create shipping cost estimator (only for shipping addresses)\n const estimateShippingCostOnCart = isShipping ? estimateShippingCost({\n api: checkoutApi.estimateShippingMethods,\n debounceMs: DEBOUNCE_TIME,\n }) : null;\n\n const notifyValues = debounce((values) => {\n const eventType = isShipping ? 'checkout/addresses/shipping' : 'checkout/addresses/billing';\n events.emit(eventType, values);\n }, ADDRESS_INPUT_DEBOUNCE_TIME);\n\n const storeConfig = checkoutApi.getStoreConfigCache();\n\n // Address type specific configurations\n const formName = isShipping ? SHIPPING_FORM_NAME : BILLING_FORM_NAME;\n const addressTitle = isShipping ? 'Shipping address' : 'Billing address';\n const className = isShipping\n ? 'checkout-shipping-form__address-form'\n : 'checkout-billing-form__address-form';\n\n return AccountProvider.render(AddressForm, {\n addressesFormTitle: addressTitle,\n className,\n fieldIdPrefix: addressType,\n formName,\n forwardFormRef: formRef,\n hideActionFormButtons: true,\n inputsDefaultValueSet: cartAddress ?? {\n countryCode: storeConfig.defaultCountry,\n },\n isOpen: true,\n onChange: (values) => {\n const canSetAddressOnCart = !isFirstRender || !hasCartAddress;\n if (canSetAddressOnCart) setAddressOnCartFn(values);\n\n // Only estimate shipping cost for shipping addresses when no cart address exists\n if (isShipping && !hasCartAddress && estimateShippingCostOnCart) {\n estimateShippingCostOnCart(values);\n }\n\n if (isFirstRender) isFirstRender = false;\n\n notifyValues(values);\n },\n showBillingCheckBox: false,\n showFormLoader: false,\n showShippingCheckBox: false,\n })(container);\n },\n );\n};\n\n/**\n * Renders order-level gift options with swatch image integration\n * @param {HTMLElement} container - DOM element to render gift options in\n * @returns {Promise<Object>} - The rendered gift options component\n */\nexport const renderGiftOptions = async (container) => renderContainer(\n CONTAINERS.GIFT_OPTIONS,\n async () => CartProvider.render(GiftOptions, {\n view: 'order',\n dataSource: 'cart',\n isEditable: false,\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(container),\n);\n\n/**\n * Renders order confirmation header with email check and sign up integration\n * @param {HTMLElement} container - DOM element to render the order header in\n * @param {Object} options - Configuration object with handlers and order data\n * @returns {Promise<Object>} - The rendered order header component\n */\nexport const renderOrderHeader = async (container, options = {}) => renderContainer(\n CONTAINERS.ORDER_HEADER,\n async () => {\n const handleSignUpClick = async ({\n inputsDefaultValueSet,\n addressesData,\n }) => {\n const signUpForm = document.createElement('div');\n\n AuthProvider.render(SignUp, {\n inputsDefaultValueSet,\n addressesData,\n routeSignIn: () => rootLink('/customer/login'),\n routeRedirectOnEmailConfirmationClose: () => rootLink('/customer/account'),\n slots: {\n ...authPrivacyPolicyConsentSlot,\n },\n })(signUpForm);\n\n await showModal(signUpForm);\n };\n\n return OrderProvider.render(OrderHeader, {\n handleEmailAvailability: checkoutApi.isEmailAvailable,\n handleSignUpClick,\n ...options,\n })(container);\n },\n);\n\n/**\n * Renders the order status component\n * @param {HTMLElement} container - The DOM element to render the order status in\n * @returns {Promise<Object>} - The rendered order status component\n */\nexport const renderOrderStatus = async (container) => renderContainer(\n CONTAINERS.ORDER_STATUS,\n async () => OrderProvider.render(OrderStatus, { slots: { OrderActions: () => null } })(container),\n);\n\n/**\n * Renders the shipping status component\n * @param {HTMLElement} container - The DOM element to render the shipping status in\n * @returns {Promise<Object>} - The rendered shipping status component\n */\nexport const renderShippingStatus = async (container) => renderContainer(\n CONTAINERS.SHIPPING_STATUS,\n async () => OrderProvider.render(ShippingStatus)(container),\n);\n\n/**\n * Renders the customer details component\n * @param {HTMLElement} container - The DOM element to render the customer details in\n * @returns {Promise<Object>} - The rendered customer details component\n */\nexport const renderCustomerDetails = async (container) => renderContainer(\n CONTAINERS.CUSTOMER_DETAILS,\n async () => OrderProvider.render(CustomerDetails)(container),\n);\n\n/**\n * Renders the order cost summary component\n * @param {HTMLElement} container - The DOM element to render the order cost summary in\n * @returns {Promise<Object>} - The rendered order cost summary component\n */\nexport const renderOrderCostSummary = async (container) => renderContainer(\n CONTAINERS.ORDER_COST_SUMMARY,\n async () => OrderProvider.render(OrderCostSummary)(container),\n);\n\n/**\n * Renders the order product list component with image slots and gift options\n * @param {HTMLElement} container - The DOM element to render the order product list in\n * @returns {Promise<Object>} - The rendered order product list component\n */\nexport const renderOrderProductList = async (container) => renderContainer(\n CONTAINERS.ORDER_PRODUCT_LIST,\n async () => OrderProvider.render(OrderProductList, {\n slots: {\n Footer: (ctx) => {\n const giftOptions = document.createElement('div');\n CartProvider.render(GiftOptions, {\n item: ctx.item,\n view: 'product',\n dataSource: 'order',\n isEditable: false,\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(giftOptions);\n ctx.appendChild(giftOptions);\n },\n CartSummaryItemImage: (ctx) => {\n const { data, defaultImageProps } = ctx;\n tryRenderAemAssetsImage(ctx, {\n alias: data.product.sku,\n imageProps: defaultImageProps,\n params: {\n width: defaultImageProps.width,\n height: defaultImageProps.height,\n },\n });\n },\n },\n })(container),\n);\n\n/**\n * Renders order-level gift options for order confirmation\n * @param {HTMLElement} container - DOM element to render order gift options in\n * @returns {Promise<Object>} - The rendered order gift options component\n */\nexport const renderOrderGiftOptions = async (container) => renderContainer(\n CONTAINERS.ORDER_GIFT_OPTIONS,\n async () => CartProvider.render(GiftOptions, {\n view: 'order',\n dataSource: 'order',\n isEditable: false,\n readOnlyFormOrderView: 'secondary',\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(container),\n);\n\n/**\n * Renders the continue shopping button for order confirmation footer\n * @param {HTMLElement} container - DOM element to render the button in\n * @returns {Promise<Object>} - The rendered continue shopping button component\n */\nexport const renderOrderConfirmationFooterButton = async (container) => renderContainer(\n CONTAINERS.ORDER_CONFIRMATION_FOOTER_BUTTON,\n async () => UI.render(Button, {\n children: 'Continue shopping',\n 'data-testid': 'order-confirmation-footer__continue-button',\n className: 'order-confirmation-footer__continue-button',\n size: 'medium',\n variant: 'primary',\n type: 'submit',\n href: rootLink('/'),\n })(container),\n);\n",
|
|
336
|
+
"fragments.js": "// eslint-disable-next-line import/no-unresolved\nimport { createFragment } from '@dropins/storefront-checkout/lib/utils.js';\n\nimport {\n CHECKOUT_BLOCK,\n ORDER_CONFIRMATION_BLOCK,\n} from './constants.js';\n\n/**\n * A frozen, nested object of CSS selectors\n * @readonly\n */\nexport const selectors = Object.freeze({\n checkout: {\n content: '.checkout__content',\n loader: '.checkout__loader',\n mergedCartBanner: '.checkout__merged-cart-banner',\n heading: '.checkout__heading',\n serverError: '.checkout__server-error',\n outOfStock: '.checkout__out-of-stock',\n login: '.checkout__login',\n shippingForm: '.checkout__shipping-form',\n billToShipping: '.checkout__bill-to-shipping',\n delivery: '.checkout__delivery',\n paymentMethods: '.checkout__payment-methods',\n billingForm: '.checkout__billing-form',\n orderSummary: '.checkout__order-summary',\n cartSummary: '.checkout__cart-summary',\n placeOrder: '.checkout__place-order',\n giftOptions: '.checkout__gift-options',\n termsAndConditions: '.checkout__terms-and-conditions',\n main: '.checkout__main',\n aside: '.checkout__aside',\n },\n orderConfirmation: {\n header: '.order-confirmation__header',\n orderStatus: '.order-confirmation__order-status',\n shippingStatus: '.order-confirmation__shipping-status',\n customerDetails: '.order-confirmation__customer-details',\n orderCostSummary: '.order-confirmation__order-cost-summary',\n giftOptions: '.order-confirmation__gift-options',\n orderProductList: '.order-confirmation__order-product-list',\n footer: '.order-confirmation__footer',\n continueButton: '.order-confirmation-footer__continue-button',\n contactSupportLink: '.order-confirmation-footer__contact-support-link',\n },\n});\n\n// =============================================================================\n// CHECKOUT\n// =============================================================================\n\n/**\n * Creates the main checkout fragment with all checkout blocks.\n * @returns {DocumentFragment} The complete checkout fragment.\n */\nexport function createCheckoutFragment() {\n return createFragment(`\n <div class=\"checkout__wrapper\">\n <div class=\"checkout__loader\"></div>\n <div class=\"checkout__content\">\n <div class=\"checkout__merged-cart-banner\"></div>\n <div class=\"checkout__main\">\n <div class=\"checkout__heading ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__server-error ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__out-of-stock ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__login ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__shipping-form ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__bill-to-shipping ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__delivery ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__payment-methods ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__billing-form ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__terms-and-conditions ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__place-order ${CHECKOUT_BLOCK}\"></div>\n </div>\n <div class=\"checkout__aside\">\n <div class=\"checkout__order-summary ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__gift-options ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__cart-summary ${CHECKOUT_BLOCK}\"></div>\n </div>\n </div>\n </div>\n `);\n}\n\n// =============================================================================\n// ORDER CONFIRMATION\n// =============================================================================\n\n/**\n * Creates the order confirmation fragment.\n * @returns {DocumentFragment} The order confirmation fragment.\n */\nexport function createOrderConfirmationFragment() {\n return createFragment(`\n <div class=\"order-confirmation\">\n <div class=\"order-confirmation__main\">\n <div class=\"order-confirmation__header ${ORDER_CONFIRMATION_BLOCK}\"></div>\n <div class=\"order-confirmation__order-status ${ORDER_CONFIRMATION_BLOCK}\"></div>\n <div class=\"order-confirmation__shipping-status ${ORDER_CONFIRMATION_BLOCK}\"></div>\n <div class=\"order-confirmation__customer-details ${ORDER_CONFIRMATION_BLOCK}\"></div>\n </div>\n <div class=\"order-confirmation__aside\">\n <div class=\"order-confirmation__order-cost-summary ${ORDER_CONFIRMATION_BLOCK}\"></div>\n <div class=\"order-confirmation__gift-options ${ORDER_CONFIRMATION_BLOCK}\"></div>\n <div class=\"order-confirmation__order-product-list ${ORDER_CONFIRMATION_BLOCK}\"></div>\n <div class=\"order-confirmation__footer ${ORDER_CONFIRMATION_BLOCK}\"></div>\n </div>\n </div>\n `);\n}\n\n/**\n * Creates the order confirmation footer content with support link.\n * @param {string} supportPath - The support page path for the contact link\n * @returns {string} The footer HTML content\n */\nexport function createOrderConfirmationFooter(supportPath) {\n return `\n <div class=\"order-confirmation-footer__continue-button\"></div>\n <div class=\"order-confirmation-footer__contact-support\">\n <p>\n Need help?\n <a\n href=\"${supportPath}\"\n rel=\"noreferrer\"\n class=\"order-confirmation-footer__contact-support-link\"\n data-testid=\"order-confirmation-footer__contact-support-link\"\n >\n Contact us\n </a>\n </p>\n </div>\n `;\n}\n",
|
|
337
|
+
"utils.js": "/* eslint-disable import/no-unresolved */\nimport { ProgressSpinner, provider as UI } from '@dropins/tools/components.js';\nimport { tryRenderAemAssetsImage } from '@dropins/tools/lib/aem/assets.js';\nimport { ORDER_DETAILS_PATH, rootLink } from '../../scripts/commerce.js';\nimport { getUserTokenCookie } from '../../scripts/initializers/index.js';\nimport createModal from '../modal/modal.js';\n\n/**\n * Displays an overlay spinner in the specified container\n * @param {Object} loaderRef - Ref object to store the spinner component\n * @param {HTMLElement} $loader - DOM element to render the spinner in\n */\nexport const displayOverlaySpinner = async (loaderRef, $loader) => {\n if (loaderRef.current) return;\n\n loaderRef.current = await UI.render(ProgressSpinner, {\n className: '.checkout__overlay-spinner',\n })($loader);\n};\n\n/**\n * Removes the overlay spinner and cleans up references\n * @param {Object} loaderRef - Ref object containing the spinner component\n * @param {HTMLElement} $loader - DOM element containing the spinner\n */\nexport const removeOverlaySpinner = (loaderRef, $loader) => {\n if (!loaderRef.current) return;\n\n loaderRef.current.remove();\n loaderRef.current = null;\n $loader.innerHTML = '';\n};\n\n// Modal state management\nlet modal;\n\n/**\n * Shows a modal with the specified content\n * @param {HTMLElement} content - DOM element to display in the modal\n */\nexport const showModal = async (content) => {\n modal = await createModal([content]);\n modal.showModal();\n};\n\n/**\n * Removes the currently displayed modal and cleans up references\n */\nexport const removeModal = () => {\n if (!modal) return;\n modal.removeModal();\n modal = null;\n};\n\n/**\n * Renders AEM asset images for gift option swatches\n * @param {Object} ctx - The context object containing imageSwatchContext and defaultImageProps\n */\nexport function swatchImageSlot(ctx) {\n const { imageSwatchContext, defaultImageProps } = ctx;\n tryRenderAemAssetsImage(ctx, {\n alias: imageSwatchContext.label,\n imageProps: defaultImageProps,\n wrapper: document.createElement('span'),\n params: {\n width: defaultImageProps.width,\n height: defaultImageProps.height,\n },\n });\n}\n\n/**\n * Builds the order details URL based on authentication status\n * @param {Object} orderData - Order data containing number and token\n * @param {string} orderDetailsPath - Path to the order details page\n * @returns {string} The constructed order details URL\n */\nexport function buildOrderDetailsUrl(orderData, orderDetailsPath = ORDER_DETAILS_PATH) {\n const token = getUserTokenCookie();\n const orderRef = token ? orderData.number : orderData.token;\n const orderNumber = orderData.number;\n const encodedOrderRef = encodeURIComponent(orderRef);\n const encodedOrderNumber = encodeURIComponent(orderNumber);\n\n return token\n ? rootLink(`${orderDetailsPath}?orderRef=${encodedOrderRef}`)\n : rootLink(`${orderDetailsPath}?orderRef=${encodedOrderRef}&orderNumber=${encodedOrderNumber}`);\n}\n"
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
"name": "commerce-checkout-multi-step",
|
|
342
|
+
"description": "Example commerce-checkout-multi-step block for storefront-checkout",
|
|
343
|
+
"files": {
|
|
344
|
+
"commerce-checkout-multi-step.js": "/* eslint-disable import/no-unresolved */\n/* eslint-disable no-unused-vars */\n\n// Dropin Tools\nimport { events } from '@dropins/tools/event-bus.js';\n\n// Initializers\nimport '../../scripts/initializers/account.js';\nimport '../../scripts/initializers/checkout.js';\nimport '../../scripts/initializers/order.js';\n\nimport { setMetaTags } from '@dropins/storefront-checkout/lib/utils.js';\n\n// Fragments\nimport {\n createCheckoutFragment,\n} from './fragments.js';\nimport createStepsManager, { redirectToCartIfEmpty } from './steps.js';\n\n// Checkout success block import and CSS preload\nimport { preloadCheckoutSuccess } from '../commerce-checkout-success/commerce-checkout-success.js';\n\npreloadCheckoutSuccess();\n\nexport default async function decorate(block) {\n setMetaTags('Checkout');\n document.title = 'Checkout';\n\n const cartData = events.lastPayload('cart/initialized');\n redirectToCartIfEmpty(cartData);\n\n block.replaceChildren(createCheckoutFragment());\n\n const stepsManager = createStepsManager(block);\n await stepsManager.init();\n}\n",
|
|
345
|
+
"commerce-checkout-multi-step.css": "/* 5. Styles */\n/* stylelint-disable selector-class-pattern */\n\n.checkout__wrapper {\n padding-left: var(--spacing-xxbig);\n padding-right: var(--spacing-xxbig);\n}\n\n.checkout__content {\n display: grid;\n align-items: start;\n grid-template-columns: repeat(var(--grid-4-columns), 1fr);\n gap: var(--spacing-big);\n padding-top: var(--spacing-medium);\n}\n\n.checkout-header h1, .checkout__step-title h2 {\n margin: 0;\n}\n\n.checkout__merged-cart-banner {\n display: grid;\n grid-column: 1 / -1;\n align-items: start;\n grid-template-columns: auto;\n}\n\n.checkout__main {\n display: grid;\n grid-column: 1 / span 7;\n row-gap: var(--spacing-big);\n margin: var(--spacing-medium) 0;\n}\n\n.checkout__aside {\n display: grid;\n grid-column: 9 / span 4;\n row-gap: var(--spacing-xbig);\n}\n\n.checkout__step-button {\n display: grid;\n justify-content: flex-end;\n}\n\n.checkout__loader {\n align-items: center;\n background: var(--color-neutral-50);\n display: flex;\n height: 100vh;\n justify-content: center;\n left: 0;\n opacity: 0.5;\n position: fixed;\n top: 0;\n width: 100%;\n z-index: 9999;\n}\n\n.checkout__loader:empty {\n display: none;\n}\n\n/* remove margin from the heading divider */\n.checkout__heading .dropin-divider,\n.checkout__wrapper .dropin-header-container .dropin-divider {\n margin: 0;\n}\n\n/* Block dividers */\n.checkout__main .dropin-header-container {\n gap: var(--spacing-xsmall);\n}\n\n/* Server error visibility */\n.checkout__server-error {\n display: none;\n}\n\n/* Show when it contains actual error content */\n.checkout__server-error:has(.dropin-illustrated-message) {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n}\n\n/* Safari fallback: show when not empty, but this may show empty divs briefly */\n@supports not selector(:has(*)) {\n .checkout__server-error:not(:empty) {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n }\n}\n\n/* Hide empty blocks */\n.checkout__block:empty {\n display: none;\n}\n\n.checkout__shipping-address,\n.checkout__shipping-methods,\n.checkout__payment-methods,\n.checkout__billing-address {\n display: flex;\n flex-direction: column;\n row-gap: var(--spacing-big);\n padding: 0;\n}\n\n/* Hide main containers when the cart is empty or there is a server error */\n.checkout__content--error .checkout__merged-cart-banner,\n.checkout__content--error .checkout__login,\n.checkout__content--error .checkout__shipping-address,\n.checkout__content--error .checkout__shipping-form,\n.checkout__content--error .checkout__bill-to-shipping,\n.checkout__content--error .checkout__shipping-methods,\n.checkout__content--error .checkout__payment-methods,\n.checkout__content--error .checkout__billing-form,\n.checkout__content--error .checkout__billing-address,\n.checkout__content--error .checkout__place-order,\n.checkout__content--error .checkout__terms-and-conditions,\n.checkout__content--empty .checkout__merged-cart-banner,\n.checkout__content--empty .checkout__server-error,\n.checkout__content--empty .checkout__login,\n.checkout__content--empty .checkout__shipping-address,\n.checkout__content--empty .checkout__shipping-form,\n.checkout__content--empty .checkout__bill-to-shipping,\n.checkout__content--empty .checkout__shipping-methods,\n.checkout__content--empty .checkout__payment-methods,\n.checkout__content--empty .checkout__billing-form,\n.checkout__content--empty .checkout__billing-address,\n.checkout__content--empty .checkout__place-order,\n.checkout__content--empty .checkout__terms-and-conditions {\n display: none !important;\n}\n\n/* Hide out-of-stock elements when there is an error or empty cart */\n.checkout__content--error .checkout__out-of-stock,\n.checkout__content--empty .checkout__out-of-stock {\n display: none !important;\n}\n\n/* Hide aside containers when the cart is empty or there is a server error */\n.checkout__content--error .checkout__aside,\n.checkout__content--empty .checkout__aside {\n display: none;\n}\n\n.checkout__content--error,\n.checkout__content--empty {\n display: grid;\n grid-template-columns: 1fr;\n}\n\n/* Hide blocks with empty divs */\n.checkout__out-of-stock:has(> :empty),\n.checkout__merged-cart-banner:has(> :empty),\n.checkout__shipping-methods:has(> div:first-child:empty),\n.checkout__bill-to-shipping:has(> :empty),\n.checkout__gift-options:has(.cart-gift-options-view--readonly:empty) {\n display: none;\n}\n\n/* Step visibility control using shared CSS classes */\n\n/* By default, show summaries and hide content */\n.checkout__step .checkout__step-content {\n display: none;\n}\n\n.checkout__step .checkout__step-summary {\n display: grid;\n}\n\n/* When step is active, show content and hide summaries */\n.checkout__step--active .checkout__step-content {\n display: grid;\n}\n\n.checkout__step--active .checkout__step-summary {\n display: none;\n}\n\n/* Cart Summary */\n.checkout__block .cart-cart-summary-list {\n padding: var(--spacing-medium);\n}\n\n/* Order Summary Coupon */\n.dropin-accordion-section__heading {\n margin: var(--spacing-medium) auto;\n}\n\n.cart-coupons__accordion {\n margin-top: var(--spacing-xsmall);\n}\n\n/* temporary fix to hide the default cart heading */\n[data-testid='default-cart-heading'] {\n display: none;\n}\n\n.cart-summary-list__heading {\n display: flex;\n justify-content: space-between;\n}\n\n.cart-summary-list__heading-text {\n font: var(--type-headline-2-strong-font);\n letter-spacing: var(--type-headline-2-strong-letter-spacing);\n color: var(--color-neutral-800);\n}\n\n.cart-cart-summary-list__heading {\n row-gap: var(--spacing-small);\n padding-top: 0;\n}\n\n.cart-cart-summary-list__heading-text {\n font: var(--type-headline-2-strong-font);\n letter-spacing: var(--type-headline-2-strong-letter-spacing);\n color: var(--color-neutral-800);\n}\n\n.cart-summary-list__edit {\n font: var(--type-body-2-strong-font);\n letter-spacing: var(--type-body-2-strong-letter-spacing);\n}\n\n.checkout__block\n .cart-cart-summary-list\n .cart-cart-summary-list__footer-divider {\n margin: var(--spacing-small) 0;\n}\n\n/* Address form */\n.checkout__shipping-form .account-address-form-wrapper__title,\n.checkout__shipping-form .dropin-header-container__title,\n.checkout__billing-form .account-address-form-wrapper__title,\n.checkout__billing-form .dropin-header-container__title {\n font: var(--type-headline-2-default-font);\n letter-spacing: var(--type-headline-2-default-letter-spacing);\n color: var(--color-neutral-800);\n margin: 0 0 var(--spacing-medium) 0;\n}\n\n/* Order confirmation */\n.order-confirmation {\n display: grid;\n align-items: start;\n grid-template-columns: repeat(var(--grid-4-columns), 1fr);\n grid-template-areas: 'main aside';\n grid-column-gap: var(--grid-4-gutters);\n margin-bottom: var(--spacing-xbig);\n padding-top: var(--spacing-xxlarge);\n}\n\n.order-confirmation__main {\n display: grid;\n grid-row-gap: var(--spacing-xbig);\n grid-column: 1 / span 7;\n}\n\n.order-confirmation__aside {\n display: grid;\n grid-row-gap: var(--spacing-xbig);\n grid-column: 9 / span 4;\n}\n\n.order-confirmation__footer {\n display: grid;\n gap: var(--spacing-small);\n text-align: center;\n}\n\n.order-confirmation__footer p {\n margin: 0;\n}\n\n.order-confirmation__footer .order-confirmation-footer__continue-button {\n margin: 0 auto;\n text-align: center;\n display: inline-block;\n}\n\n.order-confirmation-footer__contact-support {\n font: var(--type-body-2-default-font);\n letter-spacing: var(--type-body-2-default-letter-spacing);\n color: var(--color-neutral-700);\n}\n\n.order-confirmation-footer__contact-support a {\n font: var(--type-body-2-strong-font);\n letter-spacing: var(--type-body-2-strong-letter-spacing);\n color: var(--color-brand-500);\n cursor: pointer;\n}\n\n/* Hide empty blocks */\n.order-confirmation__block:empty {\n display: none;\n}\n\n/* Direct JS Summary Styles */\n.checkout__summary {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-xsmall);\n}\n\n.checkout__summary--inline {\n align-items: flex-start;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.checkout__summary-header {\n align-items: center;\n display: flex;\n justify-content: space-between;\n}\n\n.checkout__summary-content {\n flex: 1;\n}\n\n.checkout__summary-edit {\n color: var(--color-brand-500);\n font: var(--type-body-2-default-font);\n letter-spacing: var(--type-body-2-default-letter-spacing);\n text-decoration: none;\n margin: 0;\n padding: 0;\n background: none;\n border: none;\n cursor: pointer;\n}\n\n.checkout__summary-edit:hover {\n color: var(--color-brand-700);\n text-decoration: solid underline var(--color-brand-700);\n text-underline-offset: 6px;\n}\n\n.checkout__login-form-summary-email {\n font: var(--type-body-1-default-font);\n letter-spacing: var(--type-body-1-default-letter-spacing);\n}\n\n/* Shipping Methods Summary Styles */\n.checkout__shipping-methods-summary-content {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-xsmall);\n}\n\n.checkout__shipping-methods-summary-label {\n font: var(--type-body-1-default-font);\n letter-spacing: var(--type-body-1-default-letter-spacing);\n}\n\n.checkout__shipping-methods-summary-description {\n font: var(--type-body-2-default-font);\n letter-spacing: var(--type-body-2-default-letter-spacing);\n}\n\n/* Payment Methods Summary Styles */\n.checkout__payment-methods-summary-details {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-xsmall);\n}\n\n.checkout__payment-methods-summary-label {\n font: var(--type-body-1-default-font);\n letter-spacing: var(--type-body-1-default-letter-spacing);\n}\n\n/* Address Summary Styles */\n.checkout__address-summary-content {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-xsmall);\n}\n\n.checkout__address-summary-details * {\n font: var(--type-body-1-default-font);\n letter-spacing: var(--type-body-1-default-letter-spacing);\n}\n\n@media only screen and (width: 768px) {\n .order-confirmation {\n grid-template-columns: repeat(var(--grid-1-columns), 1fr);\n padding-top: 0;\n }\n\n .order-confirmation__main,\n .order-confirmation__aside {\n grid-row-gap: var(--spacing-medium);\n }\n\n .order-confirmation > div {\n grid-column: 1 / span 4;\n }\n\n .order-confirmation__block .dropin-card {\n border: 0;\n }\n\n .checkout__wrapper {\n padding-left: 1.5rem;\n padding-right: 1.5rem;\n }\n\n .checkout__content {\n grid-template-columns: 1fr;\n }\n\n .checkout__main,\n .checkout__aside {\n display: contents;\n }\n\n .checkout__block {\n order: 3;\n }\n\n .checkout__header {\n order: 1;\n }\n\n .checkout__cart-summary {\n order: 2;\n }\n\n .checkout__place-order {\n order: 4;\n margin-top: calc(-1 * var(--spacing-big));\n }\n}\n",
|
|
346
|
+
"README.md": "# Commerce Checkout Multi-step\n\nThis guide will walk you through the steps to extend the Checkout to implement a multi-step checkout using the available Storefront Dropin containers.\n\n## Overview\n\nThe multi-step checkout divides the checkout process into distinct sections that are displayed sequentially, with support for both guest and logged-in users and automatic virtual product detection:\n\n### Standard Physical Product Flow\n\n1. **Login and Shipping Address**\n - **Guest users**: Enter email address for order notifications and create shipping address\n - **Logged-in users**: Automatically detected, can select from saved addresses or create new ones\n - **Validation**: Email format validation, complete shipping address required\n - **API calls**: `setGuestEmailOnCart()`, `setShippingAddress()`\n\n2. **Shipping Methods**\n - **User action**: Select preferred delivery method from available options (Standard, Express, etc.)\n - **Display**: Shows shipping costs, delivery timeframes, and carrier information\n - **Validation**: One shipping method must be selected to proceed\n - **API calls**: `estimateShippingMethods()`, `setShippingMethodsOnCart()`\n\n3. **Payment Methods**\n - **User action**: Choose payment method (Credit Card, PayPal, Check/Money Order, etc.)\n - **Options**: \"Bill to shipping address\" checkbox for convenience\n - **Credit cards**: Secure form with real-time validation\n - **API calls**: `setPaymentMethod()`\n\n4. **Billing Address**\n - **Conditional**: Only shown if \"Bill to shipping address\" is unchecked\n - **Guest users**: Fill out complete billing address form\n - **Logged-in users**: Select from saved addresses or create new billing address\n - **API calls**: `setBillingAddress()`\n\n5. **Order Placement**\n - **Review**: Summary of all selections (items, addresses, payment, shipping)\n - **Requirements**: Accept terms and conditions checkbox\n - **Validation**: All previous steps completed and terms accepted\n - **API calls**: `placeOrder()` - final order submission\n\n6. **Order Confirmation**\n - **Display**: Order number, items purchased, total cost, payment method\n - **Shipping details**: Delivery address, shipping method, tracking information\n - **Next steps**: Links to order tracking and account management\n\n### Virtual Product Flow\n\n1. **Login**\n - **Guest users**: Enter email address for order notifications and digital product delivery\n - **Logged-in users**: Automatically detected and authenticated\n - **Validation**: Email format validation required\n - **API calls**: `setGuestEmailOnCart()` for guest users\n\n2. **Payment Methods**\n - **User action**: Choose payment method for digital products\n - **No shipping**: Shipping-related options hidden (no \"bill to shipping\" checkbox)\n - **Focus**: Immediate payment processing for instant delivery\n - **API calls**: `setPaymentMethod()`\n\n3. **Billing Address**\n - **Purpose**: Required for payment processing and tax calculation\n - **Guest users**: Complete billing address form\n - **Logged-in users**: Select from saved billing addresses\n - **API calls**: `setBillingAddress()`\n\n4. **Order Placement**\n - **Review**: Summary shows virtual products, payment method, billing address\n - **No shipping**: No delivery options or shipping costs displayed\n - **Requirements**: Accept terms and conditions\n - **API calls**: `placeOrder()` - instant digital delivery\n\n5. **Order Confirmation**\n - **Digital delivery**: Download links, access keys, or account activation\n - **No shipping**: No tracking information or delivery address shown\n - **Immediate access**: Virtual products available instantly\n\n### Cart Type Detection\n\nThe checkout uses a simple binary detection system:\n\n- **Pure Virtual Cart**: `isVirtualCart(data)` returns `true` → Follows Virtual Product Flow (5 steps)\n- **Physical or Mixed Cart**: `isVirtualCart(data)` returns `false` → Follows Standard Physical Product Flow (6 steps)\n\n> **Note**: There is no explicit \"mixed cart\" handling. Carts containing ANY physical products are treated as physical carts and require shipping information, even if they also contain virtual products.\n\n### Key Features\n\n- **User Authentication**: Supports both guest checkout and logged-in customers\n- **Customer Addresses**: Logged-in users can select from saved addresses or add new ones\n- **Virtual Products**: Automatically detects virtual products and skips shipping-related steps\n- **Binary Cart Detection**: Pure virtual carts skip shipping steps; any physical products require full shipping flow\n- **Responsive Design**: Optimized for mobile and desktop experiences\n\n> [!NOTE]\n> This implementation follows a structured, step-based organization with event-driven architecture and container-based rendering.\n\n## Architecture\n\n### File Structure\n\n- **`commerce-checkout-multi-step.js`** - Entry point and block decorator\n- **`steps.js`** - Main implementation with StepsManager and step module coordination\n- **`steps/`** - Modular step implementations\n - **`shipping.js`** - Shipping/contact information step logic\n - **`shipping-methods.js`** - Shipping method selection step logic\n - **`payment-methods.js`** - Payment method selection step logic\n - **`billing-address.js`** - Billing address step logic\n- **`fragments.js`** - HTML fragments and step-specific fragment creation functions\n- **`containers.js`** - Container rendering functions \n- **`components.js`** - UI component functions\n- **`utils.js`** - Utility functions and helpers\n- **`constants.js`** - Shared constants and configuration (including step-related constants)\n- **`commerce-checkout-multi-step.css`** - Styling\n\n### Key Patterns\n\n- **Modular step architecture**: Each step is implemented in its own module with factory functions\n- **Step-based rendering**: Each step has display, displaySummary, continue, and isComplete functions\n- **CSS-based step visibility**: Uses `CHECKOUT_STEP_CONTENT` and `CHECKOUT_STEP_SUMMARY` classes to show/hide step elements without DOM manipulation\n- **Dependency injection**: Steps receive shared dependencies and specific step function references\n- **Container management**: Proper cleanup and recreation of components\n- **Event-driven flow**: Uses event bus for state management\n- **Form validation**: Client-side validation before API calls\n- **Fragment architecture**: Modular HTML creation using dedicated fragment functions\n- **Virtual product detection**: Automatically adapts checkout flow based on cart contents\n- **User authentication**: Seamless support for both guest and logged-in user experiences\n- **Customer address management**: Smart address selection and form rendering for authenticated users\n\n### Naming Conventions\n\nThe codebase follows consistent naming patterns for better code organization and readability:\n\n#### Constants\n\n- Step-related CSS classes use descriptive names: `CHECKOUT_STEP_ACTIVE`, `CHECKOUT_STEP_CONTENT`, `CHECKOUT_STEP_SUMMARY`\n- Form names follow the pattern: `[CONTEXT]_FORM_NAME` (e.g., `LOGIN_FORM_NAME`, `SHIPPING_FORM_NAME`)\n- Session storage keys include their purpose: `SHIPPING_ADDRESS_DATA_KEY`, `BILLING_ADDRESS_DATA_KEY`\n\n#### Variables\n\n- Step fragment variables include \"Step\": `shippingStepFragment`, `paymentMethodsStepFragment`\n- Element references use descriptive prefixes: `$shippingStep`, `$paymentMethodsList`\n\n#### Functions\n\n- Fragment creation functions follow the pattern: `create[StepName]StepFragment()`\n - `createShippingStepFragment()`\n - `createShippingMethodsStepFragment()`\n - `createPaymentMethodsStepFragment()`\n - `createBillingAddressStepFragment()`\n- Step display functions: `display[StepName]Step()` and `display[StepName]StepSummary()`\n- Continue functions: `continueFrom[CurrentStep]()` - indicating which step they're continuing from\n- Flow handler functions: `handleCheckoutFlow()` - unified handler for all cart types\n- Step completion validators: `is[StepName]StepComplete()` - modular validation functions\n\n## Step-by-Step Implementation\n\n### 1. Entry Point and Initialization\n\nThe main entry point sets up meta tags and initializes the StepsManager:\n\n```javascript\nexport default async function decorate(block) {\n setMetaTags('Checkout');\n document.title = 'Checkout';\n\n block.replaceChildren(createCheckoutFragment());\n\n const stepsManager = createStepsManager(block);\n await stepsManager.init();\n}\n```\n\n### 2. Constants and Configuration\n\nConstants are centrally managed in `constants.js`:\n\n```javascript\n// Key form names for validation\nconst LOGIN_FORM_NAME = 'login-form';\nconst SHIPPING_FORM_NAME = 'selectedShippingAddress';\nconst BILLING_FORM_NAME = 'selectedBillingAddress';\nconst TERMS_AND_CONDITIONS_FORM_NAME = 'checkout-terms-and-conditions__form';\n\n// CSS classes for step management\nconst CHECKOUT_STEP_ACTIVE = 'checkout__step--active';\n\n// Session storage keys\nconst SHIPPING_ADDRESS_DATA_KEY = 'selectedShippingAddress_addressData';\nconst BILLING_ADDRESS_DATA_KEY = 'selectedBillingAddress_addressData';\n```\n\n### 3. CSS-Based Step Visibility Control\n\nThe multi-step checkout uses a CSS-based approach to control step visibility without DOM manipulation:\n\n```css\n/* By default, show summaries and hide content */\n.checkout__step .checkout__step-content {\n display: none;\n}\n\n.checkout__step .checkout__step-summary {\n display: grid;\n}\n\n/* When step is active, show content and hide summaries */\n.checkout__step--active .checkout__step-content {\n display: grid;\n}\n\n.checkout__step--active .checkout__step-summary {\n display: none;\n}\n```\n\n#### How It Works\n\n- **All steps exist in the DOM** - No creating/destroying elements\n- **`CHECKOUT_STEP_CONTENT`** - Forms, inputs, buttons (hidden by default)\n- **`CHECKOUT_STEP_SUMMARY`** - Completed step summaries (shown by default)\n- **`CHECKOUT_STEP_ACTIVE`** - Toggles between content and summary views\n\n#### Benefits\n\n- **Performance**: No DOM manipulation, just CSS class toggling\n- **Accessibility**: Screen readers can navigate all content\n- **State preservation**: Form values remain intact when switching steps\n- **Smooth transitions**: CSS can animate visibility changes\n- **SEO friendly**: All content is present in DOM\n\n### 4. Modular Step Architecture\n\nThe checkout is organized into separate step modules for better maintainability:\n\n#### Step Module Structure\n\nEach step module (`steps/[step-name].js`) exports a factory function that returns five standardized methods:\n\n- **`display(active, data)`** - Renders the step UI with forms and components\n- **`displaySummary(...)`** - Shows completed step summary with edit option\n- **`continue()`** - Validates inputs, makes API calls, and proceeds to next step\n- **`isComplete(data, flags)`** - Checks if step has all required data for completion\n- **`isActive()`** - Returns whether the step is currently active (visible to user)\n\n#### Step Dependencies\n\nSteps receive shared dependencies (form refs, authentication, etc.) and specific function references to call other steps. This enables proper coordination without tight coupling.\n\n### 5. State Management\n\nThe StepsManager maintains global checkout state:\n\n- **Cart type detection** - Tracks if cart contains virtual vs physical products\n- **Progress tracking** - Prevents concurrent step processing during API calls\n- **Form references** - Enables cross-step communication and validation\n- **Authentication state** - Manages guest vs logged-in user experience\n- **Element references** - Provides scoped DOM access for all components\n\n### 6. HTML Layout Structure\n\nThe implementation uses fragments with CSS class selectors and dedicated fragment creation functions:\n\n```javascript\n// Create the main checkout structure using fragments\nconst checkoutFragment = createCheckoutFragment();\nblock.replaceChildren(checkoutFragment);\n\n// Use scoped selectors to access elements\nconst getElement = createScopedSelector(block);\nconst { checkout } = selectors;\n\n// Access elements using class selectors\nconst elements = {\n $content: getElement(checkout.content), // .checkout__content\n $loader: getElement(checkout.loader), // .checkout__loader\n $loginForm: getElement(checkout.loginForm), // .checkout__login\n $shippingAddressForm: getElement(checkout.shippingAddressForm), // .checkout__shipping-form\n $paymentMethodsList: getElement(checkout.paymentMethodsList), // .checkout__payment-methods-list\n // ... other elements accessed via class selectors\n};\n```\n\n#### Fragment Creation Functions\n\nThe checkout structure is built using specialized fragment creation functions:\n\n```javascript\n// Step-specific fragment creation functions\nconst shippingStepFragment = getMainElement(checkout.shippingStep);\nconst shippingMethodsStepFragment = getMainElement(checkout.shippingMethodStep);\nconst paymentMethodsStepFragment = getMainElement(checkout.paymentStep);\nconst billingAddressStepFragment = getMainElement(checkout.billingStep);\n\n// Append step-specific fragments\nshippingStepFragment.appendChild(createShippingStepFragment());\nshippingMethodsStepFragment.appendChild(createShippingMethodsStepFragment());\npaymentMethodsStepFragment.appendChild(createPaymentMethodsStepFragment());\nbillingAddressStepFragment.appendChild(createBillingAddressStepFragment());\n```\n\nEach fragment creation function returns structured HTML for its respective checkout step:\n\n- `createShippingStepFragment()` - Creates login and shipping address forms\n- `createShippingMethodsStepFragment()` - Creates shipping method selection UI\n- `createPaymentMethodsStepFragment()` - Creates payment method selection and billing options\n- `createBillingAddressStepFragment()` - Creates billing address form\n\nThe actual HTML structure is generated by `createCheckoutFragment()` which creates:\n\n```html\n<div class=\"checkout__wrapper\">\n <div class=\"checkout__loader\"></div>\n <div class=\"checkout__merged-cart-banner\"></div>\n <div class=\"checkout__content\">\n <div class=\"checkout__main\">\n <div class=\"checkout__header checkout__block\"></div>\n <div class=\"checkout__server-error checkout__block\"></div>\n <div class=\"checkout__out-of-stock checkout__block\"></div>\n \n <!-- Step containers with nested elements -->\n <div class=\"checkout__shipping-address checkout__block checkout__step\">\n <div class=\"checkout__login checkout__block checkout__step-content\"></div>\n <div class=\"checkout__login-form-summary checkout__block checkout__step-summary\"></div>\n <div class=\"checkout__shipping-form checkout__block checkout__step-content\"></div>\n <div class=\"checkout__shipping-form-summary checkout__block checkout__step-summary\"></div>\n <div class=\"checkout__continue-to-shipping-methods checkout__block checkout__step-button checkout__step-content\"></div>\n </div>\n \n <!-- Additional step containers -->\n </div>\n <div class=\"checkout__aside\">\n <div class=\"checkout__order-summary checkout__block\"></div>\n <div class=\"checkout__cart-summary checkout__block\"></div>\n </div>\n </div>\n</div>\n```\n\n### 7. Step Implementation Pattern\n\nEach step module follows a consistent interface:\n\n#### Individual Step Responsibilities\n\n- **Shipping Step** (`steps/shipping.js`) - Login form and shipping address (handles virtual product logic)\n- **Shipping Methods Step** (`steps/shipping-methods.js`) - Delivery method selection (skipped for virtual products) \n- **Payment Methods Step** (`steps/payment-methods.js`) - Payment selection and bill-to-shipping option\n- **Billing Address Step** (`steps/billing-address.js`) - Billing address form (conditional on bill-to-shipping)\n\n#### Shared Step Functions\n\nAll step modules export the same standardized interface with these main functions:\n\n- **`display(active, data)`** - Renders the step UI with forms, components, and continue button\n- **`displaySummary(...)`** - Shows completed step summary with edit option and deactivates the step\n- **`continue()`** - Validates forms, makes API calls, proceeds to next step, and emits `checkout/step/completed`\n- **`isComplete(data)`** - Checks if step has all required data for completion\n- **`isActive()`** - Returns whether the step is currently active (visible to user)\n\n#### Common Step Patterns\n\nAll step modules follow consistent patterns:\n\n**Factory Function Pattern:**\n\n```javascript\nexport const create[StepName]Step = (dependencies) => {\n // Step-specific elements and logic\n const elements = { ... };\n \n // Main functions\n async function display[StepName]Step(active, data) { ... }\n async function display[StepName]StepSummary(...) { ... }\n const continueFrom[StepName]Step = withOverlaySpinner(async () => { ... });\n const is[StepName]StepComplete = (data) => { ... };\n const is[StepName]StepActive = () => { ... };\n \n // Return standardized interface\n return {\n continue: continueFrom[StepName]Step,\n display: display[StepName]Step,\n displaySummary: display[StepName]StepSummary,\n isActive: is[StepName]StepActive,\n isComplete: is[StepName]StepComplete,\n };\n};\n```\n\n**Step Lifecycle:**\n\n1. **Display**: Render forms and components, activate step UI\n2. **User Interaction**: User fills forms and clicks continue\n3. **Continue**: Validate forms, make API calls, emit completion event\n4. **Summary**: Show completed step summary with edit option\n5. **Next Step**: Proceed to next step in flow\n\n**API Integration Pattern:**\n\n- Form validation before API calls\n- Error handling with console logging\n- Success handling with step progression\n- Loading states with `withOverlaySpinner` wrapper\n\n**Event Integration Pattern:**\n\n- Listen to `checkout/values` for form data\n- Listen to `checkout/addresses/*` for address data\n- Emit `checkout/step/completed` on successful completion\n- Handle cart updates through event listeners\n\n#### Implementation Patterns\n\n- **Step Activation**: Uses CSS classes (`CHECKOUT_STEP_ACTIVE`) to control visibility\n- **Conditional Rendering**: Different UI for guest vs logged-in users and virtual vs physical products\n- **Form Validation**: Client-side validation before API calls\n- **Error Handling**: Graceful handling of API failures\n- **Spinner Integration**: Loading states during API operations\n- **Event-driven Flow**: Listens to cart updates and user authentication changes\n- **Inter-step Communication**: Steps call each other through injected function references\n\n#### Virtual vs Physical Product Logic\n\n- **Virtual Products**: Skip shipping methods step within unified flow\n- **Physical Products**: Full flow including shipping address and method selection\n- **Mixed Carts**: Treated as physical products (require shipping information)\n- **Step-aware Logic**: Each step module handles virtual product conditions internally\n\n#### Summary and Edit Pattern\n\nEach completed step renders a summary with an edit button that reactivates the step for modification.\n\n### 8. Container Management\n\nContainers are modular components that handle their own lifecycle and rendering:\n\n- **Independent Rendering**: Each container manages its own DOM updates and lifecycle\n- **Element Binding**: Containers render to designated DOM elements\n- **Form References**: Shared form references enable cross-step communication\n- **Automatic Cleanup**: Containers handle mounting and unmounting of dropin components\n- **Authentication Awareness**: Different containers for guest vs logged-in users\n\n### 9. Event Handlers\n\nEvent handlers are the core logic that responds to checkout state changes and manages the step-by-step flow. They listen to the event bus and automatically update the UI based on cart data, user authentication, and checkout progress.\n\n#### Key Event Handlers\n\n- **`handleCheckoutUpdate`** - Responds to cart changes and determines which step to display\n- **`handleAuthenticated`** - Manages login state changes and modal dismissal \n- **`handleOrderPlaced`** - Processes successful orders and redirects to confirmation\n- **`updateContainers`** - Refreshes containers when user authentication changes\n\n#### Event Handler Responsibilities\n\n- **State-based rendering**: Automatically shows the correct step based on completion status\n- **Virtual product detection**: Adapts flow for virtual products vs physical products\n- **Authentication handling**: Switches between guest and logged-in user experiences\n- **Cart merge handling**: Updates cart state during login, natural flow handles transitions\n- **Progress validation**: Ensures all required data is collected before advancing\n- **Error handling**: Manages API failures and validation errors\n\n#### Flow Management Architecture\n\nThe checkout uses a unified flow handler that adapts based on cart type:\n\n- **`handleCheckoutFlow`**: Unified flow handler for all cart types with conditional logic\n- **Conditional Steps**: Automatically skips shipping methods step for virtual products\n- **Billing Integration**: Billing logic is integrated directly into the unified flow\n\n#### Step Completion Validation\n\nEach step module provides its own completion validation through the `isComplete` method:\n\n- **`steps.shipping.isComplete(data, isVirtual)`**: Email only for virtual products, email + address for physical\n- **`steps.shippingMethods.isComplete(data)`**: Shipping method selection (automatically skipped for virtual products)\n- **`steps.paymentMethods.isComplete(data)`**: Payment method selection (applies to all cart types)\n- **`steps.billingAddress.isComplete(data)`**: Billing address validation (applies to all cart types)\n\n#### Event Registration\n\nEvent handlers are registered during initialization to listen for cart updates, authentication changes, and order completion.\n\n```javascript\n// Register event handlers\nasync function init() {\n events.on('authenticated', handleAuthenticated);\n events.on('checkout/initialized', handleCheckoutUpdate, { eager: true });\n events.on('checkout/updated', handleCheckoutUpdate);\n events.on('order/placed', handleOrderPlaced);\n events.on('checkout/step/completed', handleCheckoutStepCompleted);\n\n // Render initial components\n await Promise.all([\n renderMergedCartBanner(elements.$mergedCartBanner),\n renderOutOfStock(elements.$outOfStock),\n renderServerError(elements.$serverError, block),\n renderCheckoutHeader(elements.$header),\n // ... other initial renders\n ]);\n}\n```\n\n### 10. Event System Architecture\n\nThe multi-step checkout implements a comprehensive event-driven architecture for inter-step communication and state management.\n\n#### Core Events\n\n**Checkout Flow Events:**\n\n- **`checkout/initialized`** - Triggered when checkout is first loaded with cart data\n - **Purpose**: Initial setup and first step determination\n - **Handler**: `handleCheckoutUpdate`\n - **Eager**: `true` - processes immediately if cart data is already available\n\n- **`checkout/updated`** - Triggered when cart data changes (items, addresses, payment, etc.)\n - **Purpose**: Re-evaluates which step to display based on current cart state\n - **Handler**: `handleCheckoutUpdate`\n - **Behavior**: Determines next step in flow based on completion status\n\n- **`checkout/step/completed`** - Emitted by each step module when it successfully completes\n - **Purpose**: Enables place order button when all required steps are finished\n - **Handler**: `handleCheckoutStepCompleted`\n - **Emitted by**: All step modules (`shipping.js`, `shipping-methods.js`, `payment-methods.js`, `billing-address.js`)\n - **Logic**: Checks if all steps are complete and no step is currently active, then enables place order\n\n**Authentication Events:**\n\n- **`authenticated`** - Triggered when user logs in during checkout\n - **Purpose**: Switches from guest forms to customer address selection\n - **Handler**: `handleAuthenticated`\n - **Actions**: Updates containers and refreshes step display\n\n**Order Events:**\n\n- **`order/placed`** - Triggered when order is successfully submitted\n - **Purpose**: Redirects to order confirmation and clears session data\n - **Handler**: `handleOrderPlaced`\n - **Actions**: Clears address data, redirects to order details page\n\n#### Inter-Step Communication Events\n\n**Address Communication:**\n\n- **`checkout/addresses/shipping`** - Emitted when shipping address is selected or updated\n - **Purpose**: Shares shipping address data between steps\n - **Emitted by**: Shipping address containers in `containers.js`\n - **Used by**:\n - `steps/shipping.js` - For retrieving current shipping address\n - `steps/payment-methods.js` - For bill-to-shipping address functionality\n - **Data**: Contains complete address object with validation status\n\n- **`checkout/addresses/billing`** - Emitted when billing address is selected or updated\n - **Purpose**: Shares billing address data between steps\n - **Emitted by**: Billing address containers in `containers.js`\n - **Used by**: `steps/billing-address.js` - For retrieving current billing address\n - **Data**: Contains complete billing address object\n\n#### Event Flow Architecture\n\n```javascript\n// Step completion flow\nsteps.shipping.continue() → events.emit('checkout/step/completed', null)\n ↓\nhandleCheckoutStepCompleted() → checks all steps → enables place order if complete\n\n// Address sharing flow\nrenderShippingAddressForm() → user input → events.emit('checkout/addresses/shipping', values)\n ↓\nsteps.paymentMethods.displaySummary() → events.lastPayload('checkout/addresses/shipping')\n\n// Cart update flow\nCart API updates → events.emit('checkout/updated', data) → handleCheckoutUpdate() → determine next step\n```\n\n#### Event Handling Patterns\n\n**Debounced Emissions:**\nAddress events use debounced emissions to prevent excessive API calls:\n\n```javascript\nconst notifyValues = debounce((values) => {\n events.emit('checkout/addresses/shipping', values);\n}, DEBOUNCE_TIME);\n```\n\n**Last Payload Access:**\nSteps can access the most recent event data using `lastPayload()`:\n\n```javascript\nconst { data: shippingAddress } = events.lastPayload('checkout/addresses/shipping');\nconst { data: billingAddress } = events.lastPayload('checkout/addresses/billing');\n```\n\n**Conditional Event Processing:**\nEvent handlers check current state before processing to prevent race conditions:\n\n```javascript\nconst handleCheckoutStepCompleted = () => {\n if (checkoutSteps.some((step) => step.isActive())) return; // Prevent processing if step is active\n // ... process completion logic\n};\n```\n\n### 11. Order Confirmation\n\nAfter successful order placement, the checkout transitions to an order confirmation view:\n\n- **Page Transition**: Updates meta tags, title, and URL to order details page\n- **Order API Integration**: Initializes order dropin with order data and localization\n- **Component Rendering**: Displays order status, customer details, cost summary, and product list\n- **Virtual Product Handling**: Shows appropriate confirmation for digital vs physical products\n- **Navigation**: Provides continue shopping functionality\n\n### 12. Virtual Product Support\n\nThe checkout automatically detects virtual products and provides a streamlined flow:\n\n- **Automatic Detection**: Uses `isVirtualCart()` utility to determine cart type\n- **Unified Flow Handler**: Single `handleCheckoutFlow()` with conditional logic for virtual products\n- **Component Management**: Removes shipping method step title for virtual products using `removeComponent()`\n- **Container Management**: Unmounts shipping-related containers for virtual products\n- **Direct Navigation**: Proceeds directly from login to payment for virtual-only carts\n- **Cart Merge Support**: Seamlessly handles virtual-to-physical transitions through natural flow progression\n- **Mixed Cart Handling**: Carts with any physical products follow the full shipping flow\n\n### 13. User Authentication Handling\n\nThe checkout seamlessly supports both guest and logged-in users:\n\n- **Authentication Detection**: Uses event payload to determine user authentication status\n- **Dynamic Container Updates**: Renders different UI components based on authentication state\n- **Guest Experience**: Provides address forms for guest users to enter new information\n- **Logged-in Experience**: Shows saved address selection for authenticated customers\n- **Modal Management**: Automatically closes login modals upon successful authentication\n- **Mid-checkout Login**: Supports user authentication at any point during checkout\n\n## Key Implementation Features\n\n### Form Validation Pattern\n\n- Consistent client-side validation across all checkout steps\n- Uses native form validation with custom error handling\n- Prevents submission until all required fields are valid\n\n### Spinner Integration\n\n- Higher-order function pattern for loading states\n- Overlay spinner during API operations\n- Automatic cleanup after completion or error\n\n### Element Management\n\n- Scoped selectors for reliable element access\n- Components manage their own lifecycle and cleanup\n- No direct DOM element storage to prevent memory leaks\n\n### State-based Initialization\n\nThe checkout intelligently determines which step to show based on the current cart state using the step modules' `isComplete` methods:\n\n- `!steps.shipping.isComplete(data, currentCartIsVirtual)` → Step 1 (Login/Shipping)\n- `!steps.shippingMethods.isComplete(data)` → Step 2 (Shipping Methods) - skipped for virtual\n- `!steps.paymentMethods.isComplete(data)` → Step 3 (Payment Methods)\n- `!steps.billingAddress.isComplete(data)` → Step 4 (Billing)\n- All steps complete → Enable place order\n\n### Cart Merge Scenario Handling\n\nThe checkout handles complex cart merge scenarios when guest users log in mid-checkout:\n\n- **Virtual-to-Physical Transition**: When a guest with virtual products logs in to an account with physical products\n- **State Updates**: `currentCartIsVirtual` automatically updates to reflect the merged cart type\n- **Container Updates**: `updateContainers()` switches between guest and authenticated address forms\n- **Natural Flow**: When user continues from shipping step, the unified flow automatically handles the new cart type\n- **Component Management**: Shipping method title restoration handled through standard cart type logic\n- **User Experience**: User selects their address and naturally proceeds to shipping methods step\n\n## Styling\n\nThe multi-step checkout uses a responsive grid layout with:\n\n- Step-based visibility control\n- Responsive main/aside layout\n- Loading indicators and overlays\n- Summary component styling\n- Button and form styling\n\nComplete styles are in `commerce-checkout-multi-step.css`.\n\n## Container Functions\n\nThe implementation separates rendering logic into `containers.js`:\n\n**Guest User Containers:**\n\n- `renderLoginForm` - Email input form for guest checkout\n- `renderShippingAddressForm` - Shipping address form with validation\n- `renderBillingAddressForm` - Billing address form with validation\n\n**Authenticated User Containers:**\n\n- `renderCustomerShippingAddresses` - Customer shipping address selection\n- `renderCustomerBillingAddresses` - Customer billing address selection\n\n**Common Containers:**\n\n- `renderShippingMethods` - Delivery method selection\n- `renderPaymentMethods` - Payment method selection with credit card support\n- `renderBillToShippingAddress` - Bill-to-shipping checkbox\n- `renderTermsAndConditions` - Checkout terms acceptance\n- `renderPlaceOrder` - Final order placement with validation\n\n**State Containers:**\n\n- `renderMergedCartBanner` - Cart merge notification\n- `renderOutOfStock` - Out of stock warning\n- `renderServerError` - Error state handling\n\nOrder confirmation containers:\n\n- `renderOrderHeader` - Order confirmation header\n- `renderOrderStatus` - Order status display\n- `renderCustomerDetails` - Customer information\n- `renderOrderCostSummary` - Order cost breakdown\n- `renderOrderProductList` - Order product details\n\n## Utility Functions\n\nKey utilities in `utils.js`:\n\n**Cart and Product Detection:**\n\n- `isDataEmpty` - Check for empty cart/checkout state \n- `isVirtualCart` - Detect if cart contains only virtual products\n- `getCartAddress` - Extract shipping/billing address from cart data\n- `getCartShippingMethod` - Extract selected shipping method\n- `getCartPaymentMethod` - Extract selected payment method\n\n**Data Transformation:**\n\n- `transformAddressFormValues` - Convert form data to API format\n\n**UI Utilities:**\n\n- `setMetaTags` - Update page meta information\n- `scrollToElement` - Smooth scroll to element\n- `removeModal` - Close modal dialogs\n- `showModal` - Display modal with content\n\n**Feature-Specific:**\n\n- `estimateShippingCost` - Calculate shipping estimates (disabled for virtual products)\n\n## Binary Cart Type Logic\n\nThe checkout implementation uses a simple binary approach for cart type detection based on the `isVirtualCart()` utility function.\n\n### How `isVirtualCart()` Works\n\n- **Returns `true`**: Only when ALL cart items are virtual products\n- **Returns `false`**: For pure physical carts OR mixed carts (any physical products present)\n\n### Implications\n\n- **No explicit mixed cart logic**: Mixed carts are handled as physical carts\n- **Conservative approach**: Any physical product requires full shipping flow\n- **Virtual products in mixed carts**: No special handling, processed with physical items\n\nFragment creation utilities in `fragments.js`:\n\n- `createShippingStepFragment` - Creates login and shipping address forms\n- `createShippingMethodsStepFragment` - Creates shipping method selection UI\n- `createPaymentMethodsStepFragment` - Creates payment method selection and billing options\n- `createBillingAddressStepFragment` - Creates billing address form\n- `createCheckoutFragment` - Creates main checkout structure\n- `createOrderConfirmationFragment` - Creates order confirmation layout\n\nComponent utilities in `components.js`:\n\n- `renderSpinner` - Loading indicator component\n- `renderStepContinueBtn` - Step navigation buttons\n- `renderCheckoutHeader` - Main checkout header\n- `renderShippingStepTitle` - Step title components\n- `removeComponent` - Component lifecycle management\n",
|
|
347
|
+
"components.js": "/* eslint-disable import/no-unresolved */\n\nimport {\n Button,\n Header,\n ProgressSpinner,\n provider as UI,\n} from '@dropins/tools/components.js';\n\nimport { CHECKOUT_HEADER_CLASS } from './constants.js';\n\n/**\n * Component IDs for registry management\n * @enum {string}\n */\nexport const COMPONENT_IDS = {\n BILLING_STEP_CONTINUE_BTN: 'billingStepContinueBtn',\n BILLING_STEP_TITLE: 'billingStepTitle',\n CHECKOUT_HEADER: 'checkoutHeader',\n CHECKOUT_LOADER: 'checkoutLoader',\n ORDER_CONFIRMATION_CONTINUE_BTN: 'orderConfirmationContinueBtn',\n PAYMENT_STEP_CONTINUE_BTN: 'paymentStepContinueBtn',\n PAYMENT_STEP_TITLE: 'paymentStepTitle',\n SHIPPING_METHOD_STEP_CONTINUE_BTN: 'shippingMethodStepContinueBtn',\n SHIPPING_METHOD_STEP_TITLE: 'shippingMethodStepTitle',\n SHIPPING_STEP_CONTINUE_BTN: 'shippingStepContinueBtn',\n SHIPPING_STEP_TITLE: 'shippingStepTitle',\n};\n\n/**\n * A Map to store the API of rendered components.\n * The key is a unique string ID, and the value is the components's API object.\n * (e.g., { setProps: (props) => {...}, remove: () => {...} })\n */\nconst registry = new Map();\n\n/**\n * Checks if a component with the given ID has been rendered.\n * This is used to prevent multiple instances of the same component from being rendered.\n * @param {string} id - The unique ID of the component to check.\n * @returns {boolean} - Returns true if the component has been rendered, false otherwise.\n */\nexport const hasComponent = (id) => registry.has(id);\n\n/**\n * Removes a component from the registry and calls its remove method.\n * This is used to clean up components when they are no longer needed.\n * @param {string} id - The unique ID of the component to remove.\n * @returns {void}\n */\nexport const removeComponent = (id) => {\n const component = registry.get(id);\n\n if (component) {\n component.remove();\n registry.delete(id);\n }\n};\n\n/**\n * Helper to get a component from the registry or render and register it if not present.\n * @async\n * @param {string} id - Unique identifier for the component.\n * @param {Function} renderFn - Async function that renders the component.\n * @returns {Promise<Object>} - The rendered component API.\n */\nconst renderComponent = async (id, renderFn) => {\n if (registry.has(id)) {\n return registry.get(id);\n }\n\n const component = await renderFn();\n registry.set(id, component);\n return component;\n};\n\n/**\n * Renders an H2 header component and registers its API.\n * @async\n * @param {HTMLElement} container - The DOM element to render the header into.\n * @param {string} id - Unique identifier for the header component.\n * @param {Object} options - Configuration options for the header component.\n * @returns {Promise<void>}\n */\nconst renderH2 = async (container, id, options = {}) => renderComponent(\n id,\n async () => UI.render(Header, {\n size: 'medium',\n level: 2,\n divider: true,\n ...options,\n })(container),\n);\n\n/**\n * Renders a primary button component and registers its API.\n * @async\n * @param {HTMLElement} container - The DOM element to render the button into.\n * @param {string} id - Unique identifier for the button component.\n * @param {Object} options - Configuration options including text, onClick, href, className, etc.\n * @returns {Promise<Object>} - The rendered button component API\n */\nexport const renderPrimaryButton = async (container, id, options = {}) => renderComponent(\n id,\n async () => UI.render(Button, {\n size: 'medium',\n variant: 'primary',\n disabled: false,\n ...options,\n })(container),\n);\n\n/**\n * Renders a progress spinner component and registers its API.\n * @async\n * @param {HTMLElement} container - The DOM element to render the spinner into.\n * @param {string} id - Unique identifier for the button component.\n * @param {Object} options - Optional configuration for the spinner.\n * @returns {Promise<Object>} - The rendered spinner component API\n */\nexport const renderSpinner = async (container, id, options = {}) => renderComponent(\n id,\n async () => UI.render(ProgressSpinner, {\n className: 'checkout__overlay-spinner',\n ...options,\n })(container),\n);\n\n/**\n * Renders the main checkout header (H1).\n * @param {HTMLElement} container - The DOM element to render the header into.\n * @param {string} title - The title to display in the header\n * @returns {Promise<Object>} - The rendered header component API\n */\nexport const renderCheckoutHeader = (container, title) => renderComponent(\n COMPONENT_IDS.CHECKOUT_HEADER,\n async () => UI.render(Header, {\n className: CHECKOUT_HEADER_CLASS,\n divider: true,\n level: 1,\n size: 'large',\n title,\n })(container),\n);\n\n/**\n * Renders the shipping step title (H2).\n * @param {HTMLElement} container - The DOM element to render the header into.\n * @returns {Promise<Object>} - The rendered header component API\n */\nexport const renderShippingMethodStepTitle = (container) => renderH2(\n container,\n COMPONENT_IDS.SHIPPING_METHOD_STEP_TITLE,\n {\n title: 'Shipping Methods',\n },\n);\n\n/**\n * Renders the payment step title (H2).\n * @param {HTMLElement} container - The DOM element to render the header into.\n * @returns {Promise<Object>} - The rendered header component API\n */\nexport const renderPaymentStepTitle = (container) => renderH2(\n container,\n COMPONENT_IDS.PAYMENT_STEP_TITLE,\n {\n title: 'Payment Methods',\n },\n);\n\n/**\n * Renders the billing step title (H2).\n * @param {HTMLElement} container - The DOM element to render the header into.\n * @returns {Promise<Object>} - The rendered header component API\n */\nexport const renderBillingStepTitle = (container) => renderH2(\n container,\n COMPONENT_IDS.BILLING_STEP_TITLE,\n {\n title: 'Billing Address',\n },\n);\n\n/**\n * Changes the title of a header component by its component ID.\n * @param {string} componentId - The unique ID of the header component.\n * @param {string} title - The new title to set.\n */\nexport const changeTitle = (componentId, title) => {\n const header = registry.get(componentId);\n\n if (header) {\n header.setProps((prev) => ({ ...prev, title }));\n }\n};\n\n/**\n * Changes the title of the checkout header.\n * @param {string} title - The new title for the checkout header.\n */\nexport const changeCheckoutTitle = (title) => {\n changeTitle(COMPONENT_IDS.CHECKOUT_HEADER, title);\n};\n\n/**\n * Renders a \"Continue\" button for a given step and registers its API.\n * @async\n * @param {HTMLElement} container - The DOM element to render the button into.\n * @param {string} stepId - The unique component ID for the step's continue button.\n * @param {Function} onClick - The click handler for the button.\n * @returns {Promise<Object>} - The rendered button component API\n */\nexport const renderStepContinueBtn = async (container, stepId, onClick) => renderPrimaryButton(\n container,\n stepId,\n { children: 'Continue', onClick },\n);\n\n/**\n * Renders the \"Continue shopping\" button on the order confirmation page.\n * If the button already exists in the registry, returns the existing instance.\n * Otherwise, creates and renders a new primary button with the appropriate properties.\n *\n * @async\n * @param {HTMLElement} container - The DOM element to render the button into.\n * @returns {Promise<HTMLElement>} The rendered button element.\n */\nexport const renderOrderConfirmationContinueBtn = async (container) => renderPrimaryButton(\n container,\n COMPONENT_IDS.ORDER_CONFIRMATION_CONTINUE_BTN,\n {\n children: 'Continue shopping',\n className: 'order-confirmation-footer__continue-button',\n type: 'submit',\n href: '/',\n },\n);\n",
|
|
348
|
+
"constants.js": "// Form and address constants\nconst BILLING_FORM_NAME = 'selectedBillingAddress';\nconst BILLING_ADDRESS_DATA_KEY = `${BILLING_FORM_NAME}_addressData`;\nconst LOGIN_FORM_NAME = 'login-form';\nconst SHIPPING_FORM_NAME = 'selectedShippingAddress';\nconst SHIPPING_ADDRESS_DATA_KEY = `${SHIPPING_FORM_NAME}_addressData`;\nconst TERMS_AND_CONDITIONS_FORM_NAME = 'checkout-terms-and-conditions__form';\n\n// Timing constants\nconst DEBOUNCE_TIME = 1000;\nconst ADDRESS_INPUT_DEBOUNCE_TIME = 500;\n\n// Block and styling constants\nconst CHECKOUT_BLOCK = 'checkout__block';\nconst CHECKOUT_ERROR_CLASS = 'checkout__content--error';\nconst CHECKOUT_HEADER_CLASS = 'checkout-header';\nconst ORDER_CONFIRMATION_BLOCK = 'order-confirmation__block';\n\n// Multi-step specific constants\nconst CHECKOUT_STEP = 'checkout__step';\nconst CHECKOUT_STEP_ACTIVE = 'checkout__step--active';\nconst CHECKOUT_STEP_BUTTON = 'checkout__step-button';\nconst CHECKOUT_STEP_CONTENT = 'checkout__step-content';\nconst CHECKOUT_STEP_SUMMARY = 'checkout__step-summary';\nconst CHECKOUT_STEP_TITLE = 'checkout__step-title';\nconst DEFAULT_IS_BILL_TO_SHIPPING = true;\n\n// Default values\nconst USER_TOKEN_COOKIE_NAME = 'auth_dropin_user_token';\n\nexport {\n // Form and address constants\n ADDRESS_INPUT_DEBOUNCE_TIME,\n BILLING_ADDRESS_DATA_KEY,\n BILLING_FORM_NAME,\n LOGIN_FORM_NAME,\n SHIPPING_ADDRESS_DATA_KEY,\n SHIPPING_FORM_NAME,\n TERMS_AND_CONDITIONS_FORM_NAME,\n\n // Timing constants\n DEBOUNCE_TIME,\n\n // Block and styling constants\n CHECKOUT_BLOCK,\n CHECKOUT_ERROR_CLASS,\n CHECKOUT_HEADER_CLASS,\n ORDER_CONFIRMATION_BLOCK,\n\n // Multi-step specific constants\n CHECKOUT_STEP,\n CHECKOUT_STEP_ACTIVE,\n CHECKOUT_STEP_BUTTON,\n CHECKOUT_STEP_CONTENT,\n CHECKOUT_STEP_SUMMARY,\n CHECKOUT_STEP_TITLE,\n DEFAULT_IS_BILL_TO_SHIPPING,\n\n // Default values\n USER_TOKEN_COOKIE_NAME,\n};\n",
|
|
349
|
+
"containers.js": "/* eslint-disable max-len */\n/* eslint-disable import/no-unresolved */\n/* eslint-disable no-unused-vars */\n/* eslint-disable no-shadow */\n/* eslint-disable no-use-before-define */\n/* eslint-disable prefer-const */\n\n// Checkout Dropin\nimport * as checkoutApi from '@dropins/storefront-checkout/api.js';\nimport BillToShippingAddress from '@dropins/storefront-checkout/containers/BillToShippingAddress.js';\nimport EstimateShipping from '@dropins/storefront-checkout/containers/EstimateShipping.js';\nimport LoginForm from '@dropins/storefront-checkout/containers/LoginForm.js';\nimport MergedCartBanner from '@dropins/storefront-checkout/containers/MergedCartBanner.js';\nimport OutOfStock from '@dropins/storefront-checkout/containers/OutOfStock.js';\nimport PaymentMethods from '@dropins/storefront-checkout/containers/PaymentMethods.js';\nimport PlaceOrder from '@dropins/storefront-checkout/containers/PlaceOrder.js';\nimport ServerError from '@dropins/storefront-checkout/containers/ServerError.js';\nimport ShippingMethods from '@dropins/storefront-checkout/containers/ShippingMethods.js';\nimport TermsAndConditions from '@dropins/storefront-checkout/containers/TermsAndConditions.js';\nimport { render as CheckoutProvider } from '@dropins/storefront-checkout/render.js';\n\n// Auth Dropin\nimport * as authApi from '@dropins/storefront-auth/api.js';\nimport AuthCombine from '@dropins/storefront-auth/containers/AuthCombine.js';\nimport SignUp from '@dropins/storefront-auth/containers/SignUp.js';\nimport { render as AuthProvider } from '@dropins/storefront-auth/render.js';\n\n// Account Dropin\nimport Addresses from '@dropins/storefront-account/containers/Addresses.js';\nimport AddressForm from '@dropins/storefront-account/containers/AddressForm.js';\nimport { render as AccountProvider } from '@dropins/storefront-account/render.js';\n\n// Cart Dropin\nimport * as cartApi from '@dropins/storefront-cart/api.js';\nimport CartSummaryList from '@dropins/storefront-cart/containers/CartSummaryList.js';\nimport Coupons from '@dropins/storefront-cart/containers/Coupons.js';\nimport GiftOptions from '@dropins/storefront-cart/containers/GiftOptions.js';\nimport OrderSummary from '@dropins/storefront-cart/containers/OrderSummary.js';\nimport { render as CartProvider } from '@dropins/storefront-cart/render.js';\n\n// Payment Services Dropin\nimport { PaymentMethodCode } from '@dropins/storefront-payment-services/api.js';\nimport CreditCard from '@dropins/storefront-payment-services/containers/CreditCard.js';\nimport { render as PaymentServices } from '@dropins/storefront-payment-services/render.js';\n\n// Order Dropin\nimport CustomerDetails from '@dropins/storefront-order/containers/CustomerDetails.js';\nimport OrderCostSummary from '@dropins/storefront-order/containers/OrderCostSummary.js';\nimport OrderHeader from '@dropins/storefront-order/containers/OrderHeader.js';\nimport OrderProductList from '@dropins/storefront-order/containers/OrderProductList.js';\nimport OrderStatus from '@dropins/storefront-order/containers/OrderStatus.js';\nimport ShippingStatus from '@dropins/storefront-order/containers/ShippingStatus.js';\nimport { render as OrderProvider } from '@dropins/storefront-order/render.js';\n\n// Tools\nimport { events } from '@dropins/tools/event-bus.js';\nimport { getCookie, debounce } from '@dropins/tools/lib.js';\nimport { tryRenderAemAssetsImage } from '@dropins/tools/lib/aem/assets.js';\nimport { getConfigValue } from '@dropins/tools/lib/aem/configs.js';\n\n// Dropin Lib Functions\nimport { estimateShippingCost, getCartAddress } from '@dropins/storefront-checkout/lib/utils.js';\n\n// Utils\nimport {\n showModal,\n swatchImageSlot,\n} from './utils.js';\n\n// Slots\nimport { authPrivacyPolicyConsentSlot } from '../../scripts/constants.js';\n\n// External dependencies\nimport { fetchPlaceholders } from '../../scripts/commerce.js';\n\n// Fragments\nimport { selectors } from './fragments.js';\n\n// Constants\nimport {\n BILLING_FORM_NAME,\n CHECKOUT_ERROR_CLASS,\n DEBOUNCE_TIME,\n LOGIN_FORM_NAME,\n SHIPPING_FORM_NAME,\n USER_TOKEN_COOKIE_NAME,\n} from './constants.js';\n\n/**\n * Container IDs for registry management\n * @enum {string}\n */\nexport const CONTAINERS = Object.freeze({\n BILL_TO_SHIPPING: 'billToShipping',\n BILLING_ADDRESS_FORM: 'billingAddressForm',\n CART_COUPONS: 'cartCoupons',\n CART_SUMMARY_LIST: 'cartSummaryList',\n CUSTOMER_BILLING_ADDRESSES: 'customerBillingAddresses',\n CUSTOMER_DETAILS: 'customerDetails',\n CUSTOMER_SHIPPING_ADDRESSES: 'customerShippingAddresses',\n ESTIMATE_SHIPPING: 'estimateShipping',\n LOGIN_FORM: 'loginForm',\n MERGED_CART_BANNER: 'mergedCartBanner',\n ORDER_COST_SUMMARY: 'orderCostSummary',\n ORDER_HEADER: 'orderHeader',\n ORDER_PRODUCT_LIST: 'orderProductList',\n ORDER_STATUS: 'orderStatus',\n ORDER_SUMMARY: 'orderSummary',\n OUT_OF_STOCK: 'outOfStock',\n PAYMENT_METHODS: 'paymentMethods',\n PLACE_ORDER_BUTTON: 'placeOrderButton',\n SERVER_ERROR: 'serverError',\n SHIPPING_ADDRESS_FORM: 'shippingAddressForm',\n SHIPPING_METHODS: 'shippingMethods',\n SHIPPING_STATUS: 'shippingStatus',\n SIGN_UP_FORM: 'signUpForm',\n TERMS_AND_CONDITIONS: 'termsAndConditions',\n});\n\n/**\n * A Map to store the API of rendered containers.\n * The key is a unique string ID, and the value is the containers's API object.\n * (e.g., { setProps: (props) => {...}, remove: () => {...} })\n */\nconst registry = new Map();\n\n/**\n * Checks if a container with the given ID has been rendered.\n * This is used to prevent multiple instances of the same container from being rendered.\n * @param {string} id - The unique ID of the container to check.\n * @returns {boolean} - Returns true if the container has been rendered, false otherwise.\n */\nexport const hasContainer = (id) => registry.has(id);\n\n/**\n * Retrieves a container from the registry.\n * @param {string} id - The unique ID of the container to retrieve.\n * @returns {Object} - The container API object.\n */\nexport const getContainer = (id) => registry.get(id);\n\n/**\n * Helper to get a container from the registry or render and register it if not present.\n * @async\n * @param {string} id - Unique identifier for the container.\n * @param {Function} renderFn - Async function that renders the container.\n * @returns {Promise<Object>} - The rendered container API.\n */\nconst renderContainer = async (id, renderFn) => {\n if (registry.has(id)) {\n return registry.get(id);\n }\n\n const container = await renderFn();\n registry.set(id, container);\n return container;\n};\n\n/**\n * Unmounts and removes a container from the registry.\n * This function checks if the container is registered, removes it from the DOM,\n * and deletes its reference from the registry.\n * @param {string} id - The unique ID of the container to unmount.\n * @return {void}\n */\nexport const unmountContainer = (id) => {\n if (!registry.has(id)) {\n return;\n }\n\n const containerApi = registry.get(id);\n containerApi.remove();\n registry.delete(id);\n};\n\n/**\n * Displays the login form for guest checkout with authentication options\n * @param {HTMLElement} container - DOM element to render the login form in\n * @returns {Promise<Object>} - The rendered login form component\n */\nexport const renderLoginForm = async (container, { onSuccessCallback } = {}) => renderContainer(\n CONTAINERS.LOGIN_FORM,\n async () => CheckoutProvider.render(LoginForm, {\n name: LOGIN_FORM_NAME,\n autoSync: false,\n onSignInClick: async () => {\n const signInForm = document.createElement('div');\n AuthProvider.render(AuthCombine, {\n signInFormConfig: {\n renderSignUpLink: true,\n onSuccessCallback,\n },\n signUpFormConfig: {\n slots: {\n ...authPrivacyPolicyConsentSlot,\n },\n },\n resetPasswordFormConfig: {},\n })(signInForm);\n showModal(signInForm);\n },\n onSignOutClick: () => {\n authApi.revokeCustomerToken();\n },\n })(container),\n);\n\n/**\n * Renders the shipping address form if it hasn't been rendered already.\n * Utilizes a registry to ensure only one instance is created and reused.\n *\n * @async\n * @param {HTMLElement} container - The DOM element where the shipping address form should be rendered.\n * @param {Object} formRef - React-style ref object to store form reference.\n * @param {Object} cartData - Optional cart data to determine if address exists.\n * @returns {Promise<Object>} A promise that resolves to the API of the rendered shipping address form.\n */\nexport const renderShippingAddressForm = async (container, formRef, cartData = null) => renderContainer(\n CONTAINERS.SHIPPING_ADDRESS_FORM,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n const storeConfig = checkoutApi.getStoreConfigCache();\n const countryCode = storeConfig.defaultCountry;\n\n const estimateShippingCostOnCart = estimateShippingCost({\n debounceMs: DEBOUNCE_TIME,\n });\n\n const notifyValues = debounce((values) => {\n events.emit('checkout/addresses/shipping', values);\n }, DEBOUNCE_TIME);\n\n const hasCartAddress = !!cartData?.shippingAddresses?.[0];\n\n const handleChange = (values) => {\n notifyValues(values);\n if (!hasCartAddress && cartData) {\n estimateShippingCostOnCart(values);\n }\n };\n\n return AccountProvider.render(AddressForm, {\n addressesFormTitle: placeholders?.Checkout?.Addresses?.shippingAddressTitle,\n className: 'checkout-shipping-form__address-form',\n fieldIdPrefix: 'shipping',\n formName: SHIPPING_FORM_NAME,\n forwardFormRef: formRef,\n hideActionFormButtons: true,\n inputsDefaultValueSet: { countryCode },\n isOpen: true,\n onChange: handleChange,\n showBillingCheckBox: false,\n showShippingCheckBox: false,\n })(container);\n },\n);\n\n/**\n * Renders the customer shipping addresses selector/form if it hasn't been rendered already.\n * Displays saved customer addresses with the ability to select one or add a new address.\n * Automatically estimates shipping costs when address data changes for carts without existing addresses.\n * Utilizes a registry to ensure only one instance is created and reused.\n *\n * @async\n * @param {HTMLElement} container - The DOM element where the shipping addresses should be rendered.\n * @param {Object} formRef - React-style ref object to store form reference for external access.\n * @param {Object|null} [data=null] - Optional cart data containing shipping address information.\n * @param {Array} [data.shippingAddresses] - Array of shipping addresses from the cart.\n * @returns {Promise<Object>} A promise that resolves to the API of the rendered shipping addresses component.\n */\nexport const renderCustomerShippingAddresses = async (container, formRef, data = null) => renderContainer(\n CONTAINERS.CUSTOMER_SHIPPING_ADDRESSES,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n const cartShippingAddress = getCartAddress(data, 'shipping');\n\n const shippingAddressId = cartShippingAddress\n ? cartShippingAddress?.id ?? 0\n : undefined;\n\n const storeConfig = checkoutApi.getStoreConfigCache();\n\n const inputsDefaultValueSet = cartShippingAddress && cartShippingAddress.id === undefined\n ? cartShippingAddress\n : { countryCode: storeConfig.defaultCountry };\n\n const hasCartShippingAddress = Boolean(data.shippingAddresses?.[0]);\n\n const estimateShippingCostOnCart = estimateShippingCost({\n debounceMs: DEBOUNCE_TIME,\n });\n\n const notifyValues = debounce((values) => {\n events.emit('checkout/addresses/shipping', values);\n }, DEBOUNCE_TIME);\n\n return AccountProvider.render(Addresses, {\n addressFormTitle: placeholders?.Checkout?.Addresses?.shippingAddressTitle,\n defaultSelectAddressId: shippingAddressId,\n fieldIdPrefix: 'shipping',\n formName: SHIPPING_FORM_NAME,\n forwardFormRef: formRef,\n inputsDefaultValueSet,\n minifiedView: false,\n onAddressData: (values) => {\n notifyValues(values);\n if (!hasCartShippingAddress) estimateShippingCostOnCart(values);\n },\n selectable: true,\n selectShipping: true,\n showBillingCheckBox: false,\n showSaveCheckBox: true,\n showShippingCheckBox: false,\n title: placeholders?.Checkout?.Addresses?.shippingAddressTitle,\n })(container);\n },\n);\n\n/**\n * Renders the billing address form if it hasn't been rendered already.\n * Utilizes a registry to ensure only one instance is created and reused.\n *\n * @async\n * @param {HTMLElement} container - The DOM element where the billing address form should be rendered.\n * @param {Object} formRef - React-style ref object to store form reference.\n * @returns {Promise<Object>} A promise that resolves to the API of the rendered billing address form.\n */\nexport const renderBillingAddressForm = async (container, formRef) => renderContainer(\n CONTAINERS.BILLING_ADDRESS_FORM,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n const storeConfig = checkoutApi.getStoreConfigCache();\n\n const notifyValues = debounce((values) => {\n events.emit('checkout/addresses/billing', values);\n }, DEBOUNCE_TIME);\n\n const handleChange = (values) => {\n notifyValues(values);\n };\n\n return AccountProvider.render(AddressForm, {\n className: 'checkout-billing-form__address-form',\n fieldIdPrefix: 'billing',\n formName: BILLING_FORM_NAME,\n forwardFormRef: formRef,\n hideActionFormButtons: true,\n inputsDefaultValueSet: {\n countryCode: storeConfig.defaultCountry,\n },\n onChange: handleChange,\n isOpen: true,\n showBillingCheckBox: false,\n showShippingCheckBox: false,\n })(container);\n },\n);\n\n/**\n * Renders the customer billing addresses selector/form if it hasn't been rendered already.\n * Displays saved customer addresses with the ability to select one or add a new address for billing.\n * Provides a dropdown/selector interface for choosing from existing addresses or creating new ones.\n * Utilizes a registry to ensure only one instance is created and reused.\n *\n * @async\n * @param {HTMLElement} container - The DOM element where the billing addresses should be rendered.\n * @param {Object} formRef - React-style ref object to store form reference for external access.\n * @param {Object|null} [data=null] - Optional cart data containing billing address information.\n * @param {Array} [data.billingAddress] - Billing address object from the cart.\n * @returns {Promise<Object>} A promise that resolves to the API of the rendered billing addresses component.\n */\nexport const renderCustomerBillingAddresses = async (container, formRef, data = null) => renderContainer(\n CONTAINERS.CUSTOMER_BILLING_ADDRESSES,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n const cartBillingAddress = getCartAddress(data, 'billing');\n\n const billingAddressId = cartBillingAddress\n ? cartBillingAddress?.id ?? 0\n : undefined;\n\n const storeConfig = checkoutApi.getStoreConfigCache();\n\n const inputsDefaultValueSet = cartBillingAddress && cartBillingAddress.id === undefined\n ? cartBillingAddress\n : { countryCode: storeConfig.defaultCountry };\n\n const notifyValues = debounce((values) => {\n events.emit('checkout/addresses/billing', values);\n }, DEBOUNCE_TIME);\n\n return AccountProvider.render(Addresses, {\n addressFormTitle: placeholders?.Checkout?.Addresses?.billToNewAddress,\n defaultSelectAddressId: billingAddressId,\n formName: BILLING_FORM_NAME,\n forwardFormRef: formRef,\n inputsDefaultValueSet,\n minifiedView: false,\n selectable: true,\n selectBilling: true,\n showBillingCheckBox: false,\n showSaveCheckBox: true,\n showShippingCheckBox: false,\n title: placeholders?.Checkout?.Addresses?.billingAddressTitle,\n onAddressData: (values) => {\n notifyValues(values);\n },\n })(container);\n },\n);\n\n/**\n * Displays available shipping methods with toggle button interface\n * @param {HTMLElement} container - DOM element to render shipping methods in\n * @returns {Promise<Object>} - The rendered shipping methods component\n */\nexport const renderShippingMethods = async (container) => renderContainer(\n CONTAINERS.SHIPPING_METHODS,\n async () => CheckoutProvider.render(ShippingMethods, {\n UIComponentType: 'ToggleButton',\n displayTitle: false,\n autoSync: false,\n })(container),\n);\n\n/**\n * Displays payment methods with credit card integration and configuration slots\n * @param {HTMLElement} container - DOM element to render payment methods in\n * @param {Object} creditCardFormRef - React-style ref object for credit card form\n * @returns {Promise<Object>} - The rendered payment methods component\n */\nexport const renderPaymentMethods = async (container, creditCardFormRef) => renderContainer(\n CONTAINERS.PAYMENT_METHODS,\n async () => {\n const commerceCoreEndpoint = getConfigValue('commerce-core-endpoint') || getConfigValue('commerce-endpoint');\n const getCustomerToken = () => getCookie(USER_TOKEN_COOKIE_NAME);\n\n return CheckoutProvider.render(PaymentMethods, {\n UIComponentType: 'RadioButton',\n displayTitle: false,\n autoSync: false,\n slots: {\n Methods: {\n [PaymentMethodCode.CREDIT_CARD]: {\n render: (ctx) => {\n const $content = document.createElement('div');\n\n PaymentServices.render(CreditCard, {\n apiUrl: commerceCoreEndpoint,\n getCustomerToken,\n getCartId: () => ctx.cartId,\n creditCardFormRef,\n })($content);\n\n ctx.replaceHTML($content);\n },\n },\n [PaymentMethodCode.SMART_BUTTONS]: {\n enabled: false,\n },\n [PaymentMethodCode.APPLE_PAY]: {\n enabled: false,\n },\n [PaymentMethodCode.GOOGLE_PAY]: {\n enabled: false,\n },\n [PaymentMethodCode.VAULT]: {\n enabled: false,\n },\n [PaymentMethodCode.FASTLANE]: {\n enabled: false,\n },\n },\n },\n })(container);\n },\n);\n\n/**\n * Displays checkbox to set billing address same as shipping address\n * @param {HTMLElement} container - DOM element to render the checkbox in\n * @returns {Promise<Object>} - The rendered bill to shipping address component\n */\nexport const renderBillToShippingAddress = async (container) => renderContainer(\n CONTAINERS.BILL_TO_SHIPPING,\n async () => CheckoutProvider.render(BillToShippingAddress, {\n autoSync: false,\n })(container),\n);\n\n/**\n * Displays server error handling with retry functionality and error state management\n * @param {HTMLElement} container - DOM element to render the error component in\n * @param {HTMLElement} contentElement - Main content element to add error styling to\n * @returns {Promise<Object>} - The rendered server error component\n */\nexport const renderServerError = async (container, block) => renderContainer(\n CONTAINERS.SERVER_ERROR,\n async () => CheckoutProvider.render(ServerError, {\n autoScroll: true,\n onRetry: () => {\n const $content = block.querySelector(selectors.checkout.content);\n $content.classList.remove(CHECKOUT_ERROR_CLASS);\n },\n onServerError: () => {\n const $content = block.querySelector(selectors.checkout.content);\n $content.classList.add(CHECKOUT_ERROR_CLASS);\n },\n })(container),\n);\n\n/**\n * Displays out of stock handling with cart navigation and product update options\n * @param {HTMLElement} container - DOM element to render the component in\n * @returns {Promise<Object>} - The rendered out-of-stock component\n */\nexport const renderOutOfStock = async (container) => renderContainer(\n CONTAINERS.OUT_OF_STOCK,\n async () => CheckoutProvider.render(OutOfStock, {\n routeCart: () => '/cart',\n onCartProductsUpdate: (items) => {\n cartApi.updateProductsFromCart(items).catch(console.error);\n },\n })(container),\n);\n\n/**\n * Displays cart summary list with item count and edit functionality\n * @param {HTMLElement} container - DOM element to render the cart summary in\n * @returns {Promise<Object>} - The rendered cart summary list component\n */\nexport const renderCartSummaryList = async (container) => renderContainer(\n CONTAINERS.CART_SUMMARY_LIST,\n async () => {\n const placeholders = await fetchPlaceholders('placeholders/checkout.json');\n\n return CartProvider.render(CartSummaryList, {\n variant: 'secondary',\n slots: {\n Heading: (headingCtx) => {\n const title = placeholders?.Checkout?.Summary?.heading;\n\n const cartSummaryListHeading = document.createElement('div');\n cartSummaryListHeading.classList.add('cart-summary-list__heading');\n\n const cartSummaryListHeadingText = document.createElement('div');\n cartSummaryListHeadingText.classList.add(\n 'cart-summary-list__heading-text',\n );\n\n cartSummaryListHeadingText.innerText = title?.replace(\n '({count})',\n headingCtx.count ? `(${headingCtx.count})` : '',\n );\n const editCartLink = document.createElement('a');\n editCartLink.classList.add('cart-summary-list__edit');\n editCartLink.href = '/cart';\n editCartLink.rel = 'noreferrer';\n editCartLink.innerText = placeholders?.Checkout?.Summary?.Edit;\n\n cartSummaryListHeading.appendChild(cartSummaryListHeadingText);\n cartSummaryListHeading.appendChild(editCartLink);\n headingCtx.appendChild(cartSummaryListHeading);\n\n headingCtx.onChange((nextHeadingCtx) => {\n cartSummaryListHeadingText.innerText = title?.replace(\n '({count})',\n nextHeadingCtx.count ? `(${nextHeadingCtx.count})` : '',\n );\n });\n },\n Thumbnail: (ctx) => {\n const { item, defaultImageProps } = ctx;\n tryRenderAemAssetsImage(ctx, {\n alias: item.sku,\n imageProps: defaultImageProps,\n params: {\n width: defaultImageProps.width,\n height: defaultImageProps.height,\n },\n });\n },\n Footer: (ctx) => {\n const giftOptions = document.createElement('div');\n CartProvider.render(GiftOptions, {\n item: ctx.item,\n view: 'product',\n dataSource: 'cart',\n isEditable: false,\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(giftOptions);\n ctx.appendChild(giftOptions);\n },\n },\n })(container);\n },\n);\n\n/**\n * Displays terms and conditions with agreement slots and manual consent mode\n * @param {HTMLElement} container - DOM element to render the terms in\n * @returns {Promise<Object>} - The rendered terms and conditions component\n */\nexport const renderTermsAndConditions = async (container) => renderContainer(\n CONTAINERS.TERMS_AND_CONDITIONS,\n async () => CheckoutProvider.render(TermsAndConditions, {\n slots: {\n Agreements: (ctx) => {\n ctx.appendAgreement(() => ({\n name: 'default',\n mode: 'manual',\n translationId: 'Checkout.TermsAndConditions.label',\n }));\n },\n },\n })(container),\n);\n\n/**\n * Displays estimate shipping form for cost calculation\n * @param {HTMLElement} container - DOM element to render the estimate form in\n * @returns {Object} - The rendered estimate shipping component\n */\nexport const renderEstimateShipping = async (container) => renderContainer(\n CONTAINERS.ESTIMATE_SHIPPING,\n async () => CheckoutProvider.render(EstimateShipping)(container),\n);\n\n/**\n * Displays coupons interface for discount code management\n * @param {HTMLElement} container - DOM element to render the coupons in\n * @returns {Object} - The rendered coupons component\n */\nexport const renderCartCoupons = (container) => renderContainer(\n CONTAINERS.CART_COUPONS,\n async () => CartProvider.render(Coupons)(container),\n);\n\n/**\n * Displays order summary with integrated estimate shipping and coupons slots\n * @param {HTMLElement} container - DOM element to render the order summary in\n * @returns {Promise<Object>} - The rendered order summary component\n */\nexport const renderOrderSummary = async (container) => renderContainer(\n CONTAINERS.ORDER_SUMMARY,\n async () => CartProvider.render(OrderSummary, {\n slots: {\n EstimateShipping: (esCtx) => {\n const estimateShippingForm = document.createElement('div');\n renderEstimateShipping(estimateShippingForm);\n esCtx.appendChild(estimateShippingForm);\n },\n Coupons: (ctx) => {\n const coupons = document.createElement('div');\n renderCartCoupons(coupons);\n ctx.appendChild(coupons);\n },\n GiftOptions: (ctx) => {\n const giftOptions = document.createElement('div');\n CartProvider.render(GiftOptions, {\n view: 'order',\n dataSource: 'cart',\n isEditable: false,\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(giftOptions);\n ctx.appendChild(giftOptions);\n },\n },\n })(container),\n);\n\n/**\n * Displays merged cart banner notification for authenticated users\n * @param {HTMLElement} container - DOM element to render the banner in\n * @returns {Promise<Object>} - The rendered merged cart banner component\n */\nexport const renderMergedCartBanner = async (container) => renderContainer(\n CONTAINERS.MERGED_CART_BANNER,\n async () => CheckoutProvider.render(MergedCartBanner)(container),\n);\n\n/**\n * Displays place order button with comprehensive validation and payment handling\n * @param {HTMLElement} container - DOM element to render the place order button in\n * @param {Object} options - Configuration object\n * @returns {Promise<Object>} - The rendered place order component\n */\nexport const renderPlaceOrder = async (container, options = {}) => renderContainer(\n CONTAINERS.PLACE_ORDER_BUTTON,\n async () => CheckoutProvider.render(PlaceOrder, {\n disabled: true,\n ...options,\n })(container),\n);\n\n/**\n * Updates place order button with new props\n * @param {Object} options - Configuration object\n */\nexport const updatePlaceOrder = async (options = {}) => {\n const button = registry.get(CONTAINERS.PLACE_ORDER_BUTTON);\n\n if (button) {\n button.setProps((prev) => ({\n ...prev,\n ...options,\n }));\n }\n};\n\n/**\n * Displays order confirmation header with email check and sign up integration\n * @param {HTMLElement} container - DOM element to render the order header in\n * @param {Object} options - Configuration object with handlers and order data\n * @returns {Object} - The rendered order header component\n */\nexport const renderOrderHeader = (container, options = {}) => renderContainer(\n CONTAINERS.ORDER_HEADER,\n async () => {\n const handleSignUpClick = async ({\n inputsDefaultValueSet,\n addressesData,\n }) => {\n const signUpForm = document.createElement('div');\n\n AuthProvider.render(SignUp, {\n inputsDefaultValueSet,\n addressesData,\n routeSignIn: () => '/customer/login',\n routeRedirectOnEmailConfirmationClose: () => '/customer/account',\n slots: {\n ...authPrivacyPolicyConsentSlot,\n },\n })(container);\n\n await showModal(signUpForm);\n };\n\n return OrderProvider.render(OrderHeader, {\n handleEmailAvailability: checkoutApi.isEmailAvailable,\n handleSignUpClick,\n ...options,\n })(container);\n },\n);\n\n/**\n * Renders the order status component in the given container.\n * @param {HTMLElement} container - The DOM element to render the order status in.\n * @returns {Object} - The rendered order status component.\n */\nexport const renderOrderStatus = (container) => renderContainer(\n CONTAINERS.ORDER_STATUS,\n async () => OrderProvider.render(OrderStatus, { slots: { OrderActions: () => null } })(container),\n);\n\n/**\n * Renders the shipping status component in the given container.\n * @param {HTMLElement} container - The DOM element to render the shipping status in.\n * @returns {Object} - The rendered shipping status component.\n */\nexport const renderShippingStatus = (container) => renderContainer(\n CONTAINERS.SHIPPING_STATUS,\n async () => OrderProvider.render(ShippingStatus)(container),\n);\n\n/**\n * Renders the customer details component in the given container.\n * @param {HTMLElement} container - The DOM element to render the customer details in.\n * @returns {Object} - The rendered customer details component.\n */\nexport const renderCustomerDetails = (container) => renderContainer(\n CONTAINERS.CUSTOMER_DETAILS,\n async () => OrderProvider.render(CustomerDetails)(container),\n);\n\n/**\n * Renders the order cost summary component in the given container.\n * @param {HTMLElement} container - The DOM element to render the order cost summary in.\n * @returns {Object} - The rendered order cost summary component.\n */\nexport const renderOrderCostSummary = (container) => renderContainer(\n CONTAINERS.ORDER_COST_SUMMARY,\n async () => OrderProvider.render(OrderCostSummary)(container),\n);\n\n/**\n * Renders the order product list component in the given container.\n * @param {HTMLElement} container - The DOM element to render the order product list in.\n * @returns {Object} - The rendered order product list component.\n */\nexport const renderOrderProductList = (container) => renderContainer(\n CONTAINERS.ORDER_PRODUCT_LIST,\n async () => OrderProvider.render(OrderProductList, {\n slots: {\n Footer: (ctx) => {\n const giftOptions = document.createElement('div');\n CartProvider.render(GiftOptions, {\n item: ctx.item,\n view: 'product',\n dataSource: 'order',\n isEditable: false,\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(giftOptions);\n ctx.appendChild(giftOptions);\n },\n CartSummaryItemImage: (ctx) => {\n const { data, defaultImageProps } = ctx;\n tryRenderAemAssetsImage(ctx, {\n alias: data.product.sku,\n imageProps: defaultImageProps,\n params: {\n width: defaultImageProps.width,\n height: defaultImageProps.height,\n },\n });\n },\n },\n })(container),\n);\n",
|
|
350
|
+
"fragments.js": "// Dropin Lib Functions\nimport { createFragment, createScopedSelector } from '@dropins/storefront-checkout/lib/utils.js';\n\nimport {\n CHECKOUT_BLOCK,\n CHECKOUT_STEP,\n CHECKOUT_STEP_BUTTON,\n CHECKOUT_STEP_CONTENT,\n CHECKOUT_STEP_TITLE,\n CHECKOUT_STEP_SUMMARY,\n ORDER_CONFIRMATION_BLOCK,\n} from './constants.js';\n\n/**\n * A frozen, nested object of CSS selectors\n * @readonly\n */\nexport const selectors = Object.freeze({\n checkout: {\n aside: '.checkout__aside',\n billingForm: '.checkout__billing-form',\n billingFormSummary: '.checkout__billing-form-summary',\n billingStep: '.checkout__billing-address',\n billingStepContinueBtn: '.checkout__continue-to-place-order',\n billingStepTitle: '.checkout__billing-title',\n billToShipping: '.checkout__bill-to-shipping',\n cartSummary: '.checkout__cart-summary',\n content: '.checkout__content',\n editSummaryBtn: '.checkout__summary-edit',\n header: '.checkout__header',\n loader: '.checkout__loader',\n loginForm: '.checkout__login',\n loginFormSummary: '.checkout__login-form-summary',\n main: '.checkout__main',\n mergedCartBanner: '.checkout__merged-cart-banner',\n orderSummary: '.checkout__order-summary',\n outOfStock: '.checkout__out-of-stock',\n paymentMethodsList: '.checkout__payment-methods-list',\n paymentMethodsSummary: '.checkout__payment-methods-summary',\n paymentStep: '.checkout__payment-methods',\n paymentStepContinueBtn: '.checkout__continue-to-billing',\n paymentStepTitle: '.checkout__payment-title',\n placeOrder: '.checkout__place-order',\n serverError: '.checkout__server-error',\n shippingAddressForm: '.checkout__shipping-form',\n shippingAddressFormSummary: '.checkout__shipping-form-summary',\n shippingMethodContinueBtn: '.checkout__continue-to-payment',\n shippingMethodList: '.checkout__shipping-methods-list',\n shippingMethodSummary: '.checkout__shipping-methods-summary',\n shippingMethodStep: '.checkout__shipping-methods',\n shippingMethodStepTitle: '.checkout__shipping-methods-title',\n shippingStep: '.checkout__shipping-address',\n shippingStepContinueBtn: '.checkout__continue-to-shipping-methods',\n termsAndConditions: '.checkout__terms-and-conditions',\n },\n orderConfirmation: {\n contactSupportLink: '.order-confirmation-footer__contact-support-link',\n continueBtn: '.order-confirmation-footer__continue-button',\n customerDetails: '.order-confirmation__customer-details',\n header: '.order-confirmation__header',\n orderCostSummary: '.order-confirmation__order-cost-summary',\n orderProductList: '.order-confirmation__order-product-list',\n orderStatus: '.order-confirmation__order-status',\n shippingStatus: '.order-confirmation__shipping-status',\n },\n});\n\n// =============================================================================\n// UTILITIES\n// Helper functions for creating and querying DOM elements.\n// =============================================================================\n\n// =============================================================================\n// CHECKOUT\n// =============================================================================\n\n/**\n * Creates the shipping address fragment for the checkout.\n * @returns {DocumentFragment} The shipping address fragment.\n */\nfunction createShippingStepFragment() {\n return createFragment(`\n <div class=\"checkout__login ${CHECKOUT_BLOCK} ${CHECKOUT_STEP_CONTENT}\"></div>\n <div class=\"checkout__login-form-summary ${CHECKOUT_BLOCK} ${CHECKOUT_STEP_SUMMARY}\"></div>\n <div class=\"checkout__shipping-form ${CHECKOUT_BLOCK} ${CHECKOUT_STEP_CONTENT}\"></div>\n <div class=\"checkout__shipping-form-summary ${CHECKOUT_BLOCK} ${CHECKOUT_STEP_SUMMARY}\"></div>\n <div class=\"checkout__continue-to-shipping-methods ${CHECKOUT_BLOCK} ${CHECKOUT_STEP_CONTENT} ${CHECKOUT_STEP_BUTTON}\"></div>\n `);\n}\n\n/**\n * Creates the shipping methods fragment for the checkout.\n * @returns {DocumentFragment} The shipping methods fragment.\n */\nfunction createShippingMethodsStepFragment() {\n return createFragment(`\n <div class=\"checkout__shipping-methods-title ${CHECKOUT_BLOCK} ${CHECKOUT_STEP_TITLE}\"></div>\n <div class=\"checkout__shipping-methods-list ${CHECKOUT_BLOCK} ${CHECKOUT_STEP_CONTENT}\"></div>\n <div class=\"checkout__shipping-methods-summary ${CHECKOUT_BLOCK} ${CHECKOUT_STEP_SUMMARY}\"></div>\n <div class=\"checkout__continue-to-payment ${CHECKOUT_BLOCK} ${CHECKOUT_STEP_CONTENT} ${CHECKOUT_STEP_BUTTON}\"></div>\n `);\n}\n\n/**\n * Creates the payment methods fragment for the checkout.\n * @returns {DocumentFragment} The payment methods fragment.\n */\nfunction createPaymentMethodsStepFragment() {\n return createFragment(`\n <div class=\"checkout__payment-title ${CHECKOUT_BLOCK} ${CHECKOUT_STEP_TITLE}\"></div>\n <div class=\"checkout__payment-methods-list ${CHECKOUT_BLOCK} ${CHECKOUT_STEP_CONTENT}\"></div>\n <div class=\"checkout__bill-to-shipping ${CHECKOUT_BLOCK} ${CHECKOUT_STEP_CONTENT}\"></div>\n <div class=\"checkout__payment-methods-summary ${CHECKOUT_BLOCK} ${CHECKOUT_STEP_SUMMARY}\"></div>\n <div class=\"checkout__continue-to-billing ${CHECKOUT_BLOCK} ${CHECKOUT_STEP_CONTENT} ${CHECKOUT_STEP_BUTTON}\"></div>\n `);\n}\n\n/**\n * Creates the billing address fragment for the checkout.\n * @returns {DocumentFragment} The billing address fragment.\n */\nfunction createBillingAddressStepFragment() {\n return createFragment(`\n <div class=\"checkout__billing-title ${CHECKOUT_BLOCK} ${CHECKOUT_STEP_TITLE}\"></div>\n <div class=\"checkout__billing-form ${CHECKOUT_BLOCK} ${CHECKOUT_STEP_CONTENT}\"></div>\n <div class=\"checkout__billing-form-summary ${CHECKOUT_BLOCK} ${CHECKOUT_STEP_SUMMARY}\"></div>\n <div class=\"checkout__continue-to-place-order ${CHECKOUT_BLOCK} ${CHECKOUT_STEP_CONTENT} ${CHECKOUT_STEP_BUTTON}\"></div>\n `);\n}\n\n/**\n * Creates the main fragment for the checkout, containing all steps.\n * @returns {DocumentFragment} The main checkout fragment.\n */\nfunction createMainFragment() {\n const mainFragment = createFragment(`\n <div class=\"checkout__header ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__server-error ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__out-of-stock ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__shipping-address ${CHECKOUT_BLOCK} ${CHECKOUT_STEP}\"></div>\n <div class=\"checkout__shipping-methods ${CHECKOUT_BLOCK} ${CHECKOUT_STEP}\"></div>\n <div class=\"checkout__payment-methods ${CHECKOUT_BLOCK} ${CHECKOUT_STEP}\"></div>\n <div class=\"checkout__billing-address ${CHECKOUT_BLOCK} ${CHECKOUT_STEP}\"></div>\n <div class=\"checkout__terms-and-conditions ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__place-order ${CHECKOUT_BLOCK}\"></div>\n `);\n\n const { checkout } = selectors;\n\n const getMainElement = createScopedSelector(mainFragment);\n\n const shippingStepFragment = getMainElement(checkout.shippingStep);\n const shippingMethodsStepFragment = getMainElement(checkout.shippingMethodStep);\n const paymentMethodsStepFragment = getMainElement(checkout.paymentStep);\n const billingAddressStepFragment = getMainElement(checkout.billingStep);\n\n shippingStepFragment.appendChild(createShippingStepFragment());\n shippingMethodsStepFragment.appendChild(createShippingMethodsStepFragment());\n paymentMethodsStepFragment.appendChild(createPaymentMethodsStepFragment());\n billingAddressStepFragment.appendChild(createBillingAddressStepFragment());\n\n return mainFragment;\n}\n\n/**\n * Creates the aside fragment for the checkout, containing order and cart summary.\n * @returns {DocumentFragment} The aside checkout fragment.\n */\nfunction createAsideFragment() {\n return createFragment(`\n <div class=\"checkout__order-summary ${CHECKOUT_BLOCK}\"></div>\n <div class=\"checkout__cart-summary ${CHECKOUT_BLOCK}\"></div>\n `);\n}\n\n/**\n * Creates the root checkout fragment, including main and aside fragments.\n * @returns {DocumentFragment} The complete checkout fragment.\n */\nexport function createCheckoutFragment() {\n const checkoutFragment = createFragment(`\n <div class=\"checkout__wrapper\">\n <div class=\"checkout__loader\"></div>\n <div class=\"checkout__content\">\n <div class=\"checkout__merged-cart-banner\"></div>\n <div class=\"checkout__main\"></div>\n <div class=\"checkout__aside\"></div>\n </div>\n </div>\n `);\n\n const { checkout } = selectors;\n\n const mainFragment = checkoutFragment.querySelector(checkout.main);\n const asideFragment = checkoutFragment.querySelector(checkout.aside);\n\n mainFragment.appendChild(createMainFragment());\n asideFragment.appendChild(createAsideFragment());\n\n return checkoutFragment;\n}\n\n/**\n * Creates a summary fragment with optional edit button.\n * @param {Element|DocumentFragment} content - The content to display in the summary.\n * @param {Function|null} [onEditClick=null] - Callback for edit button click.\n * @returns {HTMLElement} The summary element.\n */\nexport const createSummary = (content, onEditClick = null) => {\n const summaryDiv = document.createElement('div');\n summaryDiv.className = 'checkout__summary checkout__summary--inline';\n\n const contentDiv = document.createElement('div');\n contentDiv.className = 'checkout__summary-content';\n contentDiv.appendChild(content);\n\n summaryDiv.appendChild(contentDiv);\n\n if (onEditClick) {\n const editBtn = document.createElement('button');\n editBtn.className = 'checkout__summary-edit';\n editBtn.type = 'button';\n editBtn.textContent = 'Edit';\n editBtn.addEventListener('click', onEditClick);\n summaryDiv.appendChild(editBtn);\n }\n\n return summaryDiv;\n};\n\n/**\n * Creates an address summary element.\n * @param {Object} [data={}] - Address data.\n * @param {Function|null} [onEditClick=null] - Callback for edit button click.\n * @returns {HTMLElement} The address summary element.\n */\nexport function createAddressSummary(data = {}, onEditClick = null) {\n const {\n firstName = '',\n lastName = '',\n street = [],\n city = '',\n region, // Region object: { code, name } | undefined\n postCode = '',\n country, // Country object: { code, label } | undefined\n telephone = '',\n } = data;\n\n const streetAddress = Array.isArray(street) ? street.join(', ') : street;\n const regionCode = region?.code || region?.name || '';\n const countryCode = country?.code || '';\n\n const detailsDiv = document.createElement('div');\n detailsDiv.className = 'checkout__address-summary-details';\n\n const nameDiv = document.createElement('div');\n nameDiv.textContent = `${firstName} ${lastName}`.trim();\n detailsDiv.appendChild(nameDiv);\n\n const streetDiv = document.createElement('div');\n streetDiv.textContent = streetAddress;\n detailsDiv.appendChild(streetDiv);\n\n const cityDiv = document.createElement('div');\n cityDiv.textContent = [city, regionCode].filter(Boolean).join(', ') + (postCode ? ` ${postCode}` : '');\n detailsDiv.appendChild(cityDiv);\n\n const countryDiv = document.createElement('div');\n countryDiv.textContent = countryCode;\n detailsDiv.appendChild(countryDiv);\n\n const telDiv = document.createElement('div');\n telDiv.textContent = telephone;\n detailsDiv.appendChild(telDiv);\n\n const contentDiv = document.createElement('div');\n contentDiv.className = 'checkout__address-summary-content';\n contentDiv.appendChild(detailsDiv);\n\n return createSummary(contentDiv, onEditClick);\n}\n\n/**\n * Creates a login form summary element.\n * @param {string} email - The email address to display.\n * @param {Function | null} onEditClick - Callback function for the edit button click.\n * @returns {HTMLElement} The created login summary element.\n */\nexport function createLoginFormSummary(email = '', onEditClick = null) {\n const span = document.createElement('span');\n span.className = 'checkout__login-form-summary-email';\n span.textContent = email;\n\n const content = document.createElement('div');\n content.className = 'checkout__login-form-summary-content';\n content.appendChild(span);\n\n return createSummary(content, onEditClick);\n}\n\n/**\n * Creates a shipping methods summary element.\n * @param {Object} [data={}] - Shipping method data.\n * @param {Function|null} [onEditClick=null] - Callback for edit button click.\n * @returns {HTMLElement} The created shipping methods summary element.\n */\nexport const createShippingMethodsSummary = (data = {}, onEditClick = null) => {\n const content = document.createElement('div');\n content.className = 'checkout__shipping-methods-summary-content';\n\n const label = document.createElement('span');\n label.className = 'checkout__shipping-methods-summary-label';\n label.textContent = data.label || '';\n content.appendChild(label);\n\n if (data.description) {\n const desc = document.createElement('span');\n desc.className = 'checkout__shipping-methods-summary-description';\n desc.textContent = data.description;\n content.appendChild(desc);\n }\n\n return createSummary(content, onEditClick);\n};\n\n/**\n * Creates a payment methods summary element.\n * @param {Object} [data={}] - Payment method data.\n * @param {Function|null} [onEditClick=null] - Callback for edit button click.\n * @returns {HTMLElement} The created payment methods summary element.\n */\nexport const createPaymentMethodsSummary = (data = {}, onEditClick = null) => {\n const container = document.createElement('div');\n container.className = 'checkout__payment-methods-summary-details';\n\n const labelSpan = document.createElement('span');\n labelSpan.className = 'checkout__payment-methods-summary-label';\n labelSpan.textContent = data.title || '';\n container.appendChild(labelSpan);\n\n return createSummary(container, onEditClick);\n};\n",
|
|
351
|
+
"steps/billing-address.js": "/* eslint-disable import/no-unresolved */\n/* eslint-disable no-unused-vars */\n/* eslint-disable import/prefer-default-export */\n\n// Dropin Tools\nimport { events } from '@dropins/tools/event-bus.js';\n\n// Checkout Dropin\nimport * as checkoutApi from '@dropins/storefront-checkout/api.js';\n\n// Dropin Lib Functions\nimport {\n getCartAddress,\n transformAddressFormValuesToAddressInput,\n validateForm,\n} from '@dropins/storefront-checkout/lib/utils.js';\n\n// Container functions\nimport {\n renderBillingAddressForm, renderCustomerBillingAddresses,\n} from '../containers.js';\n\n// Components\nimport {\n COMPONENT_IDS,\n renderStepContinueBtn,\n} from '../components.js';\n\n// Fragments\nimport {\n createAddressSummary,\n selectors,\n} from '../fragments.js';\n\n// Constants\nimport {\n BILLING_FORM_NAME, CHECKOUT_STEP_ACTIVE, DEFAULT_IS_BILL_TO_SHIPPING,\n} from '../constants.js';\n\n/**\n * Creates billing address step management functions\n */\nexport const createBillingAddressStep = ({\n disablePlaceOrderButton,\n formRefs,\n getElement,\n isAuthenticated,\n withOverlaySpinner,\n}) => {\n // Billing address specific DOM elements\n const { checkout } = selectors;\n\n const elements = {\n $billingForm: getElement(checkout.billingForm),\n $billingFormSummary: getElement(checkout.billingFormSummary),\n $billingStep: getElement(checkout.billingStep),\n $billingStepContinueBtn: getElement(checkout.billingStepContinueBtn),\n };\n\n const continueFromBillingStep = withOverlaySpinner(async () => {\n const checkoutValues = events.lastPayload('checkout/values');\n const isBillToShipping = checkoutValues?.isBillToShipping ?? DEFAULT_IS_BILL_TO_SHIPPING;\n\n if (!isBillToShipping) {\n if (!validateForm({ name: BILLING_FORM_NAME, ref: formRefs.billingForm })) return;\n\n // Get billing address from form ref first, fall back to events if form is unmounted\n const billingAddress = formRefs.billingForm.current?.formData\n || events.lastPayload('checkout/addresses/billing')?.data;\n\n try {\n // eslint-disable-next-line max-len\n await checkoutApi.setBillingAddress(transformAddressFormValuesToAddressInput(billingAddress));\n\n await displayBillingStep(false);\n } catch (error) {\n console.error('Failed to set billing address:', error);\n return;\n }\n\n if (billingAddress) {\n await displayBillingStepSummary(billingAddress, !isBillToShipping);\n }\n } else {\n await displayBillingStep(false);\n }\n\n events.emit('checkout/step/completed', null);\n });\n\n async function displayBillingStep(active = true, data = null) {\n if (isAuthenticated()) {\n await renderCustomerBillingAddresses(\n elements.$billingForm,\n formRefs.billingForm,\n data,\n );\n } else {\n await renderBillingAddressForm(\n elements.$billingForm,\n formRefs.billingForm,\n );\n }\n\n await renderStepContinueBtn(\n elements.$billingStepContinueBtn,\n COMPONENT_IDS.BILLING_STEP_CONTINUE_BTN,\n continueFromBillingStep,\n );\n\n elements.$billingStep.classList.toggle(CHECKOUT_STEP_ACTIVE, active);\n }\n\n async function displayBillingStepSummary(data, showEditLink = true) {\n const handleEdit = showEditLink ? async () => {\n disablePlaceOrderButton();\n await displayBillingStep(true);\n } : null;\n\n const summary = createAddressSummary(data, handleEdit);\n elements.$billingFormSummary.innerHTML = '';\n elements.$billingFormSummary.appendChild(summary);\n\n elements.$billingStep.classList.remove(CHECKOUT_STEP_ACTIVE);\n }\n\n const isBillingStepComplete = (data) => {\n const cartBillingAddress = getCartAddress(data, 'billing');\n const sameAsBilling = data?.shippingAddresses?.[0]?.sameAsBilling;\n return !!(cartBillingAddress || sameAsBilling);\n };\n\n const isBillingStepActive = () => elements.$billingStep.classList.contains(CHECKOUT_STEP_ACTIVE);\n\n return {\n continue: continueFromBillingStep,\n display: displayBillingStep,\n displaySummary: displayBillingStepSummary,\n isActive: isBillingStepActive,\n isComplete: isBillingStepComplete,\n };\n};\n",
|
|
352
|
+
"steps/payment-methods.js": "/* eslint-disable import/no-unresolved */\n/* eslint-disable no-unused-vars */\n/* eslint-disable import/prefer-default-export */\n\n// Dropin Tools\nimport { events } from '@dropins/tools/event-bus.js';\n\n// Checkout Dropin\nimport * as checkoutApi from '@dropins/storefront-checkout/api.js';\n\n// Payment Services Dropin\nimport { PaymentMethodCode } from '@dropins/storefront-payment-services/api.js';\n\n// Library utils\nimport { getCartPaymentMethod } from '@dropins/storefront-checkout/lib/utils.js';\n\n// Container functions\nimport {\n renderPaymentMethods,\n renderBillToShippingAddress,\n} from '../containers.js';\n\n// Components\nimport {\n COMPONENT_IDS,\n renderStepContinueBtn,\n} from '../components.js';\n\n// Fragments\nimport {\n createPaymentMethodsSummary,\n selectors,\n} from '../fragments.js';\n\n// Constants\nimport {\n CHECKOUT_STEP_ACTIVE,\n} from '../constants.js';\n\n/**\n * Creates payment methods step management functions\n */\nexport const createPaymentMethodsStep = ({\n disablePlaceOrderButton,\n displayBillingStep,\n displayBillingStepSummary,\n formRefs,\n getElement,\n withOverlaySpinner,\n}) => {\n // Payment methods specific DOM elements\n const { checkout } = selectors;\n const elements = {\n $paymentMethodsList: getElement(checkout.paymentMethodsList),\n $paymentMethodsSummary: getElement(checkout.paymentMethodsSummary),\n $paymentStep: getElement(checkout.paymentStep),\n $paymentStepContinueBtn: getElement(checkout.paymentStepContinueBtn),\n $billToShipping: getElement(checkout.billToShipping),\n };\n\n function hasRequiredFields(method) {\n if (!method) return false;\n return method.code !== '' && method.title !== '';\n }\n\n const continueFromPaymentStep = withOverlaySpinner(async () => {\n const checkoutValues = events.lastPayload('checkout/values');\n\n // Validate payment method, if failed, abort continuation\n if (checkoutValues?.selectedPaymentMethod?.code === PaymentMethodCode.CREDIT_CARD) {\n if (!formRefs.creditCardForm?.current) {\n console.error('DEBUG: Credit card form not rendered.');\n return;\n }\n\n try {\n if (!formRefs.creditCardForm.current.validate()) {\n return;\n }\n } catch (error) {\n console.error('DEBUG: Error during credit card validation:', error);\n return;\n }\n }\n\n const selectedPaymentMethod = checkoutValues?.selectedPaymentMethod;\n\n if (selectedPaymentMethod?.code) {\n try {\n await checkoutApi.setPaymentMethod({ code: selectedPaymentMethod.code });\n\n // Make the payment methods step inactive since it's now completed\n await displayPaymentStep(false);\n // Show the payment summary\n await displayPaymentStepSummary(selectedPaymentMethod);\n } catch (error) {\n console.error('Failed to set payment method:', error);\n return;\n }\n }\n\n if (checkoutValues?.isBillToShipping) {\n // Bill to shipping address is checked - set billing address and show summary\n try {\n await checkoutApi.setBillingAddress({ sameAsShipping: true });\n\n // Use shipping address data for billing summary since they're the same\n const { data: shippingAddress } = events.lastPayload('checkout/addresses/shipping');\n\n if (shippingAddress) {\n await displayBillingStepSummary(shippingAddress, false);\n }\n } catch (error) {\n console.error('Failed to set billing address (same as shipping):', error);\n return;\n }\n } else {\n // Bill to shipping address is NOT checked - show billing form\n await displayBillingStep(true);\n }\n\n events.emit('checkout/step/completed', null);\n });\n\n async function displayPaymentStep(active = true) {\n await renderPaymentMethods(\n elements.$paymentMethodsList,\n formRefs.creditCardForm,\n );\n\n await renderBillToShippingAddress(elements.$billToShipping);\n\n await renderStepContinueBtn(\n elements.$paymentStepContinueBtn,\n COMPONENT_IDS.PAYMENT_STEP_CONTINUE_BTN,\n continueFromPaymentStep,\n );\n\n elements.$paymentStep.classList.toggle(CHECKOUT_STEP_ACTIVE, active);\n }\n\n async function displayPaymentStepSummary(paymentMethod = null) {\n if (!hasRequiredFields(paymentMethod)) {\n return;\n }\n\n const handleEdit = async () => {\n disablePlaceOrderButton();\n await displayPaymentStep(true);\n };\n\n const summary = createPaymentMethodsSummary(paymentMethod, handleEdit);\n elements.$paymentMethodsSummary.innerHTML = '';\n elements.$paymentMethodsSummary.appendChild(summary);\n\n elements.$paymentStep.classList.remove(CHECKOUT_STEP_ACTIVE);\n }\n\n const isPaymentStepComplete = (data) => {\n const paymentMethod = getCartPaymentMethod(data);\n return !!paymentMethod;\n };\n\n const isPaymentStepActive = () => elements.$paymentStep.classList.contains(CHECKOUT_STEP_ACTIVE);\n\n return {\n continue: continueFromPaymentStep,\n display: displayPaymentStep,\n displaySummary: displayPaymentStepSummary,\n isActive: isPaymentStepActive,\n isComplete: isPaymentStepComplete,\n };\n};\n",
|
|
353
|
+
"steps/shipping-methods.js": "/* eslint-disable import/no-unresolved */\n/* eslint-disable no-unused-vars */\n/* eslint-disable import/prefer-default-export */\n\n// Dropin Tools\nimport { events } from '@dropins/tools/event-bus.js';\n\n// Checkout Dropin\nimport * as checkoutApi from '@dropins/storefront-checkout/api.js';\nimport {\n getCartShippingMethod,\n isVirtualCart,\n} from '@dropins/storefront-checkout/lib/utils.js';\n\n// Container functions\nimport {\n renderShippingMethods,\n} from '../containers.js';\n\n// Components\nimport {\n COMPONENT_IDS,\n renderStepContinueBtn,\n} from '../components.js';\n\n// Fragments\nimport {\n createShippingMethodsSummary,\n selectors,\n} from '../fragments.js';\n\n// Constants\nimport {\n CHECKOUT_STEP_ACTIVE,\n} from '../constants.js';\n\n/**\n * Creates shipping methods step management functions\n */\nexport const createShippingMethodsStep = ({\n disablePlaceOrderButton,\n displayPaymentStep,\n getElement,\n withOverlaySpinner,\n}) => {\n // Shipping methods specific DOM elements\n const { checkout } = selectors;\n const elements = {\n $shippingMethodStep: getElement(checkout.shippingMethodStep),\n $shippingMethodList: getElement(checkout.shippingMethodList),\n $shippingMethodContinueBtn: getElement(checkout.shippingMethodContinueBtn),\n $shippingMethodSummary: getElement(checkout.shippingMethodSummary),\n };\n\n const continueFromShippingMethodStep = withOverlaySpinner(async () => {\n const checkoutValues = events.lastPayload('checkout/values');\n\n // set shipping method on cart\n const shippingMethod = checkoutValues?.selectedShippingMethod;\n\n if (shippingMethod?.value) {\n try {\n await checkoutApi.setShippingMethodsOnCart([{\n carrier_code: shippingMethod.carrier.code,\n method_code: shippingMethod.code,\n }]);\n } catch (error) {\n console.error('Failed to set shipping method:', error);\n return;\n }\n }\n\n await displayShippingMethodStep(false);\n await displayShippingMethodStepSummary(shippingMethod);\n await displayPaymentStep(true);\n\n events.emit('checkout/step/completed', null);\n });\n\n async function displayShippingMethodStep(active = true) {\n await renderShippingMethods(elements.$shippingMethodList);\n\n await renderStepContinueBtn(\n elements.$shippingMethodContinueBtn,\n COMPONENT_IDS.SHIPPING_METHOD_STEP_CONTINUE_BTN,\n continueFromShippingMethodStep,\n );\n\n elements.$shippingMethodStep.classList.toggle(CHECKOUT_STEP_ACTIVE, active);\n }\n\n async function displayShippingMethodStepSummary(shippingMethod = null) {\n if (!shippingMethod) {\n console.warn('No shipping method available for summary');\n return;\n }\n\n // Transform shipping method data to match expected format\n const summaryData = {\n label: shippingMethod.carrier?.title || shippingMethod.carrier?.code || 'Unknown Carrier',\n description: shippingMethod.title || shippingMethod.description || '',\n };\n\n const handleEdit = async () => {\n disablePlaceOrderButton();\n await displayShippingMethodStep(true);\n };\n\n // Create and append the summary\n const summary = createShippingMethodsSummary(summaryData, handleEdit);\n elements.$shippingMethodSummary.innerHTML = '';\n elements.$shippingMethodSummary.appendChild(summary);\n\n elements.$shippingMethodStep.classList.remove(CHECKOUT_STEP_ACTIVE);\n }\n\n function isShippingMethodStepComplete(data) {\n if (isVirtualCart(data)) return true;\n const shippingMethod = getCartShippingMethod(data);\n return !!shippingMethod;\n }\n\n function isShippingMethodStepActive() {\n return elements.$shippingMethodStep.classList.contains(CHECKOUT_STEP_ACTIVE);\n }\n\n return {\n continue: continueFromShippingMethodStep,\n display: displayShippingMethodStep,\n displaySummary: displayShippingMethodStepSummary,\n isActive: isShippingMethodStepActive,\n isComplete: isShippingMethodStepComplete,\n };\n};\n",
|
|
354
|
+
"steps/shipping.js": "/* eslint-disable import/no-unresolved */\n/* eslint-disable no-unused-vars */\n/* eslint-disable import/prefer-default-export */\n\n// Dropin Tools\nimport { events } from '@dropins/tools/event-bus.js';\n\n// Checkout Dropin\nimport * as checkoutApi from '@dropins/storefront-checkout/api.js';\n\n// Dropin Lib Functions\nimport {\n getCartAddress,\n isVirtualCart,\n transformAddressFormValuesToAddressInput,\n validateForm,\n} from '@dropins/storefront-checkout/lib/utils.js';\n\n// Container functions\nimport {\n CONTAINERS,\n renderCustomerShippingAddresses,\n renderLoginForm,\n renderShippingAddressForm,\n unmountContainer,\n} from '../containers.js';\n\n// Components\nimport {\n COMPONENT_IDS,\n renderStepContinueBtn,\n} from '../components.js';\n\n// Fragments\nimport {\n createAddressSummary,\n createLoginFormSummary,\n selectors,\n} from '../fragments.js';\n\n// Constants\nimport {\n CHECKOUT_STEP_ACTIVE,\n LOGIN_FORM_NAME,\n SHIPPING_FORM_NAME,\n} from '../constants.js';\n\n/**\n * Creates shipping step management functions\n */\nexport const createShippingStep = ({\n activateStep,\n disablePlaceOrderButton,\n displayPaymentStep,\n displayShippingMethodStep,\n formRefs,\n getElement,\n isAuthenticated,\n withOverlaySpinner,\n}) => {\n // Shipping-specific DOM elements\n const { checkout } = selectors;\n\n const elements = {\n $loginForm: getElement(checkout.loginForm),\n $loginFormSummary: getElement(checkout.loginFormSummary),\n $shippingAddressForm: getElement(checkout.shippingAddressForm),\n $shippingAddressFormSummary: getElement(checkout.shippingAddressFormSummary),\n $shippingStep: getElement(checkout.shippingStep),\n $shippingStepContinueBtn: getElement(checkout.shippingStepContinueBtn),\n };\n\n const isActiveCartVirtual = () => {\n const cart = events.lastPayload('checkout/updated');\n return isVirtualCart(cart);\n };\n\n const continueFromShippingStep = withOverlaySpinner(async () => {\n const authenticated = isAuthenticated();\n\n const checkoutValues = events.lastPayload('checkout/values');\n const email = checkoutValues?.email;\n\n if (!authenticated) {\n if (!validateForm({ name: LOGIN_FORM_NAME })) return;\n\n try {\n await checkoutApi.setGuestEmailOnCart(email);\n } catch (error) {\n console.error('Error setting guest email on cart:', error);\n return;\n }\n }\n\n if (isActiveCartVirtual()) {\n // Virtual products: skip shipping address validation and go directly to payment\n await displayShippingStep(false);\n await displayShippingStepSummary(email, null); // No shipping address for virtual\n await displayPaymentStep(true);\n return;\n }\n\n // Physical products: handle shipping address validation\n if (!validateForm({ name: SHIPPING_FORM_NAME, ref: formRefs.shippingForm })) return;\n\n // Get shipping address from form ref first, fall back to events if form is unmounted\n const shippingAddress = formRefs.shippingForm.current?.formData\n || events.lastPayload('checkout/addresses/shipping')?.data;\n\n try {\n // eslint-disable-next-line max-len\n await checkoutApi.setShippingAddress(transformAddressFormValuesToAddressInput(shippingAddress));\n } catch (error) {\n console.error('Failed to set email and shipping address:', error);\n return;\n }\n\n await displayShippingStep(false);\n await displayShippingStepSummary(email, shippingAddress);\n await displayShippingMethodStep(true);\n\n events.emit('checkout/step/completed', null);\n });\n\n async function displayShippingStep(active = true, data = null) {\n activateStep(elements.$shippingStep, active);\n\n await renderLoginForm(elements.$loginForm, {\n onSuccessCallback: withOverlaySpinner(() => new Promise((resolve) => {\n const listener = events.on('checkout/updated', () => {\n listener.off();\n resolve();\n });\n })),\n });\n\n if (!isActiveCartVirtual()) {\n if (isAuthenticated()) {\n await renderCustomerShippingAddresses(\n elements.$shippingAddressForm,\n formRefs.shippingForm,\n data,\n );\n } else {\n await renderShippingAddressForm(\n elements.$shippingAddressForm,\n formRefs.shippingForm,\n data,\n );\n }\n } else {\n // For virtual products, unmount any existing shipping forms and clear container\n unmountContainer(CONTAINERS.SHIPPING_ADDRESS_FORM);\n unmountContainer(CONTAINERS.CUSTOMER_SHIPPING_ADDRESSES);\n formRefs.shippingForm.current = null;\n elements.$shippingAddressForm.innerHTML = '';\n }\n\n await renderStepContinueBtn(\n elements.$shippingStepContinueBtn,\n COMPONENT_IDS.SHIPPING_STEP_CONTINUE_BTN,\n continueFromShippingStep,\n );\n }\n\n async function displayShippingStepSummary(email, shippingAddress = null) {\n const handleEdit = async () => {\n disablePlaceOrderButton();\n await displayShippingStep(true, events.lastPayload('checkout/updated'));\n };\n\n const loginFormSummary = createLoginFormSummary(email, handleEdit);\n elements.$loginFormSummary.innerHTML = '';\n elements.$loginFormSummary.appendChild(loginFormSummary);\n\n // Only show shipping address summary for physical products\n if (shippingAddress) {\n const shippingAddressFormSummary = createAddressSummary(shippingAddress, handleEdit);\n elements.$shippingAddressFormSummary.innerHTML = '';\n elements.$shippingAddressFormSummary.appendChild(shippingAddressFormSummary);\n }\n\n elements.$shippingStep.classList.remove(CHECKOUT_STEP_ACTIVE);\n }\n\n const isShippingStepComplete = (data) => {\n if (isVirtualCart(data)) {\n // Virtual products: only email required\n return !!data.email;\n }\n // Physical products: email + shipping address required\n const cartShippingAddress = getCartAddress(data, 'shipping');\n return !!(data.email && cartShippingAddress);\n };\n\n // eslint-disable-next-line max-len\n const isShippingStepActive = () => elements.$shippingStep.classList.contains(CHECKOUT_STEP_ACTIVE);\n\n return {\n continue: continueFromShippingStep,\n display: displayShippingStep,\n displaySummary: displayShippingStepSummary,\n isActive: isShippingStepActive,\n isComplete: isShippingStepComplete,\n };\n};\n",
|
|
355
|
+
"steps.js": "/* eslint-disable import/no-unresolved */\n/* eslint-disable no-unused-vars */\n/* eslint-disable no-shadow */\n/* eslint-disable no-use-before-define */\n/* eslint-disable prefer-const */\n/* eslint-disable max-len */\n\n// Dropin Tools\nimport { events } from '@dropins/tools/event-bus.js';\nimport { initReCaptcha } from '@dropins/tools/recaptcha.js';\n\n// Order Dropin\nimport * as orderApi from '@dropins/storefront-order/api.js';\n\n// Initializers\nimport '../../scripts/initializers/account.js';\nimport '../../scripts/initializers/checkout.js';\nimport '../../scripts/initializers/order.js';\n\n// Scripts\nimport { PaymentMethodCode } from '@dropins/storefront-payment-services/api.js';\n\n// Dropin Lib Functions\nimport {\n createScopedSelector,\n getCartAddress,\n getCartPaymentMethod,\n getCartShippingMethod,\n isVirtualCart,\n scrollToElement,\n setMetaTags,\n validateForm,\n} from '@dropins/storefront-checkout/lib/utils.js';\nimport { rootLink } from '../../scripts/commerce.js';\nimport { getUserTokenCookie } from '../../scripts/initializers/index.js';\n\n// Container functions\nimport {\n CONTAINERS,\n renderCartSummaryList,\n renderCustomerBillingAddresses,\n renderCustomerShippingAddresses,\n renderMergedCartBanner,\n renderOrderSummary,\n renderOutOfStock,\n renderPlaceOrder,\n renderServerError,\n renderTermsAndConditions,\n unmountContainer,\n updatePlaceOrder,\n} from './containers.js';\n\n// Components\nimport {\n COMPONENT_IDS,\n removeComponent,\n renderBillingStepTitle,\n renderCheckoutHeader,\n renderPaymentStepTitle,\n renderShippingMethodStepTitle,\n renderSpinner,\n} from './components.js';\n\n// Constants\nimport {\n BILLING_ADDRESS_DATA_KEY,\n BILLING_FORM_NAME,\n CHECKOUT_STEP_ACTIVE,\n SHIPPING_ADDRESS_DATA_KEY,\n TERMS_AND_CONDITIONS_FORM_NAME,\n} from './constants.js';\n\n// Utils\nimport { buildOrderDetailsUrl } from './utils.js';\n\n// Checkout success block import and CSS preload\nimport { renderCheckoutSuccess } from '../commerce-checkout-success/commerce-checkout-success.js';\n\n// Fragments\nimport { selectors } from './fragments.js';\n\n// Step modules\nimport { createBillingAddressStep } from './steps/billing-address.js';\nimport { createPaymentMethodsStep } from './steps/payment-methods.js';\nimport { createShippingMethodsStep } from './steps/shipping-methods.js';\nimport { createShippingStep } from './steps/shipping.js';\n\nexport const redirectToCartIfEmpty = (cartData) => {\n const isOrderPlaced = events.lastPayload('order/placed') !== undefined;\n\n if (!isOrderPlaced && (cartData === null || cartData?.items?.length === 0)) {\n window.location.href = rootLink('/cart');\n }\n};\n\nconst createStepsManager = (block) => {\n // Global state\n let isInProgress = false;\n\n // Create a scoped selector for the block\n const getElement = createScopedSelector(block);\n\n // Form references\n const formRefs = {\n shippingForm: { current: null },\n billingForm: { current: null },\n creditCardForm: { current: null },\n };\n\n const { checkout, orderConfirmation } = selectors;\n\n // Get block elements using the checkout selectors (excluding step-specific elements)\n const elements = {\n $content: getElement(checkout.content),\n $loader: getElement(checkout.loader),\n $header: getElement(checkout.header),\n $cartSummary: getElement(checkout.cartSummary),\n $orderSummary: getElement(checkout.orderSummary),\n $shippingAddressForm: getElement(checkout.shippingAddressForm),\n $billingStepTitle: getElement(checkout.billingStepTitle),\n $billingForm: getElement(checkout.billingForm),\n $mergedCartBanner: getElement(checkout.mergedCartBanner),\n $outOfStock: getElement(checkout.outOfStock),\n $paymentStepTitle: getElement(checkout.paymentStepTitle),\n $placeOrder: getElement(checkout.placeOrder),\n $serverError: getElement(checkout.serverError),\n $shippingMethodStepTitle: getElement(checkout.shippingMethodStepTitle),\n $termsAndConditions: getElement(checkout.termsAndConditions),\n };\n\n // Helper methods\n const isAuthenticated = () => !!getUserTokenCookie();\n\n const withOverlaySpinner = (callback) => async (...args) => {\n elements.$loader.innerHTML = '';\n await renderSpinner(elements.$loader, COMPONENT_IDS.CHECKOUT_LOADER);\n\n try {\n return await callback(...args);\n } finally {\n removeComponent(COMPONENT_IDS.CHECKOUT_LOADER);\n }\n };\n\n const activateStep = (stepElement, active = true) => {\n stepElement.classList.toggle(CHECKOUT_STEP_ACTIVE, active);\n };\n\n const disablePlaceOrderButton = () => {\n updatePlaceOrder({ disabled: true });\n };\n\n // Initialize step modules with their dependencies\n const sharedDependencies = {\n activateStep,\n disablePlaceOrderButton,\n formRefs,\n getElement,\n isAuthenticated,\n withOverlaySpinner,\n };\n\n // Create step modules\n const steps = {\n shipping: createShippingStep({\n ...sharedDependencies,\n displayShippingMethodStep: (active) => steps.shippingMethods.display(active),\n displayPaymentStep: (active) => steps.paymentMethods.display(active),\n }),\n shippingMethods: createShippingMethodsStep({\n ...sharedDependencies,\n displayPaymentStep: (active) => steps.paymentMethods.display(active),\n }),\n paymentMethods: createPaymentMethodsStep({\n ...sharedDependencies,\n displayBillingStep: (active, data) => steps.billingAddress.display(active, data),\n displayBillingStepSummary: (data, showEditLink) => steps.billingAddress.displaySummary(data, showEditLink),\n }),\n billingAddress: createBillingAddressStep(sharedDependencies),\n };\n\n // Container props and handlers\n const handleValidation = () => {\n let success = true;\n if (success) {\n success = validateForm({ name: BILLING_FORM_NAME, ref: formRefs.billingForm });\n }\n\n if (success) {\n success = validateForm({ name: TERMS_AND_CONDITIONS_FORM_NAME });\n if (!success) scrollToElement(elements.$termsAndConditions);\n }\n\n return success;\n };\n\n const handlePlaceOrder = withOverlaySpinner(async ({ cartId, code }) => {\n try {\n // Payment Services credit card submission\n if (code === PaymentMethodCode.CREDIT_CARD) {\n if (!formRefs.creditCardForm.current) {\n console.error('Credit card form not rendered.');\n return;\n }\n // Validation already done in payment step, just submit\n await formRefs.creditCardForm.current.submit();\n }\n // Place order\n await orderApi.placeOrder(cartId);\n } catch (error) {\n console.error('Error placing order:', error);\n throw error;\n }\n });\n\n // Track auth state to detect changes\n let wasAuthenticated = isAuthenticated();\n\n const handleAuthenticated = async (authenticated) => {\n if (!authenticated) return;\n window.location.reload();\n };\n\n // Unified checkout flow handler for both virtual and physical products\n const handleCheckoutFlow = async (data) => {\n // Step 1: Shipping/Contact Information\n if (!steps.shipping.isComplete(data)) {\n await steps.shipping.display(true, data);\n return;\n }\n\n const cartShippingAddress = getCartAddress(data, 'shipping');\n await steps.shipping.display(false, data);\n await steps.shipping.displaySummary(data.email, cartShippingAddress);\n\n // Step 2: Shipping Methods (physical products only)\n if (!isVirtualCart(data)) {\n if (!steps.shippingMethods.isComplete(data)) {\n await steps.shippingMethods.display(true);\n return;\n }\n\n const shippingMethod = getCartShippingMethod(data);\n await steps.shippingMethods.display(false);\n await steps.shippingMethods.displaySummary(shippingMethod);\n }\n\n // Step 3: Payment\n if (!steps.paymentMethods.isComplete(data)) {\n await steps.paymentMethods.display(true);\n return;\n }\n\n const paymentMethod = getCartPaymentMethod(data);\n await steps.paymentMethods.display(false);\n await steps.paymentMethods.displaySummary(paymentMethod);\n\n // Step 4: Billing\n if (!steps.billingAddress.isComplete(data)) {\n await steps.paymentMethods.displaySummary(paymentMethod);\n await steps.billingAddress.display(true, data);\n return;\n }\n\n await steps.paymentMethods.displaySummary(paymentMethod);\n\n const cartBillingAddress = getCartAddress(data, 'billing');\n const sameAsBilling = data?.shippingAddresses?.[0]?.sameAsBilling;\n\n if (cartBillingAddress) {\n await steps.billingAddress.displaySummary(cartBillingAddress, !sameAsBilling);\n }\n\n updatePlaceOrder({ disabled: false });\n };\n\n const handleCheckoutUpdated = async (data) => {\n await initReCaptcha(0);\n\n // Manage shipping method title based on cart type\n if (isVirtualCart(data)) {\n removeComponent(COMPONENT_IDS.SHIPPING_METHOD_STEP_TITLE);\n } else {\n await renderShippingMethodStepTitle(elements.$shippingMethodStepTitle);\n }\n\n // Handle authentication status changes\n const currentAuthState = isAuthenticated();\n if (wasAuthenticated !== currentAuthState && currentAuthState) {\n formRefs.shippingForm.current = null;\n formRefs.billingForm.current = null;\n\n // User logged in - switch to customer address forms\n unmountContainer(CONTAINERS.SHIPPING_ADDRESS_FORM);\n await renderCustomerShippingAddresses(elements.$shippingAddressForm, formRefs.shippingForm, data);\n\n unmountContainer(CONTAINERS.BILLING_ADDRESS_FORM);\n await renderCustomerBillingAddresses(elements.$billingForm, formRefs.billingForm, data);\n }\n wasAuthenticated = currentAuthState;\n\n if (isInProgress) {\n // During checkout progress, only update existing containers if needed\n // Don't show new steps - that's handled by handleCheckoutFlow\n return;\n }\n\n isInProgress = true;\n\n // Execute unified checkout flow\n await handleCheckoutFlow(data);\n };\n\n const handleCheckoutStepCompleted = () => {\n const checkoutSteps = Object.values(steps);\n\n if (checkoutSteps.some((step) => step.isActive())) return;\n\n const data = events.lastPayload('checkout/updated');\n\n const areAllStepsCompleted = checkoutSteps\n .every((step) => step.isComplete(data));\n\n if (areAllStepsCompleted) {\n updatePlaceOrder({ disabled: false });\n }\n };\n\n const handleOrderPlaced = async (orderData) => {\n setMetaTags('Order Confirmation');\n document.title = 'Order Confirmation';\n\n // Clear address form data\n sessionStorage.removeItem(SHIPPING_ADDRESS_DATA_KEY);\n sessionStorage.removeItem(BILLING_ADDRESS_DATA_KEY);\n\n const url = buildOrderDetailsUrl(orderData);\n\n window.history.pushState({}, '', url);\n\n await renderCheckoutSuccess(block, { orderData });\n };\n\n async function init() {\n events.on('authenticated', handleAuthenticated);\n events.on('checkout/initialized', handleCheckoutUpdated, { eager: true });\n events.on('checkout/updated', handleCheckoutUpdated);\n events.on('order/placed', handleOrderPlaced);\n events.on('checkout/step/completed', handleCheckoutStepCompleted);\n events.on('cart/initialized', redirectToCartIfEmpty, { eager: true });\n events.on('cart/data', redirectToCartIfEmpty);\n\n // Render the initial view\n await Promise.all([\n renderMergedCartBanner(elements.$mergedCartBanner),\n renderOutOfStock(elements.$outOfStock),\n renderServerError(elements.$serverError, block),\n renderCheckoutHeader(elements.$header, 'Checkout'),\n renderShippingMethodStepTitle(elements.$shippingMethodStepTitle),\n renderPaymentStepTitle(elements.$paymentStepTitle),\n renderBillingStepTitle(elements.$billingStepTitle),\n renderOrderSummary(elements.$orderSummary),\n renderCartSummaryList(elements.$cartSummary),\n renderTermsAndConditions(elements.$termsAndConditions),\n renderPlaceOrder(elements.$placeOrder, { handleValidation, handlePlaceOrder }),\n ]);\n }\n\n return { init };\n};\n\nexport default createStepsManager;\n",
|
|
356
|
+
"utils.js": "/* eslint-disable import/no-unresolved */\nimport { tryRenderAemAssetsImage } from '@dropins/tools/lib/aem/assets.js';\nimport { ORDER_DETAILS_PATH, rootLink } from '../../scripts/commerce.js';\nimport { getUserTokenCookie } from '../../scripts/initializers/index.js';\nimport createModal from '../modal/modal.js';\n\nlet modal;\n\n/**\n * Shows a modal with the specified content\n * @param {HTMLElement} content - DOM element to display in the modal\n */\nexport const showModal = async (content) => {\n modal = await createModal([content]);\n modal.showModal();\n};\n\n/**\n * Removes the currently displayed modal and cleans up references\n */\nexport const removeModal = () => {\n if (!modal) return;\n modal.removeModal();\n modal = null;\n};\n\n/**\n * Renders AEM asset images for gift option swatches\n * @param {Object} ctx - The context object containing imageSwatchContext and defaultImageProps\n */\nexport function swatchImageSlot(ctx) {\n const { imageSwatchContext, defaultImageProps } = ctx;\n tryRenderAemAssetsImage(ctx, {\n alias: imageSwatchContext.label,\n imageProps: defaultImageProps,\n wrapper: document.createElement('span'),\n params: {\n width: defaultImageProps.width,\n height: defaultImageProps.height,\n },\n });\n}\n\n/**\n * Builds the order details URL based on authentication status\n * @param {Object} orderData - Order data containing number and token\n * @param {string} orderDetailsPath - Path to the order details page\n * @returns {string} The constructed order details URL\n */\nexport function buildOrderDetailsUrl(orderData, orderDetailsPath = ORDER_DETAILS_PATH) {\n const token = getUserTokenCookie();\n const orderRef = token ? orderData.number : orderData.token;\n const orderNumber = orderData.number;\n const encodedOrderRef = encodeURIComponent(orderRef);\n const encodedOrderNumber = encodeURIComponent(orderNumber);\n\n return token\n ? rootLink(`${orderDetailsPath}?orderRef=${encodedOrderRef}`)\n : rootLink(`${orderDetailsPath}?orderRef=${encodedOrderRef}&orderNumber=${encodedOrderNumber}`);\n}\n"
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
"name": "commerce-checkout-success",
|
|
361
|
+
"description": "Example commerce-checkout-success block for storefront-checkout",
|
|
362
|
+
"files": {
|
|
363
|
+
"commerce-checkout-success.js": "/* eslint-disable import/no-unresolved */\n\n// Tools and initializers\nimport { Button, provider as UI } from '@dropins/tools/components.js';\nimport { initializers } from '@dropins/tools/initializer.js';\nimport { tryRenderAemAssetsImage } from '@dropins/tools/lib/aem/assets.js';\nimport { events } from '@dropins/tools/event-bus.js';\n\n// Order Dropin API\nimport * as orderApi from '@dropins/storefront-order/api.js';\nimport { render as OrderProvider } from '@dropins/storefront-order/render.js';\nimport OrderHeader from '@dropins/storefront-order/containers/OrderHeader.js';\nimport OrderStatus from '@dropins/storefront-order/containers/OrderStatus.js';\nimport ShippingStatus from '@dropins/storefront-order/containers/ShippingStatus.js';\nimport CustomerDetails from '@dropins/storefront-order/containers/CustomerDetails.js';\nimport OrderCostSummary from '@dropins/storefront-order/containers/OrderCostSummary.js';\nimport OrderProductList from '@dropins/storefront-order/containers/OrderProductList.js';\n\n// Checkout API/utils used for header and DOM\nimport * as checkoutApi from '@dropins/storefront-checkout/api.js';\nimport { createFragment, createScopedSelector } from '@dropins/storefront-checkout/lib/utils.js';\n\n// Cart (for gift options within order confirmation)\nimport { render as CartProvider } from '@dropins/storefront-cart/render.js';\nimport GiftOptions from '@dropins/storefront-cart/containers/GiftOptions.js';\n\n// Auth (for sign-up modal in header)\nimport { render as AuthProvider } from '@dropins/storefront-auth/render.js';\nimport SignUp from '@dropins/storefront-auth/containers/SignUp.js';\n\n// Commerce helpers\nimport {\n fetchPlaceholders,\n rootLink,\n SUPPORT_PATH,\n authPrivacyPolicyConsentSlot,\n} from '../../scripts/commerce.js';\n\n// Ensure order drop-in initializer side effects are applied\nimport '../../scripts/initializers/order.js';\n\n// Local modal helper\nimport createModal from '../modal/modal.js';\nimport { loadCSS } from '../../scripts/aem.js';\n\n// ----------------------------------------------------------------------------\n// Local selectors and fragments (order confirmation only)\n// ----------------------------------------------------------------------------\n\nconst selectors = Object.freeze({\n orderConfirmation: {\n header: '.order-confirmation__header',\n orderStatus: '.order-confirmation__order-status',\n shippingStatus: '.order-confirmation__shipping-status',\n customerDetails: '.order-confirmation__customer-details',\n orderCostSummary: '.order-confirmation__order-cost-summary',\n giftOptions: '.order-confirmation__gift-options',\n orderProductList: '.order-confirmation__order-product-list',\n footer: '.order-confirmation__footer',\n continueButton: '.order-confirmation-footer__continue-button',\n },\n});\n\nfunction createOrderConfirmationFragment() {\n return createFragment(`\n <div class=\"order-confirmation\">\n <div class=\"order-confirmation__main\">\n <div class=\"order-confirmation__header order-confirmation__block\"></div>\n <div class=\"order-confirmation__order-status order-confirmation__block\"></div>\n <div class=\"order-confirmation__shipping-status order-confirmation__block\"></div>\n <div class=\"order-confirmation__customer-details order-confirmation__block\"></div>\n </div>\n <div class=\"order-confirmation__aside\">\n <div class=\"order-confirmation__order-cost-summary order-confirmation__block\"></div>\n <div class=\"order-confirmation__gift-options order-confirmation__block\"></div>\n <div class=\"order-confirmation__order-product-list order-confirmation__block\"></div>\n <div class=\"order-confirmation__footer order-confirmation__block\"></div>\n </div>\n </div>\n `);\n}\n\nfunction createOrderConfirmationFooter(supportPath) {\n return `\n <div class=\"order-confirmation-footer__continue-button\"></div>\n <div class=\"order-confirmation-footer__contact-support\">\n <p>\n Need help?\n <a\n href=\"${supportPath}\"\n rel=\"noreferrer\"\n class=\"order-confirmation-footer__contact-support-link\"\n data-testid=\"order-confirmation-footer__contact-support-link\"\n >\n Contact us\n </a>\n </p>\n </div>\n `;\n}\n\n// ----------------------------------------------------------------------------\n// Local utility slots (swatch and modal)\n// ----------------------------------------------------------------------------\n\nfunction swatchImageSlot(ctx) {\n const { imageSwatchContext, defaultImageProps } = ctx;\n tryRenderAemAssetsImage(ctx, {\n alias: imageSwatchContext.label,\n imageProps: defaultImageProps,\n wrapper: document.createElement('span'),\n params: {\n width: defaultImageProps.width,\n height: defaultImageProps.height,\n },\n });\n}\n\nlet signUpModal;\n\nconst handleAuthenticated = (authenticated) => {\n if (authenticated) {\n window.location.reload();\n }\n};\n\n// ----------------------------------------------------------------------------\n// Local renderers (order confirmation only)\n// ----------------------------------------------------------------------------\n\nasync function renderOrderHeader(container, options = {}) {\n const handleSignUpClick = async ({ inputsDefaultValueSet, addressesData }) => {\n const signUpForm = document.createElement('div');\n AuthProvider.render(SignUp, {\n inputsDefaultValueSet,\n addressesData,\n routeSignIn: () => rootLink('/customer/login'),\n routeRedirectOnEmailConfirmationClose: () => rootLink('/customer/account'),\n slots: { ...authPrivacyPolicyConsentSlot },\n })(signUpForm);\n signUpModal = await createModal([signUpForm]);\n signUpModal.showModal();\n };\n\n return OrderProvider.render(OrderHeader, {\n handleEmailAvailability: checkoutApi.isEmailAvailable,\n handleSignUpClick,\n ...options,\n })(container);\n}\n\nasync function renderOrderStatus(container) {\n return OrderProvider.render(OrderStatus, { slots: { OrderActions: () => null } })(container);\n}\n\nasync function renderShippingStatus(container) {\n return OrderProvider.render(ShippingStatus)(container);\n}\n\nasync function renderCustomerDetails(container) {\n return OrderProvider.render(CustomerDetails)(container);\n}\n\nasync function renderOrderCostSummary(container) {\n return OrderProvider.render(OrderCostSummary)(container);\n}\n\nasync function renderOrderProductList(container) {\n return OrderProvider.render(OrderProductList, {\n slots: {\n Footer: (ctx) => {\n const giftOptions = document.createElement('div');\n CartProvider.render(GiftOptions, {\n item: ctx.item,\n view: 'product',\n dataSource: 'order',\n isEditable: false,\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(giftOptions);\n ctx.appendChild(giftOptions);\n },\n CartSummaryItemImage: (ctx) => {\n const { data, defaultImageProps } = ctx;\n tryRenderAemAssetsImage(ctx, {\n alias: data.product.sku,\n imageProps: defaultImageProps,\n params: {\n width: defaultImageProps.width,\n height: defaultImageProps.height,\n },\n });\n },\n },\n })(container);\n}\n\nasync function renderOrderGiftOptions(container) {\n return CartProvider.render(GiftOptions, {\n view: 'order',\n dataSource: 'order',\n isEditable: false,\n readOnlyFormOrderView: 'secondary',\n slots: {\n SwatchImage: swatchImageSlot,\n },\n })(container);\n}\n\nasync function renderOrderConfirmationFooterButton(container) {\n return UI.render(Button, {\n children: 'Continue shopping',\n 'data-testid': 'order-confirmation-footer__continue-button',\n className: 'order-confirmation-footer__continue-button',\n size: 'medium',\n variant: 'primary',\n type: 'submit',\n href: rootLink('/'),\n })(container);\n}\n\nasync function renderCheckoutSuccessContent(container, { orderData } = {}) {\n // Register event handler for authenticated event\n events.on('authenticated', handleAuthenticated);\n\n // Scroll to top on success view\n window.scrollTo(0, 0);\n\n // Create order confirmation layout using local fragments\n const orderConfirmationFragment = createOrderConfirmationFragment();\n\n // Scoped selector for the fragment\n const getOrderElement = createScopedSelector(orderConfirmationFragment);\n\n // Query all required elements using local selectors\n const $orderConfirmationHeader = getOrderElement(selectors.orderConfirmation.header);\n const $orderStatus = getOrderElement(selectors.orderConfirmation.orderStatus);\n const $shippingStatus = getOrderElement(selectors.orderConfirmation.shippingStatus);\n const $customerDetails = getOrderElement(selectors.orderConfirmation.customerDetails);\n const $orderCostSummary = getOrderElement(selectors.orderConfirmation.orderCostSummary);\n const $orderGiftOptions = getOrderElement(selectors.orderConfirmation.giftOptions);\n const $orderProductList = getOrderElement(selectors.orderConfirmation.orderProductList);\n const $orderConfirmationFooter = getOrderElement(selectors.orderConfirmation.footer);\n\n container.replaceChildren(orderConfirmationFragment);\n\n // Mount order drop-in with localized placeholders (and optional order data)\n const labels = await fetchPlaceholders();\n const langDefinitions = { default: { ...labels } };\n const initOptions = orderData ? { langDefinitions, orderData } : { langDefinitions };\n await initializers.mountImmediately(orderApi.initialize, initOptions);\n\n // Render all order confirmation containers using local renderers\n await Promise.all([\n renderOrderHeader($orderConfirmationHeader, { orderData }),\n renderOrderStatus($orderStatus),\n renderShippingStatus($shippingStatus),\n renderCustomerDetails($customerDetails),\n renderOrderCostSummary($orderCostSummary),\n renderOrderProductList($orderProductList),\n renderOrderGiftOptions($orderGiftOptions),\n ]);\n\n // Footer content and continue button\n $orderConfirmationFooter.innerHTML = createOrderConfirmationFooter(rootLink(SUPPORT_PATH));\n const $continueBtn = $orderConfirmationFooter.querySelector(\n selectors.orderConfirmation.continueButton,\n );\n await renderOrderConfirmationFooterButton($continueBtn);\n}\n\nexport function preloadCheckoutSuccess() {\n return loadCSS(`${window.hlx.codeBasePath}/blocks/commerce-checkout-success/commerce-checkout-success.css`);\n}\n\nexport async function renderCheckoutSuccess(container, { orderData } = {}) {\n return renderCheckoutSuccessContent(container, { orderData });\n}\n\nexport default async function decorate(block) {\n await renderCheckoutSuccessContent(block);\n}\n",
|
|
364
|
+
"commerce-checkout-success.css": "/* stylelint-disable selector-class-pattern */\n\n.order-confirmation {\n display: grid;\n align-items: start;\n grid-template-columns: repeat(var(--grid-4-columns), 1fr);\n grid-template-areas: 'main aside';\n column-gap: var(--grid-4-gutters);\n margin-bottom: var(--spacing-xbig);\n padding-top: var(--spacing-xxlarge);\n}\n\n.order-confirmation__main {\n display: grid;\n row-gap: var(--spacing-xbig);\n grid-column: 1 / span 7;\n}\n\n.order-confirmation__aside {\n display: grid;\n row-gap: var(--spacing-xbig);\n grid-column: 9 / span 4;\n}\n\n.order-confirmation__footer {\n display: grid;\n gap: var(--spacing-small);\n text-align: center;\n}\n\n.order-confirmation__footer p {\n margin: 0;\n}\n\n.order-confirmation__footer .order-confirmation-footer__continue-button {\n margin: 0 auto;\n text-align: center;\n display: inline-block;\n}\n\n.order-confirmation-footer__contact-support {\n font: var(--type-body-2-default-font);\n letter-spacing: var(--type-body-2-default-letter-spacing);\n color: var(--color-neutral-700);\n}\n\n.order-confirmation-footer__contact-support a {\n font: var(--type-body-2-strong-font);\n letter-spacing: var(--type-body-2-strong-letter-spacing);\n color: var(--color-brand-500);\n cursor: pointer;\n}\n\n/* Hide empty blocks */\n.order-confirmation__block:empty {\n display: none;\n}\n\n@media only screen and (min-width: 320px) and (max-width: 768px) {\n .order-confirmation {\n grid-template-columns: repeat(var(--grid-1-columns), 1fr);\n padding-top: 0;\n }\n\n .order-confirmation__main,\n .order-confirmation__aside {\n row-gap: var(--spacing-medium);\n }\n\n .order-confirmation > div {\n grid-column: 1 / span 4;\n }\n\n .order-confirmation__block .dropin-card {\n border: 0;\n }\n}\n",
|
|
365
|
+
"README.md": "# Commerce Checkout Success Block\n\n## Overview\n\nThe Commerce Checkout Success block renders the post-purchase order confirmation experience. It initializes the Storefront Order drop-in, displays order details (status, shipping, customer information, costs, products), includes gift options in a read-only view, and provides a continue shopping action with a contact support link.\n\n## Integration\n\n### Block Configuration\n\nNo block configuration is read from the DOM.\n\n### URL Parameters\n\nNo URL parameters are read by this block.\n\n### Local Storage\n\nNo localStorage keys are used by this block.\n\n### Dependencies\n\n- `@dropins/storefront-order` (Order containers and initializer)\n- `@dropins/storefront-cart` (Gift options container for order items)\n- `@dropins/storefront-auth` (Sign Up modal)\n- `@dropins/tools` (UI Button, provider, initializers)\n- Local helpers: `scripts/commerce.js`, `scripts/aem.js`, `blocks/modal/modal.js`\n\n## Public API\n\n- `default export decorate(block)` — Renders the success view into the provided block element.\n- `export async function renderCheckoutSuccess(container, { orderData } = {})` — Renders the success view into `container`. Pass `orderData` to provide pre-fetched order data; otherwise the Order drop-in will fetch it when possible (not possible when placing an order as a guest using an email address that’s already associated with an existing account).\n- `export function preloadCheckoutSuccess()` — Preloads the checkout success CSS. Call this before `renderCheckoutSuccess`.\n\nExample (programmatic):\n\n```js\nimport { preloadCheckoutSuccess, renderCheckoutSuccess } from './blocks/commerce-checkout-success/commerce-checkout-success.js';\n\n// Load CSS\npreloadCheckoutSuccess();\n\n// Pass pre-fetched order data\nawait renderCheckoutSuccess(container, { orderData });\n```\n\n## Behavior\n\n- Scrolls to top on load for proper confirmation visibility.\n- Loads `commerce-checkout-success.css` styles.\n- Fetches localized placeholders and mounts the Order drop-in initializer with `langDefinitions` and optional `orderData`.\n- Renders order confirmation sections (header, status, shipping status, customer details, cost summary, product list, gift options).\n- Footer includes a \"Continue shopping\" button and a \"Contact us\" support link (from `SUPPORT_PATH`).\n- Product list integrates read-only Gift Options per item; product and swatch images attempt to use AEM Assets via SKU/label aliasing.\n\n## DOM Structure\n\nThis block builds a scoped fragment that follows this structure:\n\n```html\n<div class=\"order-confirmation\">\n <div class=\"order-confirmation__main\">\n <div class=\"order-confirmation__header\"></div>\n <div class=\"order-confirmation__order-status\"></div>\n <div class=\"order-confirmation__shipping-status\"></div>\n <div class=\"order-confirmation__customer-details\"></div>\n </div>\n <div class=\"order-confirmation__aside\">\n <div class=\"order-confirmation__order-cost-summary\"></div>\n <div class=\"order-confirmation__gift-options\"></div>\n <div class=\"order-confirmation__order-product-list\"></div>\n <div class=\"order-confirmation__footer\"></div>\n </div>\n</div>\n```\n\n## Rendered Containers\n\n- `OrderHeader` — Displays order header with optional Sign Up action for guests.\n- `OrderStatus` — Shows overall order status.\n- `ShippingStatus` — Displays shipping status for physical items.\n- `CustomerDetails` — Shows purchaser details.\n- `OrderCostSummary` — Displays totals, taxes, discounts.\n- `OrderProductList` — Lists items in the order; integrates Gift Options (read-only) in item footer.\n- `GiftOptions` — Separate read-only gift options summary.\n- Footer — \"Continue shopping\" button and support link.\n\n## Slots and Customization\n\n- Product list uses a `Footer` slot to render read-only `GiftOptions` per item.\n- `CartSummaryItemImage` slot attempts to render images from AEM Assets using SKU as alias.\n- Swatch images attempt AEM Assets by label alias with default sizing.\n- Header Sign Up action opens a modal that renders the Auth `SignUp` container and uses privacy policy consent slots from `scripts/commerce.js`.\n\n## Styling\n\n- Styles are defined in `commerce-checkout-success.css`.\n- Class naming follows the `order-confirmation__*` pattern for main, aside, and sectional blocks.\n- The footer button uses the shared UI `Button` component and standard size/variant props.\n\n## Error Handling\n\n- Relies on Storefront Order and Cart containers for data fetching states and error UI.\n- Support link is always rendered to route customers to assistance when needed.\n\n## Usage\n\nAdd the block to your confirmation page template:\n\n```html\n<div class=\"block commerce-checkout-success\"></div>\n```\n\nWhen used via the decorator system, the block will automatically render the confirmation experience into the block container. For programmatic usage, use the `renderCheckoutSuccess` export and pass `orderData`.\n\n## Authentication Flow\n\nThe block listens for authentication state changes via `events.on('authenticated', callback)`. When a guest user successfully signs up through the modal, the page automatically reloads to reflect the authenticated state across all components.\n\n## File Structure\n\n- `commerce-checkout-success.js` — Entry point and block decorator; builds fragment, mounts Order drop-in, renders containers, and footer actions.\n- `commerce-checkout-success.css` — Styles for the confirmation layout.\n\n## Notes\n\n- This block is read-only from a checkout perspective; cart and payment edits occur in the checkout flow. The success page focuses on confirmation, details, and post-purchase actions.\n"
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
"name": "modal",
|
|
370
|
+
"description": "Example modal block for storefront-checkout",
|
|
371
|
+
"files": {
|
|
372
|
+
"modal.js": "import { loadCSS, buildBlock } from '../../scripts/aem.js';\n\nexport default async function createModal(contentNodes) {\n await loadCSS('./blocks/modal/modal.css');\n const dialog = document.createElement('dialog');\n dialog.setAttribute('tabindex', 1);\n dialog.setAttribute('role', 'dialog');\n\n const dialogContent = document.createElement('div');\n dialogContent.classList.add('modal-content');\n dialogContent.append(...contentNodes);\n dialog.append(dialogContent);\n\n const closeButton = document.createElement('button');\n closeButton.classList.add('close-button');\n closeButton.setAttribute('aria-label', 'Close');\n closeButton.setAttribute('data-dismiss', 'modal');\n closeButton.type = 'button';\n closeButton.innerHTML = '<span class=\"icon icon-close\"></span>';\n closeButton.addEventListener('click', () => dialog.close());\n dialog.append(closeButton);\n\n // close dialog on clicks outside the dialog. https://stackoverflow.com/a/70593278/79461\n dialog.addEventListener('click', (event) => {\n if (event.pointerType !== 'mouse') return;\n\n const dialogDimensions = dialog.getBoundingClientRect();\n if (\n event.clientX < dialogDimensions.left\n || event.clientX > dialogDimensions.right\n || event.clientY < dialogDimensions.top\n || event.clientY > dialogDimensions.bottom\n ) {\n dialog.close();\n }\n });\n\n const block = buildBlock('modal', '');\n document.querySelector('main').append(block);\n\n dialog.addEventListener('close', () => {\n document.body.classList.remove('modal-open');\n block.remove();\n });\n\n block.append(dialog);\n\n return {\n block,\n removeModal: () => dialog.close(),\n showModal: () => {\n dialog.showModal();\n // Google Chrome restores the scroll position when the dialog is reopened,\n // so we need to reset it.\n setTimeout(() => {\n dialogContent.scrollTop = 0;\n }, 0);\n\n // Focus the first input when content is fully loaded using MutationObserver.\n const observer = new MutationObserver(() => {\n const firstInput = dialogContent.querySelector('input');\n if (firstInput) {\n firstInput.focus();\n observer.disconnect();\n }\n });\n\n observer.observe(dialogContent, { childList: true, subtree: true });\n\n document.body.classList.add('modal-open');\n },\n };\n}\n",
|
|
373
|
+
"modal.css": "body.modal-open {\n overflow: hidden;\n}\n\n.modal dialog {\n --dialog-border-radius: var(--shape-border-radius-2);\n\n overscroll-behavior: none;\n border: var(--shape-border-width-1) solid var(--color-neutral-400);\n border-radius: var(--dialog-border-radius);\n width: 100vw;\n}\n\n.modal dialog .modal-content {\n overflow-y: auto;\n overscroll-behavior: none;\n max-height: calc(100dvh - 60px);\n}\n\n.modal dialog::backdrop {\n background-color: rgb(0 0 0 / 50%);\n}\n\n.modal .close-button {\n position: absolute;\n top: 0;\n right: 0;\n width: 48px;\n height: 48px;\n margin: 0;\n border: none;\n border-radius: 0;\n padding: 0;\n background-color: transparent;\n color: var(--color-brand-600);\n line-height: 0;\n}\n\n.modal .close-button .icon.icon-close {\n content: '';\n width: 24px;\n height: 24px;\n}\n\n.modal .close-button .icon.icon-close::before,\n.modal .close-button .icon.icon-close::after {\n content: '';\n box-sizing: border-box;\n display: block;\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%) rotate(45deg);\n width: 24px;\n height: 2px;\n border-radius: var(--shape-border-radius-1);\n background-color: currentcolor;\n}\n\n.modal .close-button .icon.icon-close::after {\n transform: translate(-50%, -50%) rotate(-45deg)\n}\n\n.modal .close-button span {\n cursor: pointer;\n}\n\n.modal dialog .section {\n padding: 0;\n}\n\n@media (width >= 600px) {\n .modal dialog {\n width: 80vw;\n max-width: 700px;\n }\n\n .modal dialog .modal-content {\n max-height: calc(100vh - 90px);\n }\n}\n"
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
]
|
|
377
|
+
}
|