{"version":3,"file":"cart-drawer-section.min.js","mappings":"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","sources":["webpack://lange/./lib/cart-event-queue.ts","webpack://lange/./lib/dom-ready.ts","webpack://lange/./sections/cart-drawer/cart-actions/index.ts","webpack://lange/./lib/perf.ts","webpack://lange/./sections/cart-drawer/cart-drawer/index.ts","webpack://lange/../node_modules/@integrabeauty/cookie/src/index.ts","webpack://lange/./sections/cart-drawer/cart-gwp/index.ts","webpack://lange/./sections/cart-drawer/cart/index.ts","webpack://lange/../node_modules/@integrabeauty/shopify-ajax-api/src/cart.ts","webpack://lange/./lib/cart/digest.ts","webpack://lange/./lib/cart/equality.ts","webpack://lange/./lib/decorate-error.ts","webpack://lange/./lib/cart/error.ts","webpack://lange/../node_modules/@integrabeauty/assert/src/index.ts","webpack://lange/../node_modules/uuid/dist/rng.js","webpack://lange/../node_modules/uuid/dist/stringify.js","webpack://lange/../node_modules/uuid/dist/v4.js","webpack://lange/./lib/object.ts","webpack://lange/./lib/cart/discount/discount.ts","webpack://lange/./lib/untrusted-value.ts","webpack://lange/./lib/cart/state.ts","webpack://lange/./lib/cart/transaction.ts","webpack://lange/./lib/cart/change.ts","webpack://lange/./lib/cart/remove.ts","webpack://lange/./lib/cart/update.ts","webpack://lange/./sections/cart-drawer/cart-line-item/index.ts","webpack://lange/./sections/cart-drawer/cart-messages/index.ts","webpack://lange/./sections/cart-drawer/cart-totals/index.ts","webpack://lange/./sections/cart-drawer/cart-upsell-item/index.ts","webpack://lange/./sections/cart-drawer/cart-upsell-items/index.ts","webpack://lange/../node_modules/@shopify/theme-currency/currency.js","webpack://lange/./sections/cart-drawer/free-shipping-banner/index.ts","webpack://lange/./sections/cart-drawer/gift-qualified-message/index.ts","webpack://lange/./lib/cart/discount/helper.ts","webpack://lange/./lib/cart/add.ts","webpack://lange/./sections/cart-drawer/offer-view/index.ts","webpack://lange/./sections/cart-drawer/gift-with-purchase-product/index.ts","webpack://lange/./sections/cart-drawer/gift-with-purchase-progress/index.ts","webpack://lange/./sections/cart-drawer/promo-goal-progress-v2/index.ts","webpack://lange/./sections/cart-drawer/discount-input/index.ts","webpack://lange/./lib/cart/discount/apply.ts","webpack://lange/./lib/cart/discount/remove.ts","webpack://lange/./sections/cart-drawer/promo-goal/index.ts","webpack://lange/./sections/cart-drawer/promo-goal-progress/qualifying-subtotal.ts","webpack://lange/./sections/cart-drawer/promo-goal-progress/index.ts","webpack://lange/./lib/growthbook-feature.ts","webpack://lange/./sections/cart-drawer/shipping-experiment.ts","webpack://lange/./sections/cart-drawer/progress-bar-custom/index.ts","webpack://lange/./sections/cart-drawer/progress-view/index.ts","webpack://lange/./sections/cart-drawer/upsell-view/index.ts","webpack://lange/./sections/cart-drawer/checkout-started.ts","webpack://lange/./sections/cart-drawer/drawer-control.ts","webpack://lange/./sections/cart-drawer/gift-with-purchase-experiment.ts"],"sourcesContent":["// Accessors for the ambient cart event queue seeded and filled by the always-on cart lifecycle\n// snippet. The snippet registers a parse-time listener that buffers every cart event into the\n// queue, so a section whose bundle loads later can replay what it missed. Reaching the queue\n// through this lib, rather than the global directly, keeps the coupling to the ambient layer\n// explicit. The buffered events come off the queue untyped, so the guards here narrow each one\n// during replay.\n\ntype CartUpdatedEvent = GlobalEventHandlersEventMap['cart-updated'];\ntype CartUpdateErredEvent = GlobalEventHandlersEventMap['cart-update-erred'];\n\n/**\n * Returns the ambient queue of buffered cart events. A consumer replays this as it initializes,\n * then listens live for further events.\n */\nexport function getEventQueue() {\n  return cart_lifecycle.queue;\n}\n\n/**\n * Whether the value is a cart updated event. A consumer replaying the queue narrows each buffered\n * event with this before handling it.\n */\nexport function isCartUpdatedEvent(value: any): value is CartUpdatedEvent {\n  return value instanceof Event && value.type === 'cart-updated';\n}\n\n/**\n * Whether the value is a cart update erred event.\n */\nexport function isCartUpdateErredEvent(value: any): value is CartUpdateErredEvent {\n  return value instanceof Event && value.type === 'cart-update-erred';\n}\n","/**\n * Releases the handle returned from the ready() function.\n *\n * This is idempotent. Additional calls are pointless but harmless.\n */\nexport type ReleaseHandle = () => void;\n\n/**\n * A subset of events that pertain to document ready state.\n */\ntype LoadingEventMap = Pick<DocumentEventMap, 'DOMContentLoaded' | 'load'>;\n\n/**\n * Calls the provided callback when the document is ready. This works both when the document is\n * already ready and when it is not, so callers do not have to care. This exists because it ensures\n * that each caller consistently expresses its dependency on document readiness, and to save a bit\n * of boilerplate.\n *\n * A document is considered ready when it has been parsed. This corresponds to the ready state of\n * 'interactive'. When the document has been fully loaded, the ready state is 'complete'. The\n * callback runs either way, so callers do not have to check which one they are in.\n *\n * Ready means the html is parsed and the DOM is there to query. It does not mean the page's other\n * scripts have run. Deferred scripts and module scripts run after the ready state turns\n * 'interactive' and before DOMContentLoaded fires, so a callback here can run before them. When a\n * caller needs some other script to have run already, this is the wrong tool, and it should wait on\n * whatever that script produces instead.\n *\n * That remedy has a catch of its own when the calling bundle is loaded with async. An async bundle\n * can execute after DOMContentLoaded has already fired, so a plain listener for that event never\n * runs and the work silently never happens. Nothing here can detect that, because by the time this\n * file executes the event is long gone. Catching it needs something registered at parse time,\n * before any bundle loads.\n *\n * There is a second trap right next to that one, and it applies only when waiting for\n * DOMContentLoaded. The ready state stays 'interactive' after DOMContentLoaded has fired, all the\n * way until 'complete', so the ready state cannot tell you whether DOMContentLoaded is still coming\n * or has already gone. Waiting for load is not exposed to this, because the ready state turns\n * 'complete' and load fires in the same task, so seeing 'complete' means load has fired too.\n *\n * When the document is already ready, the callback is invoked via setTimeout. This way, if the\n * callback throws, the error surfaces to the global error handler in a way that is similar to what\n * happens when an event listener throws.\n *\n * We might one day want to move this to an NPM library.\n *\n * Beware of a quirk here. setTimeout yields, and the busier the frontend, the longer it takes to\n * fire. Page load is exactly when the frontend is busiest. The timeout is a floor and not a\n * guarantee, so there is no real bound on how long that yield lasts. What this means is that using\n * this library can introduce more delay than you might expect. Compare code that runs on async load\n * and checks ready state and invokes immediately to code that runs after setTimeout. The former\n * runs in the same tick, the latter runs in \"some later tick\". That later tick can be surprisingly\n * much later. That can cause initial paint issues.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event\n * @see https://html.spec.whatwg.org/multipage/parsing.html#the-end\n */\nexport function ready(callback: (event?: Event) => void,\n  type: keyof LoadingEventMap = 'DOMContentLoaded') {\n  if (document.readyState === 'complete' || (type === 'DOMContentLoaded' &&\n    document.readyState === 'interactive')) {\n    const timer = setTimeout(callback, 0);\n    const handle: ReleaseHandle = () => clearTimeout(timer);\n    return handle;\n  } else {\n    addEventListener(type, callback);\n    const handle: ReleaseHandle = () => removeEventListener(type, callback);\n    return handle;\n  }\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport { ready, type ReleaseHandle } from '../../../lib/dom-ready.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders a \"Continue to Checkout\" button and a \"Continue Shopping\" button that closes the side\n * cart.\n */\nclass CartActions extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * Boolean represented as string to indicate if the close button should be hidden\n     */\n    hideCloseButton: string;\n\n    /**\n     * Boolean represented as string to indicate if query parameter to skip shop pay when navigating\n     * to checkout should be set to true.\n     */\n    skipShopPay: 'false' | 'true';\n  };\n\n  private checkoutInitSucceeded = false;\n  private onCheckoutButtonClickedBound = this.onCheckoutButtonClicked.bind(this);\n  private onCartTransactionStartedBound = this.onCartTransactionStarted.bind(this);\n  private onCartTransactionCompletedBound = this.onCartTransactionCompleted.bind(this);\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n  private onReadyBound = this.onReady.bind(this);\n  private onPageShowBound = this.onPageShow.bind(this);\n  private onContinueShoppingButtonClickedBound = this.onContinueShoppingButtonClicked.bind(this);\n\n  private declare releaseReady: ReleaseHandle;\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    this.releaseReady = ready(this.onReadyBound);\n\n    addEventListener('pageshow', this.onPageShowBound);\n  }\n\n  public disconnectedCallback() {\n    const checkoutButton = this.shadowRoot.querySelector<HTMLElement>('#checkout-button');\n    checkoutButton?.removeEventListener('click', this.onCheckoutButtonClickedBound);\n\n    const continueShopping = this.shadowRoot.querySelector<HTMLButtonElement>('#continue-shopping');\n    continueShopping?.removeEventListener('click', this.onContinueShoppingButtonClickedBound);\n\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n    removeEventListener('cart-transaction-started', this.onCartTransactionStartedBound);\n    removeEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n    removeEventListener('pageshow', this.onPageShowBound);\n\n    if (this.releaseReady) {\n      this.releaseReady();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onReady() {\n    if (!this.isConnected) {\n      return;\n    }\n\n    const checkoutButton = this.shadowRoot.querySelector<HTMLElement>('#checkout-button');\n    checkoutButton.addEventListener('click', this.onCheckoutButtonClickedBound);\n\n    const continueButton = this.shadowRoot.querySelector<HTMLButtonElement>('#continue-shopping');\n    if (continueButton) {\n      if (this.dataset.hideCloseButton === 'true') {\n        continueButton.remove();\n      } else {\n        continueButton.addEventListener('click', this.onContinueShoppingButtonClickedBound);\n      }\n    }\n\n    addEventListener('cart-transaction-started', this.onCartTransactionStartedBound);\n    addEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  /**\n   * Handle a click on the continue to checkout button. It turns out that this is a little more\n   * complicated than it seems like it should be at first glance because we have some complicated\n   * race conditions and sad paths to handle.\n   *\n   * One issue is that the navigation sometimes takes a while. The current page remains visible\n   * sometimes right up until a large amount of the next page is loaded. Since the checkout page can\n   * take a terribly long time to load (e.g. 10+ seconds), it gives the user the impression they did\n   * not click on the button at all or that there was some error, and so they naturally click again.\n   * So another issue is that we have to carefully handle multiple clicks.\n   *\n   * Handling multiple clicks is also important because of what happens to the checkout cache during\n   * navigation. Users can edit the checkout on the checkout page. However, we do not keep the cache\n   * up to date on the checkout page. Therefore, the checkout service clears the cached checkout\n   * before redirecting. This causes an error if the user manages to click on the continue to\n   * checkout button more than once, after the cache has been cleared, but before the new page has\n   * loaded. We have to prevent those additional clicks from trying to trigger the redirect again\n   * because it leads to an error where the checkout is not found in the cache.\n   *\n   * Another subtle issue here is that disabling the button does not actually make it so that the\n   * button cannot be clicked or that its click handler is not invoked. So we have to actually check\n   * for whether the button is disabled. This is because a click that lands on a child element of a\n   * disabled button still bubbles up to and invokes this handler in Chrome and Safari (Firefox\n   * stops the bubbling). Form membership is not a factor; the button is a real button element.\n   *\n   * We only need to re-enable the button on error. On successful redirection we can leave the\n   * button state as disabled up until the browser changes the page.\n   *\n   * We do not have to log errors to Sentry here. Error handling and console logging is done by the\n   * checkout service.\n   *\n   * @see https://stackoverflow.com/questions/37109771\n   */\n  private onCheckoutButtonClicked(event: MouseEvent) {\n    const button = event.currentTarget;\n\n    // We need to narrow the type of the button variable from EventTarget to HTMLButtonElement. We\n    // want to do so without hard casting the type. The listener is bound to the element with the id\n    // #checkout-button and we are using currentTarget instead of target. This element is an actual\n    // button in the HTML. We want to later have access to the disabled property, which is not\n    // available on HTMLElement, but is on HTMLButtonElement. The correct expression here is to use\n    // instanceof HTMLButtonElement.\n\n    if (!(button instanceof HTMLButtonElement)) {\n      console.log('ignoring checkout button click because click is not on a button');\n      return;\n    }\n\n    // A disabled button does not receive clicks on itself, but a click on one of its child elements\n    // still bubbles up through the disabled button and reaches this listener in Chrome and Safari.\n    // So we manually enforce the disabled state instead of relying on the browser to prevent\n    // clicks.\n\n    if (button.disabled) {\n      console.log('ignoring checkout button click because button is disabled');\n      return;\n    }\n\n    try {\n      sessionStorage.setItem('checkout_button_clicked_epoch', Date.now().toString());\n    } catch (error) {\n      console.warn(error);\n    }\n\n    const cartDrawer = this.closest('cart-drawer');\n    const upsellView = cartDrawer?.querySelector('upsell-view');\n    if (upsellView) {\n      // query all upsells that are not hidden\n      const activeUpsells = upsellView.querySelectorAll('[data-product-id]:not([hidden])');\n      if (activeUpsells.length > 0 && !cart_drawer.upsell_modal_displayed) {\n        cartDrawer.dataset.view = 'upsell';\n        cart_drawer.upsell_modal_displayed = true;\n\n        // The messages are stale by the time the shopper comes back to the cart from the upsell.\n        const cartElement = this.closest('cart-element');\n        if (cartElement) {\n          cartElement.dataset.clearMessages = 'true';\n        }\n\n        return;\n      } else {\n        console.log('not showing upsell view on checkout button click');\n      }\n    } else {\n      console.log('element upsell-view not found on checkout button click');\n    }\n\n    // Disable the checkout button. This does not actually prevent it from being clicked because the\n    // button is located outside of a form element. However, it does cause future click events to be\n    // handled by the click listener differently. Disabling the button before the redirect is\n    // important because there is sometimes a substantial visual lag that occurs during the redirect\n    // where we have observed visitors try to click the button multiple times while waiting.\n    button.disabled = true;\n\n    // This uses a relative url so as to work on multiple domains\n    const url = new URL(Shopify.routes.root + 'checkout', 'https://langehair.com');\n    if (this.dataset.skipShopPay === 'true') {\n      url.searchParams.set('skip_shop_pay', 'true');\n    }\n\n    location.href = url.toString();\n  }\n\n  /**\n   * We only start listening for this event from within the connected callback so we know the dom is\n   * ready and the backdrop element exists.\n   */\n  private onCartTransactionStarted(_event: Event) {\n    const button = this.shadowRoot.querySelector<HTMLButtonElement>('#checkout-button');\n    button.disabled = true;\n  }\n\n  private onCartTransactionCompleted(_event: WindowEventMap['cart-transaction-completed']) {\n    if (this.checkoutInitSucceeded) {\n      this.endLoading();\n    }\n  }\n\n  /**\n   * Enable the button and hide the spinner.\n   */\n  private endLoading() {\n    const button = this.shadowRoot.querySelector<HTMLButtonElement>('#checkout-button');\n    button.disabled = false;\n  }\n\n  private onCartUpdated(event: GlobalEventHandlersEventMap['cart-updated']) {\n    if (event.detail.is_initial) {\n      this.checkoutInitSucceeded = true;\n    }\n\n    // Only enable the button if we previously observed a successful checkout-initialized event.\n    if (this.checkoutInitSucceeded) {\n      const button = this.shadowRoot.querySelector<HTMLButtonElement>('#checkout-button');\n      button.disabled = false;\n    }\n  }\n\n  private onPageShow(event: PageTransitionEvent) {\n    if (event.persisted) {\n      this.endLoading();\n    }\n  }\n\n  private onContinueShoppingButtonClicked(_event: MouseEvent) {\n    const cartDrawer = this.closest('cart-drawer');\n    if (cartDrawer?.dataset.view === 'upsell') {\n      cartDrawer.dataset.view = 'cart';\n    } else {\n      type Detail = WindowEventMap['cart-close-requested']['detail'];\n      const event = new CustomEvent<Detail>('cart-close-requested', {\n        bubbles: true,\n        composed: true,\n        detail: {\n          animate: true,\n          source: 'continue-shopping-button'\n        }\n      });\n      this.dispatchEvent(event);\n    }\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'cart-actions': CartActions;\n  }\n}\n\nif (!customElements.get('cart-actions')) {\n  customElements.define('cart-actions', CartActions);\n}\n","/**\n * Returns a timestamp in milliseconds for measuring durations.\n *\n * This decorates performance.now().\n *\n * This uses performance.now(), which counts from page load rather than the Unix epoch, and falls\n * back to Date.now().\n *\n * If native throws, this still returns a value, but it logs a message as a side effect.\n *\n * This happens in a try/catch, so it is safe.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/now\n */\nexport function now() {\n  try {\n    if (typeof performance?.now === 'function') {\n      return performance.now();\n    }\n  } catch (error) {\n    console.debug(error);\n  }\n\n  return Date.now();\n}\n\n/**\n * Records a named point on the User Timing timeline in devtools.\n *\n * This decorates performance.mark within a try/catch that suppresses errors.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark\n */\nexport function mark(name: string) {\n  try {\n    if (typeof performance?.mark === 'function') {\n      performance.mark(name);\n    }\n  } catch (error) {\n    console.debug(error);\n  }\n}\n\n/**\n * Records a named span between two timestamps on the User Timing timeline in devtools.\n *\n * This decorates performance.measure within a try/catch that suppresses errors.\n *\n * The bounds are explicit timestamps rather than named marks, which avoids the error the marked\n * name form throws when a start mark was never recorded.\n *\n * The detail param is optional and must be structured-cloneable. It should be a plain object. If\n * the value cannot be cloned, then performance.measure throws, which is caught and logged by this\n * wrapper. If specified, the detail shows up in the tooltip in the devtools panel.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure\n */\nexport function measure(name: string, start: number, end: number, detail?: unknown) {\n  try {\n    if (typeof performance?.measure === 'function') {\n      performance.measure(name, { start, end, detail });\n    }\n  } catch (error) {\n    console.debug(error);\n  }\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { getEventQueue, isCartUpdatedEvent, isCartUpdateErredEvent } from '../../../lib/cart-event-queue.js';\nimport { now } from '../../../lib/perf.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * This element is a higher level wrapper to implement the shopping bag drawer. It does not require\n * any knowledge of the underlying cart-element functionality and is only concerned with managing\n * the drawer state.\n *\n * The drawer shows and hides itself off `data-modal-state` rather than `data-show`. For now it sets\n * that attribute itself whenever `data-show` changes. Later the modal manager sets it instead.\n *\n * The panel holds one or more views, each a named slot ending in `-view`, and shows only the one\n * matching `data-view`. The progress view is shown while a cart change is in flight, and the drawer\n * switches it back to the cart once the change completes. It is also shown while the cart\n * initializes, and the drawer switches it back to the cart once init succeeds or fails.\n *\n * On the cart page the element is rendered with `data-layout=\"embedded\"`. The panel then sits in\n * the page as an ordinary block that is always shown, rather than a dialog, and the drawer control\n * script leaves it alone.\n */\nclass CartDrawer extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-modal-state', 'data-show', 'data-view'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * If 'true' the opening and closing of the modal will be animated\n     */\n    animate: 'false' | 'true';\n\n    /**\n     * \"true\" while the cart is initializing. Set and cleared by this element, and read by the\n     * drawer control so a drawer opened during init opens on the progress view.\n     */\n    cartInitializing: string;\n\n    /**\n     * \"embedded\" on the cart page, where the panel sits in the page instead of floating over it.\n     * Missing means the usual drawer. Set in the liquid and fixed for the life of the page.\n     */\n    layout: string;\n\n    /**\n     * Whether the cart drawer is shown, as \"open\". Anything else, or no value at all, means closed.\n     *\n     * A change only takes effect while the element is on the page. A change made while it is\n     * detached is ignored, and connecting it later does not pick it up. Markup that arrives with\n     * the attribute already set still works.\n     */\n    modalState: string;\n\n    /**\n     * Whether the cart drawer is open.\n     *\n     * If the attribute is set to \"true\" the cart drawer is visible. If the attribute is set to\n     * \"false\", not set, or missing, the cart drawer will not be visible.\n     *\n     * A change only takes effect while the element is on the page. A change made while it is\n     * detached is ignored, and connecting it later does not pick it up. Markup that arrives with\n     * the attribute already set still works.\n     */\n    show: 'false' | 'true';\n\n    /**\n     * The name of the view shown in the panel, such as \"cart\", which shows the slot named \"cart-\n     * view\". Missing means \"cart\".\n     */\n    view: string;\n  };\n\n  private onCartInitRequestedBound = this.onCartInitRequested.bind(this);\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n  private onCartUpdateErredBound = this.onCartUpdateErred.bind(this);\n  private onCartTransactionCompletedBound = this.onCartTransactionCompleted.bind(this);\n  private showCartAfterProgressBound = this.showCartAfterProgress.bind(this);\n\n  /**\n   * When the progress view was last switched to, so the switch back to the cart can wait out the\n   * minimum time the progress view stays up.\n   */\n  private declare progressShownAt: number;\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    if (this.dataset.layout === 'embedded') {\n      this.renderEmbeddedLayout();\n    }\n\n    // Usually this element is built before the cart arrives, so it starts on the progress view. The\n    // cart can have landed already though, and then the progress view would flash up for nothing,\n    // so the queue decides. Any error in the queue counts as settled, no need to single out the\n    // init failure. Every other code comes from an operation that needs a working cart.\n    const settled = getEventQueue().some(event => isCartUpdatedEvent(event) ||\n      isCartUpdateErredEvent(event));\n    if (!settled) {\n      this.startInitProgress();\n    }\n\n    this.renderView();\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-init-requested', this.onCartInitRequestedBound);\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n    addEventListener('cart-update-erred', this.onCartUpdateErredBound);\n    addEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('cart-init-requested', this.onCartInitRequestedBound);\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n    removeEventListener('cart-update-erred', this.onCartUpdateErredBound);\n    removeEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n  }\n\n  public attributeChangedCallback(name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && name === 'data-show' && oldValue !== newValue) {\n      this.dataset.modalState = newValue === 'true' ? 'open' : 'closed';\n    }\n\n    if (this.isConnected && name === 'data-modal-state' && oldValue !== newValue) {\n      this.render();\n    }\n\n    // The liquid writes the first view into the markup, and this callback also runs for it when the\n    // element upgrades. That is not a switch, and connecting renders it, so we skip it here rather\n    // than scroll and move focus on page load.\n    if (this.isConnected && name === 'data-view' && oldValue !== null && oldValue !== newValue) {\n      this.onViewChanged();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  /**\n   * If the emitted \"cart-updated\" event is not the initialization of the cart on page load, we must\n   * scroll the cart drawer window to the top to ensure any cart related messaging is brought into\n   * view for the customer.\n   *\n   * Only the cart view shows that messaging. Adding an item from the upsell view also updates the\n   * cart, and scrolling there would jump the shopper away from the list they are browsing.\n   */\n  private onCartUpdated(event: GlobalEventHandlersEventMap['cart-updated']) {\n    this.endInitProgress();\n\n    if (!event.detail.is_initial && (this.dataset.view || 'cart') === 'cart') {\n      const modal = this.shadowRoot.querySelector('#panel');\n      modal.scrollTo(0, 0);\n    }\n  }\n\n  /**\n   * A failed init never sends a cart, so the error ends the progress view instead. The error\n   * message renders in the cart view.\n   */\n  private onCartUpdateErred(_event: GlobalEventHandlersEventMap['cart-update-erred']) {\n    this.endInitProgress();\n  }\n\n  /**\n   * The shopper asked to load their bag again after init failed, so the progress view comes back\n   * while init runs again.\n   */\n  private onCartInitRequested(_event: WindowEventMap['cart-init-requested']) {\n    this.startInitProgress();\n  }\n\n  /**\n   * The flag is set before the switch, so the switch sees it and stays quiet on the cart page.\n   */\n  private startInitProgress() {\n    this.dataset.cartInitializing = 'true';\n\n    const progressView = this.querySelector<HTMLElement>('[slot=\"progress-view\"]');\n    if (progressView) {\n      progressView.dataset.message = 'Just a moment while we load up your shopping bag ...';\n    }\n\n    this.dataset.view = 'progress';\n  }\n\n  /**\n   * Runs on every cart update, so it does nothing unless init is what the progress view is up for.\n   * The flag is cleared after the switch, so the switch still sees it and stays quiet on the cart\n   * page.\n   */\n  private endInitProgress() {\n    if (this.dataset.cartInitializing !== 'true') {\n      return;\n    }\n\n    if (this.dataset.view === 'progress') {\n      this.dataset.view = 'cart';\n    }\n\n    delete this.dataset.cartInitializing;\n  }\n\n  /**\n   * Any cart change finishing switches the progress view back to the cart, which is what keeps the\n   * progress view from being tied to one kind of change. A change that failed completes too, so its\n   * error shows on the cart view. The progress view stays up at least 250ms, so a fast change does\n   * not flash it for a split second.\n   *\n   * This runs while the drawer is closed too, such as when the shopper closed it partway through an\n   * add. Switching a closed drawer is harmless, since it picks its view again when it next opens.\n   */\n  private onCartTransactionCompleted(_event: WindowEventMap['cart-transaction-completed']) {\n    if (this.dataset.view === 'progress') {\n      const remaining = 250 - (now() - this.progressShownAt);\n      setTimeout(this.showCartAfterProgressBound, Math.max(0, remaining));\n    }\n  }\n\n  /**\n   * Checks the view again because the drawer may have closed and reopened on the cart, or moved on\n   * to another view, while the minimum time ran out.\n   */\n  private showCartAfterProgress() {\n    if (this.dataset.view === 'progress') {\n      this.dataset.view = 'cart';\n    }\n  }\n\n  private showCartDrawer() {\n    const modal = this.shadowRoot.querySelector<HTMLElement>('#panel');\n    modal.inert = false;\n    modal.scrollTo(0, 0);\n\n    this.focusView();\n  }\n\n  private hideCartDrawer() {\n    const modal = this.shadowRoot.querySelector<HTMLElement>('#panel');\n    modal.inert = true;\n  }\n\n  private render() {\n    if (this.dataset.modalState === 'open') {\n      this.showCartDrawer();\n    } else {\n      this.hideCartDrawer();\n    }\n  }\n\n  /**\n   * Shows the view slot matching `data-view` and hides the rest. Hiding a slot hides whatever is\n   * assigned to it.\n   */\n  private renderView() {\n    const viewSlotName = `${this.dataset.view || 'cart'}-view`;\n    const slots = this.shadowRoot.querySelectorAll<HTMLSlotElement>('slot[name$=\"-view\"]');\n    for (const slot of slots) {\n      slot.hidden = slot.name !== viewSlotName;\n    }\n  }\n\n  /**\n   * Turns the panel from a dialog into a plain block for the cart page. The panel takes focus when\n   * the view switches, so it needs a tab index to be focusable, and -1 keeps it out of the tab\n   * order.\n   */\n  private renderEmbeddedLayout() {\n    const panel = this.shadowRoot.querySelector<HTMLElement>('#panel');\n    panel.removeAttribute('role');\n    panel.removeAttribute('aria-modal');\n    panel.removeAttribute('aria-label');\n    panel.inert = false;\n    panel.tabIndex = -1;\n  }\n\n  /**\n   * Asks the view being shown to take focus. Each view puts it on its own close button, which sits\n   * in that view's header.\n   */\n  private focusView() {\n    const view = this.querySelector<HTMLElement>(`[slot=\"${this.dataset.view || 'cart'}-view\"]`);\n    if (view) {\n      view.dataset.focusRequested = 'true';\n    }\n  }\n\n  /**\n   * A switch while the drawer is open starts the new view at its top, with focus on the new view\n   * the same as when the drawer opens.\n   */\n  private onViewChanged() {\n    this.renderView();\n\n    if (this.dataset.view === 'progress') {\n      this.progressShownAt = now();\n    }\n\n    if (this.dataset.layout === 'embedded') {\n      // Init switches happen on their own as the page loads, so they leave the page and focus where\n      // they are.\n      if (this.dataset.cartInitializing === 'true') {\n        return;\n      }\n\n      // The shopper is usually scrolled down to the Checkout button when the view switches, so the\n      // page goes back to the top to bring the start of the new view on screen.\n      scrollTo(0, 0);\n      this.shadowRoot.querySelector<HTMLElement>('#panel').focus({ preventScroll: true });\n    } else if (this.dataset.modalState === 'open') {\n      this.shadowRoot.querySelector('#panel').scrollTo(0, 0);\n      this.focusView();\n    }\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'cart-drawer': CartDrawer;\n  }\n}\n\nif (!customElements.get('cart-drawer')) {\n  customElements.define('cart-drawer', CartDrawer);\n}\n","/**\n * Represents the properties of a cookie.\n */\nexport interface Cookie {\n  /**\n   * Include a leading \".\" to include subdomains.\n   *\n   * @example \".langehair.com\"\n   */\n  domain: string;\n\n  /**\n   * Indicates the maximum lifetime of the cookie as an HTTP-date timestamp. See Date for the\n   * required formatting.\n   *\n   * If unspecified, the cookie becomes a session cookie. A session finishes when the client shuts\n   * down, after which the session cookie is removed.\n   *\n   * Warning: Many web browsers have a session restore feature that will save all tabs and restore\n   * them the next time the browser is used. Session cookies will also be restored, as if the\n   * browser was never closed.\n   *\n   * When an Expires date is set, the deadline is relative to the client the cookie is being set on,\n   * not the server.\n   */\n  expires: Date;\n\n  /**\n   * A <cookie-name> can contain any US-ASCII characters except for: control characters (ASCII\n   * characters `0` up to `31` and ASCII character `127`) or separator characters (space, tab and\n   * the characters: `( ) < > @ , ; : \\ \" / [ ] ? = { })`.\n   */\n  name: string;\n\n  /**\n   * Path is generally \"/\" which is for all pages for a domain. Otherwise the cookie is specific to\n   * the given path (e.g. particular page).\n   */\n  path: string;\n\n  /**\n   * Controls whether or not a cookie is sent with cross-site requests, providing some protection\n   * against cross-site request forgery attacks (CSRF).\n   *\n   * * Strict Means that the browser sends the cookie only for same-site requests, that is, requests\n   *   originating from the same site that set the cookie. If a request originates from a different\n   *   domain or scheme (even with the same domain), no cookies with the SameSite=Strict attribute\n   *   are sent.\n   * * Lax Means that the cookie is not sent on cross-site requests, such as on requests to load\n   *   images or frames, but is sent when a user is navigating to the origin site from an external\n   *   site (for example, when following a link). This is the default behavior if the SameSite\n   *   attribute is not specified.\n   * * None means that the browser sends the cookie with both cross-site and same-site requests. The\n   *   Secure attribute must also be set when setting this value, like so SameSite=None; Secure. If\n   *   Secure is missing an error will be logged\n   *\n   * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie\n   */\n  same_site: 'Lax' | 'None' | 'Strict';\n\n  /**\n   * Indicates that the cookie is sent to the server only when a request is made with the https:\n   * scheme (except on localhost), and therefore, is more resistant to man-in-the-middle attacks.\n   */\n  secure: boolean;\n\n  /**\n   * A <cookie-value> can optionally be wrapped in double quotes and include any US-ASCII character\n   * excluding control characters (ASCII characters 0 up to 31 and ASCII character 127), Whitespace,\n   * double quotes, commas, semicolons, and backslashes.\n   */\n  value: string;\n}\n\n/**\n * Decodes a percent-encoded cookie name or value, falling back to the raw input when decoding is\n * not possible.\n *\n * Cookie storage is shared by every script on a domain, including third party libraries that write\n * raw, improperly-encoded values. A single cookie whose value contains a malformed percent-\n * sequence, such as a literal percent sign that is not followed by two hexadecimal digits, makes\n * decodeURIComponent throw a URIError. Because the whole cookie string is parsed at once, one such\n * cookie would otherwise poison every read on the page rather than just its own entry. Returning\n * the undecoded text keeps the remaining cookies readable, and a value that was never percent-\n * encoded is returned unchanged anyway.\n */\nfunction safeDecode(value: string) {\n  try {\n    return decodeURIComponent(value);\n  } catch (error) {\n    return value;\n  }\n}\n\n/**\n * Parses the value of document.cookie into an an array of cookie objects.\n *\n * **This decodes the cookie name and value. You should not decode the cookie name or value\n * yourself**.\n *\n * Cookies can be set with separate flags. When there are two cookies with the same name but with\n * different flags, the name will appear twice in document.cookie. Therefore, this returns an array\n * of Cookie objects instead of a single object where each name is a property.\n *\n * @throws {Error} if document is undefined, if document.cookie cannot be accessed for some reason\n * such as security restrictions on iframes or browser restrictions, or if document.cookie does not\n * produce a string\n */\nexport function parse() {\n  return document.cookie.split(';').map<Pick<Cookie, 'name' | 'value'>>(parts => {\n    const index = parts.indexOf('=');\n    // decodeURIComponent(undefined) produces strange results in some browsers, such as the string\n    // undefined, so we have to avoid that\n    const encodedValue = parts.slice(index + 1)?.trim();\n    return {\n      name: safeDecode(parts.slice(0, index).trim()),\n      value: typeof encodedValue === 'string' ? safeDecode(encodedValue) : undefined\n    };\n  });\n}\n\n/**\n * Retrieves the value of a particular cookie by name.\n *\n * If a cookie with the given name is not found, then undefined is returned.\n *\n * Internally this calls a private function to parse the cookie and that function accesses\n * document.cookie and for that reason this function can throw. This will almost never happen except\n * when attempting to access cookies cross-context, e.g. accessing parent window cookies from within\n * an iframe where the iframe has certain attributes. Some third party libraries do this naively and\n * without concern for the security constraints imposed by browsers. As a result, you should always\n * consider handling errors when calling this function.\n *\n * Because this can throw, **please implement proper error handling** when calling this function.\n *\n * **This decodes the cookie name and value. You should not decode the cookie name or value\n * yourself**.\n *\n * Cookie values are always strings. If you happen to be storing something like a JSON-like object\n * in the cookie value, then the raw JSON string is returned and is not parsed on your behalf. If\n * you are enclosing your value in double quotes, the leading and trailing double quote characters\n * are included in the returned value.\n *\n * **WARNING**: This is a naive read. When examining the list of cookies, this returns the first one\n * in the list. More than one cookie can have the same name when two or more cookies have the same\n * name but different flags. This is entirely dependent on the order of the names in\n * document.cookie, which is completely unknown and can change at any time and probably varies by\n * browser. Therefore, when there is more than one cookie with the same name, it is possible this\n * returns an unexpected cookie value.\n *\n * @throws {SecurityError}\n * @throws {Error}\n */\nexport function read(name: string) {\n  return readAll(name).shift();\n}\n\nexport function readAll(name: string) {\n  return parse().filter(cookie => cookie.name === name).map(cookie => cookie.value);\n}\n\n/**\n * Given a cookie, generate the string that can be supplied to document.cookie\n *\n * **This encodes the cookie name and cookie. Do not encode these yourself**.\n */\nexport function serialize(cookie: Partial<Cookie>) {\n  if (!cookie.name) {\n    throw new Error('Cookie name is required');\n  }\n\n  const parts: string[] = [];\n\n  parts.push(`${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value || '')}`);\n\n  if (typeof cookie.path === 'string') {\n    parts.push(`Path=${cookie.path}`);\n  }\n\n  if (typeof cookie.domain === 'string') {\n    parts.push(`Domain=${cookie.domain}`);\n  }\n\n  if (cookie.expires && typeof cookie.expires.getTime === 'function') {\n    parts.push(`Expires=${cookie.expires.toUTCString()}`);\n  }\n\n  if (typeof cookie.same_site === 'string') {\n    parts.push(`SameSite=${cookie.same_site}`);\n  }\n\n  if (cookie.secure) {\n    parts.push('Secure');\n  }\n\n  // The delimiter must include a single space after semicolon\n\n  return parts.join('; ');\n}\n\n/**\n * This function can throw because setting document.cookie sometimes throws an error.\n *\n * This function can throw on invalid input, such as when not specifying a required property such as\n * cookie name.\n *\n * **This encodes the cookie name and cookie. Do not encode these yourself**.\n */\nexport function write(cookie: Partial<Cookie>) {\n  document.cookie = serialize(cookie);\n}\n\n/**\n * Deletes a cookie.\n *\n * Of the cookie properties, only the name is required. However, some browsers are extremely strict\n * about how a cookie is deleted. To delete a cookie the exact same other properties must also be\n * provided, other than the cookie's value and expiration date. For example, if the cookie to be\n * deleted is secure, then you must specify the secure property to delete the cookie.\n *\n * If the properties do not match, this completes without error and the cookie that was to be\n * deleted still remains.\n *\n * Deleting a cookie is done by setting its value to an empty string and then assigning the full\n * cookie value to document.cookie.\n *\n * This function may throw an error because deleting a cookie is done by assigning a value to\n * document.cookie which can sometimes throw.\n *\n * The expires property is ignored. This sets expiration date to 1970 to force cookie to become\n * expired.\n *\n * Insecure cookies cannot be removed. These are cookies belonging to the http protocol.\n */\nexport function remove(cookie: Partial<Cookie>) {\n  write({ ...cookie, value: '', expires: new Date(0) });\n}\n","import { read } from '@integrabeauty/cookie';\nimport type { CustomElement } from '@integrabeauty/custom-elements';\nimport type { Cart } from '@integrabeauty/shopify-ajax-api';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\ntype PriceEl = HTMLElementTagNameMap['price-element'];\ntype ProductEl = HTMLElementTagNameMap['product-element'];\ntype VariantEl = HTMLElementTagNameMap['variant-element'];\n\n/**\n * A single selectable reward variant scraped from a `<variant-element>` inside a `<cart-gwp-tier>`.\n * Data is extracted once at parse time and stored as plain values\n */\ninterface Variant {\n  /**\n   * Whether this variant is available to add to cart. Unavailable variants are filtered out before\n   * rendering.\n   */\n  available: boolean;\n\n  /**\n   * Original price before discount, in cents. Zero if there is no compare-at price.\n   */\n  compareAtPrice: number;\n\n  /**\n   * URL of the variant image, or the product's main image as a fallback. Empty string if no image\n   * is available.\n   */\n  imageSrc: string;\n\n  /**\n   * Display title combining the product title and variant title, e.g. \"Shampoo - 8oz\". When the\n   * variant title is \"Default Title\" (Shopify placeholder for single-option products), only the\n   * product title is used.\n   */\n  title: string;\n\n  /**\n   * URL to the specific variant page, e.g. \"/products/shampoo?variant=123\". Used as the href for\n   * the image clickable area.\n   */\n  url: string;\n\n  /**\n   * Shopify variant ID as a string.\n   */\n  variantId: string;\n}\n\n/**\n * A single GWP reward tier parsed from a child {@link CartGwpTier} element.\n */\ninterface GwpTier {\n  /**\n   * Message shown when the threshold has been reached but the reward has not yet been claimed.\n   * Defaults to \"Your free gift is waiting\" if not set in Shopify Admin.\n   */\n  claimMessage: string;\n\n  /**\n   * Cart total in cents at the time of the last state update. Used by the active tier's progress\n   * bar to show current spend vs threshold. Zero until the first cart-updated event is processed.\n   */\n  currentAmountCents: number;\n\n  /**\n   * Tail of the message shown on locked tiers: \"Add $X to {goalText}\". Should be written in\n   * lowercase to continue the sentence, e.g. \"unlock free shipping\". Defaults to \"unlock free\n   * shipping\" or \"unlock a free gift\" based on reward type if not set in Admin.\n   */\n  goalText: string;\n\n  /**\n   * Discount code that activates this reward. Applied to the cart when the customer adds the GWP\n   * variant, and used to detect whether the reward is already present in the cart via line-level\n   * discount allocations.\n   */\n  promoCode: string;\n\n  /**\n   * Identifies the reward type. Known values: 'free_gift', 'free_shipping'.\n   */\n  rewardType: string;\n\n  /**\n   * Current display state of this tier. Recalculated on every cart-updated event before rendering.\n   */\n  state: TierState;\n\n  /**\n   * Minimum cart total required to unlock this reward, in cents.\n   */\n  thresholdCents: number;\n\n  /**\n   * Message shown once the reward has been claimed or the shipping tier unlocked. Defaults to\n   * \"You've unlocked free shipping\" or \"You've unlocked a free gift\" based on reward type.\n   */\n  unlockedMessage: string;\n\n  /**\n   * Available reward variants scraped from the tier's nested `<product-element>` children. Only\n   * direct-child variants are included to avoid picking up variants from bundle components.\n   */\n  variants: Variant[];\n}\n\n/**\n * Represents the current display state of a single reward tier.\n *\n * - `locked` — threshold not reached and not the next tier in line\n * - `active` — the next tier to unlock; progress bar is shown\n * - `unclaimed` — threshold reached, free product available to add to cart\n * - `claimed` — threshold reached and the free product is already in cart\n * - `unlocked_shipping` — threshold reached for a free_shipping reward\n */\ntype TierState = 'active' | 'claimed' | 'locked' | 'unclaimed' | 'unlocked_shipping';\n\n/**\n * This element renders gift-with-purchase reward tiers inside the cart drawer. It reacts to cart-\n * updated events to recalculate tier states and re-render accordingly.\n *\n * Each tier is sourced from a child `<cart-gwp-tier>` element written by Liquid. Tier data is\n * parsed once and cached, since the DOM does not change between cart updates.\n */\nclass CartGwp extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * ISO 4217 currency code for the current market, e.g. \"USD\".\n     */\n    currencyCode: string;\n\n    /**\n     * Currency symbol character for the current market, e.g. \"$\".\n     */\n    currencySymbol: string;\n  };\n\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n\n  /**\n   * Tier data parsed from child cart-gwp-tier elements. Cached after the first parse because the\n   * Liquid-rendered DOM structure does not change during the page session.\n   */\n  private tiers: GwpTier[] | null = null;\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    // Replay any cart-updated events that fired before this element connected, so the initial cart\n    // state is applied immediately without waiting for the next update.\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onCartUpdated(event: GlobalEventHandlersEventMap['cart-updated']) {\n    this.updateTierStates(event.detail.cart);\n    this.render();\n    this.maybeAutoAdd();\n  }\n\n  private updateTierStates(cart: Cart) {\n    const tiers = this.collectTierData();\n    if (tiers.length === 0) {\n      return;\n    }\n\n    const lockedThresholds = tiers\n      .filter(t => cart.total_price < t.thresholdCents)\n      .map(t => t.thresholdCents);\n\n    // The active tier is the locked tier with the lowest threshold (next in line to unlock).\n    const nextThreshold = Math.min(...lockedThresholds);\n\n    for (const tier of tiers) {\n      tier.state = determineTierState(tier, cart, nextThreshold);\n      tier.currentAmountCents = cart.total_price;\n    }\n  }\n\n  /**\n   * Simulates an add-to-cart-button click for every unclaimed free-gift tier that has exactly one\n   * available variant, when the customer is in the GWP auto-add test group. render() must be called\n   * first so the buttons are present in the shadow DOM.\n   */\n  private maybeAutoAdd() {\n    if (readGwpAutoAddCookie() !== 'true') {\n      return;\n    }\n\n    const tiers = this.collectTierData();\n    for (const tier of tiers) {\n      if (tier.state !== 'unclaimed' || tier.rewardType !== 'free_gift') {\n        continue;\n      }\n\n      const available = tier.variants.filter(v => v.available);\n      if (available.length !== 1) {\n        continue;\n      }\n\n      const selector =\n        `.tier add-to-cart-button[data-discount-code=\"${CSS.escape(tier.promoCode)}\"]`;\n      const addToCartButton = this.shadowRoot.querySelector(selector);\n      const button = addToCartButton.shadowRoot.querySelector('button');\n      button.click();\n    }\n  }\n\n  private collectTierData() {\n    if (this.tiers) {\n      return this.tiers;\n    }\n\n    const tiers: GwpTier[] = [];\n\n    const tierElements = this.querySelectorAll('cart-gwp-tier');\n    for (const tierEl of tierElements) {\n      const rewardType = tierEl.dataset.rewardType ?? '';\n      const isShipping = rewardType === 'free_shipping';\n\n      tiers.push({\n        promoCode: tierEl.dataset.promoCode ?? '',\n        thresholdCents: parseInt(tierEl.dataset.thresholdCents ?? '0', 10),\n        rewardType,\n        goalText: tierEl.dataset.goalText ||\n          (isShipping ? 'unlock free shipping' : 'unlock a free gift'),\n        claimMessage: tierEl.dataset.claimMessage || 'Your free gift is waiting',\n        unlockedMessage: tierEl.dataset.unlockedMessage ||\n          (isShipping ? 'You\\'ve unlocked free shipping' : 'You\\'ve unlocked a free gift'),\n        variants: parseVariants(tierEl),\n        state: 'locked',\n        currentAmountCents: 0\n      });\n    }\n\n    tiers.sort((a, b) => a.thresholdCents - b.thresholdCents);\n    this.tiers = tiers;\n    return tiers;\n  }\n\n  private renderTier(tier: GwpTier) {\n    const template = this.shadowRoot.querySelector<HTMLTemplateElement>('#tier');\n    const frag = document.importNode(template.content, true);\n    const tierEl = frag.querySelector('.tier');\n\n    tierEl.classList.add(tier.state);\n\n    const isUnlocked = tier.state === 'unclaimed' ||\n      tier.state === 'claimed' ||\n      tier.state === 'unlocked_shipping';\n    const iconId = isUnlocked ? '#icon-checkmark' : '#icon-locked';\n    const iconTemplate = this.shadowRoot.querySelector<HTMLTemplateElement>(iconId);\n    tierEl.querySelector('.icon').appendChild(document.importNode(iconTemplate.content, true));\n\n    const labelPriceEl = tierEl.querySelector<PriceEl>('.label-price');\n    const labelNameEl = tierEl.querySelector('.label-name');\n    if (tier.state === 'active') {\n      labelPriceEl.dataset.currencyCode = this.dataset.currencyCode;\n      labelPriceEl.dataset.currencySymbol = this.dataset.currencySymbol;\n      labelPriceEl.dataset.subunits = tier.thresholdCents.toString();\n      labelNameEl.textContent = ` — ${getTierLabel(tier.rewardType, tier.state)}`;\n    } else {\n      labelPriceEl.remove();\n      labelNameEl.textContent = getTierLabel(tier.rewardType, tier.state);\n    }\n\n    const amountsEl = tierEl.querySelector('.amounts');\n    if (tier.state === 'active' || isUnlocked) {\n      const currentSubunits = tier.state === 'active' ?\n        Math.min(tier.currentAmountCents, tier.thresholdCents) :\n        tier.thresholdCents;\n\n      const currentEl = amountsEl.querySelector<PriceEl>('.current');\n      currentEl.dataset.currencyCode = this.dataset.currencyCode;\n      currentEl.dataset.currencySymbol = this.dataset.currencySymbol;\n      currentEl.dataset.subunits = currentSubunits.toString();\n\n      const thresholdEl = amountsEl.querySelector<PriceEl>('.threshold');\n      thresholdEl.dataset.currencyCode = this.dataset.currencyCode;\n      thresholdEl.dataset.currencySymbol = this.dataset.currencySymbol;\n      thresholdEl.dataset.subunits = tier.thresholdCents.toString();\n    } else {\n      // Locked tiers with no progress show no amounts.\n      amountsEl.remove();\n    }\n\n    const messagePrefixEl = tierEl.querySelector('.message-prefix');\n    const messagePriceEl = tierEl.querySelector<PriceEl>('.message-price');\n    const messageTextEl = tierEl.querySelector('.message-text');\n\n    if (tier.state === 'locked' || tier.state === 'active') {\n      const remaining = Math.max(0, tier.thresholdCents - tier.currentAmountCents);\n      messagePrefixEl.textContent = 'Add ';\n      messagePriceEl.dataset.currencyCode = this.dataset.currencyCode;\n      messagePriceEl.dataset.currencySymbol = this.dataset.currencySymbol;\n      messagePriceEl.dataset.subunits = remaining.toString();\n      messageTextEl.textContent = ` to ${tier.goalText}`;\n    } else {\n      messagePrefixEl.remove();\n      messagePriceEl.remove();\n      if (tier.state === 'claimed' || tier.state === 'unlocked_shipping') {\n        messageTextEl.textContent = tier.unlockedMessage;\n      } else {\n        messageTextEl.textContent = tier.claimMessage;\n      }\n    }\n\n    const progressEl = tierEl.querySelector('.progress');\n    if (tier.state === 'active') {\n      const percent = tier.thresholdCents > 0 ?\n        Math.min(100, Math.round((tier.currentAmountCents / tier.thresholdCents) * 100)) :\n        100;\n      progressEl.setAttribute('aria-valuenow', percent.toString());\n      progressEl.querySelector<HTMLElement>('.bar').style.width = `${percent}%`;\n    } else {\n      progressEl.remove();\n    }\n\n    const cardsEl = tierEl.querySelector('.cards');\n    if (tier.state === 'unclaimed') {\n      const availableVariants = tier.variants.filter(v => v.available);\n      for (const variant of availableVariants) {\n        cardsEl.appendChild(this.renderVariantCard(variant, tier.promoCode));\n      }\n    } else {\n      cardsEl.remove();\n    }\n\n    return tierEl;\n  }\n\n  private renderVariantCard(variant: Variant, promoCode: string) {\n    const template = this.shadowRoot.querySelector<HTMLTemplateElement>('#variant-card');\n    const frag = document.importNode(template.content, true);\n    const card = frag.querySelector('.card');\n\n    const link = card.querySelector<HTMLAnchorElement>('.image-link');\n    link.href = variant.url;\n\n    const img = link.querySelector('img');\n    if (variant.imageSrc) {\n      img.src = variant.imageSrc;\n      img.alt = variant.title;\n    } else {\n      link.remove();\n    }\n\n    const titleEl = card.querySelector<HTMLSpanElement>('.title');\n    titleEl.textContent = variant.title;\n\n    const compareEl = card.querySelector('.compare');\n    if (variant.compareAtPrice > 0) {\n      const priceEl = compareEl.querySelector('price-element');\n      priceEl.dataset.currencyCode = this.dataset.currencyCode;\n      priceEl.dataset.currencySymbol = this.dataset.currencySymbol;\n      priceEl.dataset.subunits = variant.compareAtPrice.toString();\n    } else {\n      compareEl.remove();\n    }\n\n    // The _is_gwp attribute is a line item property that flags this cart item as a gift-with-\n    // purchase reward. This allows order management and analytics code to identify GWP items\n    // without relying on the discount code alone.\n    const customAttribute = document.createElement('custom-attribute');\n    customAttribute.dataset.key = '_is_gwp';\n    customAttribute.dataset.value = 'true';\n\n    const variantEl = document.createElement('variant-element');\n    variantEl.slot = 'variants';\n    variantEl.dataset.id = variant.variantId;\n    variantEl.dataset.quantity = '1';\n    variantEl.appendChild(customAttribute);\n\n    const addToCartButton = card.querySelector('add-to-cart-button');\n    addToCartButton.dataset.discountCode = promoCode;\n    addToCartButton.appendChild(variantEl);\n\n    return card;\n  }\n\n  private render() {\n    if (!this.tiers?.length) {\n      return;\n    }\n\n    const tiersContainer = this.shadowRoot.querySelector('#tiers');\n    tiersContainer.textContent = '';\n\n    for (const tier of this.tiers) {\n      tiersContainer.appendChild(this.renderTier(tier));\n    }\n  }\n}\n\n/**\n * Returns the value of the allow-gwp-auto-add cookie, or undefined if absent or unavailable.\n */\nfunction readGwpAutoAddCookie() {\n  let value;\n  try {\n    value = read('allow-gwp-auto-add');\n  } catch (error) {\n    // ignore the errors - fallback to showing the normal selection UI for the customer if cookies\n    // are unavailable\n  }\n\n  return value;\n}\n\n/**\n * Extracts all available variants from a tier element's direct `<product-element>` children. Uses\n * `:scope >` selectors to avoid including variants from nested bundle components\n */\nfunction parseVariants(tierEl: CartGwpTier): Variant[] {\n  const variants: Variant[] = [];\n\n  const productElements = tierEl.querySelectorAll<ProductEl>(':scope > product-element');\n  for (const productEl of productElements) {\n    const selectedId = productEl.dataset.selectedVariantId;\n\n    // Prefer the selected variant; fall back to the first available one.\n    const variantEl =\n      productEl.querySelector<VariantEl>(`:scope > variant-element[data-id=\"${selectedId}\"]`) ??\n      productEl.querySelector<VariantEl>(':scope > variant-element[data-id]');\n\n    if (!variantEl) {\n      continue;\n    }\n\n    const productTitle = productEl.dataset.title ?? '';\n    const variantTitle = variantEl.dataset.title ?? '';\n\n    // \"Default Title\" is Shopify's placeholder for single-option products; omit it from the title.\n    const title = variantTitle && variantTitle !== 'Default Title' ?\n      `${productTitle} - ${variantTitle}` :\n      productTitle;\n\n    variants.push({\n      available: variantEl.dataset.available === 'true',\n      compareAtPrice: parseInt(variantEl.dataset.compareAtPrice ?? '0', 10),\n      imageSrc: variantEl.dataset.imageSrc ?? productEl.dataset.mainImage ?? '',\n      title,\n      url: productEl.dataset.url + '?variant=' + variantEl.dataset.id,\n      variantId: variantEl.dataset.id ?? ''\n    });\n  }\n\n  return variants;\n}\n\n/** Calculates the current display state for a tier based on the cart total and unlock order. */\nfunction determineTierState(tier: GwpTier, cart: Cart, nextThreshold: number) {\n  const totalPrice = cart.total_price;\n\n  if (totalPrice >= tier.thresholdCents) {\n    if (tier.rewardType === 'free_shipping') {\n      return 'unlocked_shipping';\n    }\n\n    if (isGwpClaimedInCart(tier.promoCode, cart)) {\n      return 'claimed';\n    }\n\n    return 'unclaimed';\n  }\n\n  if (tier.thresholdCents === nextThreshold) {\n    return 'active';\n  }\n\n  return 'locked';\n}\n\n/**\n * Returns true if a line item in the cart has a discount allocation whose title matches the given\n * promo code. This is how we detect that the customer has already claimed a GWP reward.\n */\nfunction isGwpClaimedInCart(promoCode: string, cart: Cart) {\n  const code = promoCode.toLowerCase();\n  return cart.items.some(item =>\n    item.line_level_discount_allocations.some(alloc =>\n      alloc.discount_application.title.toLowerCase() === code\n    )\n  );\n}\n\n/**\n * Returns the human-readable label for a tier header derived from its reward type and current\n * state. Labels are intentionally generic (\"Free Gift\", \"Free Shipping\") rather than showing the\n * raw promo code.\n */\nfunction getTierLabel(rewardType: string, state: TierState) {\n  if (rewardType === 'free_shipping') {\n    return 'Free Shipping';\n  }\n\n  if (state === 'claimed') {\n    return 'Free Gift Unlocked';\n  }\n\n  return 'Free Gift';\n}\n\n/** Type guard that narrows a queued event to the cart-updated event type. */\n/**\n * Provides typed access to the data attributes on a `<cart-gwp-tier>` element, which is rendered by\n * Liquid from a GWP metaobject tier entry.\n */\ninterface CartGwpTier extends HTMLElement {\n  readonly dataset: {\n    /**\n     * Message shown when the threshold has been reached but the reward has not been claimed.\n     */\n    claimMessage: string;\n\n    /**\n     * Tail of the \"Add $X to {goalText}\" message shown on locked tiers. Should be written in\n     * lowercase, e.g. \"unlock free shipping\". Leave blank to use the default.\n     */\n    goalText: string;\n\n    /**\n     * Discount code that activates this reward.\n     */\n    promoCode: string;\n\n    /**\n     * Identifies the reward type. Known values: 'free_gift', 'free_shipping'.\n     */\n    rewardType: string;\n\n    /**\n     * Minimum cart total required to unlock this reward, in cents, as a string.\n     */\n    thresholdCents: string;\n\n    /**\n     * Message shown once the reward has been claimed or the shipping tier unlocked.\n     */\n    unlockedMessage: string;\n  };\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'cart-gwp': CartGwp;\n    'cart-gwp-tier': CartGwpTier;\n  }\n}\n\nif (!customElements.get('cart-gwp')) {\n  customElements.define('cart-gwp', CartGwp);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport type { Cart, Item } from '@integrabeauty/shopify-ajax-api';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport { ready, type ReleaseHandle } from '../../../lib/dom-ready.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders cart state and provides UI for adding and removing items and starting checkout\n */\nclass CartElement extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-clear-messages', 'data-focus-requested'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * URL for header \"our best sellers\" anchor link\n     */\n    bestSellersUrl: string;\n\n    /**\n     * Set from outside, such as when the drawer opens, to clear the cart messages. Setting it again\n     * clears them again, even with the same value. Removing it clears nothing.\n     */\n    clearMessages: string;\n\n    /**\n     * Country ISO code. Required.\n     *\n     * @example \"US\"\n     */\n    countryCode: string;\n\n    /**\n     * Currency ISO code. Required.\n     *\n     * @example \"US\"\n     */\n    currencyCode: string;\n\n    /**\n     * Currency Symbol. Required.\n     *\n     * @example \"US\"\n     */\n    currencySymbol: string;\n\n    /**\n     * Set to \"true\" by the cart drawer when it shows this view, asking the view to put focus on its\n     * close button. The view removes it again once it has, so the next request fires too.\n     */\n    focusRequested: string;\n\n    /**\n     * The name of the current Shopify template. Affects whether the side cart is displayed.\n     */\n    templateName: string;\n  };\n\n  private onCartTransactionStartedBound = this.onCartTransactionStarted.bind(this);\n  private onCartTransactionCompletedBound = this.onCartTransactionCompleted.bind(this);\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n  private onCloseClickedBound = this.onCloseClicked.bind(this);\n  private onContinueShoppingButtonClickBound = this.onContinueShoppingButtonClick.bind(this);\n  private onReadyBound = this.onReady.bind(this);\n  private onShowDiscountInputClickedBound = this.onShowDiscountInputClicked.bind(this);\n\n  private cartViewedDispatched = false;\n\n  private declare releaseReady: ReleaseHandle;\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n    this.releaseReady = ready(this.onReadyBound);\n  }\n\n  public attributeChangedCallback(name: string, _oldValue: string, newValue: string) {\n    if (this.isConnected && name === 'data-focus-requested' && newValue === 'true') {\n      this.focusCloseButton();\n    }\n\n    if (this.isConnected && name === 'data-clear-messages' && newValue !== null) {\n      this.clearMessages(newValue);\n    }\n  }\n\n  /**\n   * Passes the clear request on to the messages, so callers outside only ever touch this element\n   * and never reach into its shadow root.\n   */\n  private clearMessages(value: string) {\n    const cartMessages = this.shadowRoot.querySelector('cart-messages');\n    if (cartMessages) {\n      cartMessages.dataset.clear = value;\n    }\n  }\n\n  public disconnectedCallback() {\n    const continueShopping = this.shadowRoot.querySelector<HTMLElement>('#continue-shopping');\n    continueShopping?.removeEventListener('click', this.onContinueShoppingButtonClickBound);\n\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton?.removeEventListener('click', this.onCloseClickedBound);\n\n    removeEventListener('cart-transaction-started', this.onCartTransactionStartedBound);\n    removeEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n\n    // Extra paranoia, this should always be defined\n    if (this.releaseReady) {\n      this.releaseReady();\n    }\n\n    const showDiscountInput = this.shadowRoot.querySelector<HTMLElement>(\n      '.show-discount-input button');\n    showDiscountInput?.removeEventListener('click', this.onShowDiscountInputClickedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onReady() {\n    if (!this.isConnected) {\n      return;\n    }\n\n    const continueShopping = this.shadowRoot.querySelector<HTMLElement>('#continue-shopping');\n    continueShopping.addEventListener('click', this.onContinueShoppingButtonClickBound);\n\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton.addEventListener('click', this.onCloseClickedBound);\n\n    if (this.dataset.bestSellersUrl) {\n      const bestSellersEl = this.shadowRoot.querySelector<HTMLAnchorElement>('a.best-sellers-link');\n      bestSellersEl.href = this.dataset.bestSellersUrl;\n    }\n\n    addEventListener('cart-transaction-started', this.onCartTransactionStartedBound);\n    addEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n\n    const showDiscountInput = this.shadowRoot.querySelector<HTMLElement>(\n      '.show-discount-input button');\n    showDiscountInput.addEventListener('click', this.onShowDiscountInputClickedBound);\n  }\n\n  /**\n   * Focus moves here programmatically, so without forcing the visible-focus state the browser\n   * suppresses the focus ring after a mouse-driven open or switch, and the just-focused close\n   * button gives no visual cue of where focus landed. Older browsers that ignore the option degrade\n   * to an unadorned programmatic focus.\n   */\n  private focusCloseButton() {\n    delete this.dataset.focusRequested;\n    this.shadowRoot.querySelector('close-button').focus({ focusVisible: true });\n  }\n\n  private onCloseClicked(_event: MouseEvent) {\n    // A rapid second activation of the close control can fire this again after the first close has\n    // already begun. We do not de-duplicate here and instead rely on the cart drawer's close\n    // ignoring a request when it is already closed, so the repeat stays a silent no-op rather than\n    // producing a spurious modal close.\n    type Detail = WindowEventMap['cart-close-requested']['detail'];\n    const event = new CustomEvent<Detail>('cart-close-requested', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        animate: true,\n        source: 'close-button'\n      }\n    });\n    this.dispatchEvent(event);\n  }\n\n  private onContinueShoppingButtonClick(_event: MouseEvent) {\n    // if user is on the cart page there is no cart drawer and the \"Continue Shopping\" CTA should\n    // instead route the user to the home page.\n    if (this.dataset.templateName === 'cart') {\n      location.href = location.origin;\n      return;\n    }\n\n    type Detail = WindowEventMap['cart-close-requested']['detail'];\n    const event = new CustomEvent<Detail>('cart-close-requested', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        animate: true,\n        source: 'cart-continue-shopping-button'\n      }\n    });\n    this.dispatchEvent(event);\n  }\n\n  /**\n   * Renders the first cart we receive.\n   *\n   * Watch out, onCartUpdated below does nearly all of this same show and hide work a second time,\n   * for every update after the first one. The two copies use different names for the same things,\n   * so promo here is promoGoalEl down there and installmentsMessage here is\n   * installmentPaymentMessageElement down there, and they look those elements up in different ways\n   * too. Nobody has collapsed the two, so a change to what shows and hides here almost certainly\n   * needs the same change down there.\n   */\n  private onCartUpdatedInitial(event: GlobalEventHandlersEventMap['cart-updated']) {\n    const cart = event.detail.cart;\n\n    if (cart.items.length > 0) {\n      this.shadowRoot.querySelector<HTMLElement>('#cart-contents').hidden = false;\n      this.shadowRoot.querySelector<HTMLElement>('.footer').hidden = false;\n      this.shadowRoot.querySelector('#line-items').role = 'list';\n    } else {\n      const emptyCart = this.shadowRoot.querySelector<HTMLElement>('#empty-cart');\n      emptyCart.hidden = false;\n    }\n\n    // We will load only one of these\n    const promo = this.querySelector<HTMLElement>(\n      'free-shipping-banner, promo-goal-progress, cart-gwp');\n\n    const installmentsMessage = this.querySelector('installment-payment-message');\n\n    if (this.dataset.countryCode !== 'US') {\n      if (installmentsMessage) {\n        installmentsMessage.hidden = true;\n      }\n\n      if (promo) {\n        promo.hidden = true;\n      }\n    } else {\n      if (installmentsMessage) {\n        installmentsMessage.hidden = false;\n      }\n\n      if (cart.items.some(isMBG)) {\n        installmentsMessage?.classList.add('disclaimer');\n      } else {\n        installmentsMessage?.classList.remove('disclaimer');\n      }\n\n      if (installmentsMessage) {\n        installmentsMessage.dataset.estimatedTotal = cart.total_price.toString();\n      }\n\n      if (promo) {\n        promo.hidden = false;\n      }\n    }\n\n    // If the cart has TBYB products in the cart with deferred payments, we need to show the due\n    // today messaging. We determine the total deferred payments in the cart and if this value is\n    // greater than zero we show messaging. Otherwise we hide it.\n    this.renderDeferredPaymentMessage(cart);\n\n    const estTotal = this.shadowRoot.querySelector<HTMLElement>('#estimated-total');\n    estTotal.dataset.subunits = cart.total_price.toString();\n    estTotal.dataset.currencyCode = this.dataset.currencyCode;\n    estTotal.dataset.currencySymbol = this.dataset.currencySymbol;\n\n    this.hideBackdrop();\n  }\n\n  private onCartUpdated(event: GlobalEventHandlersEventMap['cart-updated']) {\n    const cart = event.detail.cart;\n\n    // A row that fails to render should not also stop the totals and the show and hide work below\n    // from running.\n    try {\n      this.renderLineItems(cart.items);\n\n      // Only an add made through an add to cart button pulses. The free gift the cart adds on its\n      // own, the quantity dropdown, and an update synced from another tab do not.\n      if (event.detail.scenario === 'button') {\n        this.pulseLastAdded(event.detail.variants_added);\n      }\n    } catch (error) {\n      console.error(error);\n    }\n\n    // A cart arriving is the moment there is something to look at, so this is where the cart page\n    // announces its view. It does nothing in the drawer, which announces its own when it opens.\n    this.dispatchCartViewed();\n\n    const heading = this.shadowRoot.querySelector('h2');\n    const quantity = cart.item_count;\n    if (quantity === 0) {\n      heading.textContent = 'Shopping Bag';\n    } else if (quantity === 1) {\n      heading.textContent = 'Shopping Bag (1 item)';\n    } else {\n      heading.textContent = ['Shopping Bag (', quantity.toString(), ' items)'].join('');\n    }\n\n    if (event.detail.is_initial) {\n      this.onCartUpdatedInitial(event);\n      return;\n    }\n\n    // From here down is the second copy of the show and hide work described on onCartUpdatedInitial\n    // above.\n    const cartContents = this.shadowRoot.querySelector<HTMLElement>('#cart-contents');\n    const emptyCart = this.shadowRoot.querySelector<HTMLElement>('#empty-cart');\n    const footerEl = this.shadowRoot.querySelector<HTMLElement>('.footer');\n    const lineItems = this.shadowRoot.querySelector('#line-items');\n\n    if (cart.items.length > 0) {\n      if (cartContents) {\n        cartContents.hidden = false;\n      }\n\n      emptyCart.hidden = true;\n      footerEl.hidden = false;\n      lineItems.role = 'list';\n    } else {\n      emptyCart.hidden = false;\n\n      if (cartContents) {\n        cartContents.hidden = true;\n      }\n\n      footerEl.hidden = true;\n      lineItems.role = null;\n    }\n\n    // we will load only one of these, either the free shipping banner or promo goal progress\n    const promoGoalEl = this.querySelector('free-shipping-banner') ||\n      this.querySelector('promo-goal-progress') ||\n      this.querySelector('cart-gwp');\n\n    const installmentPaymentMessageElement = this.querySelector('installment-payment-message');\n\n    if (this.dataset.countryCode === 'US') {\n      if (installmentPaymentMessageElement) {\n        installmentPaymentMessageElement.hidden = false;\n      }\n\n      if (cart.items.some(isMBG)) {\n        installmentPaymentMessageElement?.classList.add('disclaimer');\n      } else {\n        installmentPaymentMessageElement?.classList.remove('disclaimer');\n      }\n\n      if (installmentPaymentMessageElement) {\n        installmentPaymentMessageElement.dataset.estimatedTotal = cart.total_price.toString();\n      }\n\n      if (cart.attributes?._aux_opt_in_v2 === 'new') {\n        const promoGoalV2El = this.querySelector('promo-goal-progress-v2');\n        if (promoGoalV2El) {\n          promoGoalV2El.dataset.show = 'true';\n        }\n      }\n\n      if (promoGoalEl) {\n        promoGoalEl.hidden = false;\n      }\n    } else {\n      if (installmentPaymentMessageElement) {\n        installmentPaymentMessageElement.hidden = true;\n      }\n\n      if (promoGoalEl) {\n        promoGoalEl.hidden = true;\n      }\n    }\n\n    const estTotal = this.shadowRoot.querySelector<HTMLElement>('#estimated-total');\n    estTotal.dataset.subunits = cart.total_price.toString();\n    estTotal.dataset.currencyCode = this.dataset.currencyCode;\n    estTotal.dataset.currencySymbol = this.dataset.currencySymbol;\n\n    // If the cart has TBYB products in the cart with deferred payments, we need to show the due\n    // today messaging. We determine the total deferred payments in the cart and if this value is\n    // greater than zero we show messaging. Otherwise we hide it.\n    this.renderDeferredPaymentMessage(cart);\n  }\n\n  /**\n   * Announces that the shopper is looking at their cart, on the cart page only. The drawer has an\n   * obvious moment for this, the point where it opens, and the cart page has nothing like it. The\n   * cart arrives after the page does, so we go with the first one we receive, which is the earliest\n   * point where there is a cart to look at.\n   *\n   * Only the first. Every update after that is the shopper changing something in a cart they are\n   * already looking at, which is not a new view.\n   *\n   * The flag holding that is per instance and not per page load, so a second instance announces a\n   * second view. Watch out for that if this element ever ends up on the page more than once.\n   */\n  private dispatchCartViewed() {\n    if (this.dataset.templateName !== 'cart') {\n      return;\n    }\n\n    if (this.cartViewedDispatched) {\n      return;\n    }\n\n    this.cartViewedDispatched = true;\n\n    type CartViewedEvent = GlobalEventHandlersEventMap['cart-viewed'];\n    const event = new CustomEvent<CartViewedEvent['detail']>('cart-viewed', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        surface: 'page'\n      }\n    });\n    this.dispatchEvent(event);\n  }\n\n  private renderDeferredPaymentMessage(cart: Cart) {\n    let deferredAmount = 0;\n    for (const item of cart.items) {\n      if (item.selling_plan_allocation?.selling_plan?.recurring_deliveries === false &&\n        item.selling_plan_allocation?.selling_plan?.name !== 'Pre-Order') {\n        deferredAmount += item.selling_plan_allocation.price;\n\n        // If a discount was applied to the item, we need to subtract that from the deferred amount\n        // because the TryNow app is not aware of any discounts and therefore the\n        // selling_plan_allocation.price property reflects the price before discount.\n        if (item.line_level_total_discount > 0) {\n          deferredAmount -= item.line_level_total_discount;\n        }\n      }\n    }\n\n    if (deferredAmount > 0) {\n      const dueTodayEl = this.shadowRoot.querySelector<HTMLElement>('#due-today');\n      dueTodayEl.dataset.subunits = (cart.total_price - deferredAmount).toString();\n      dueTodayEl.dataset.currencyCode = this.dataset.currencyCode;\n      dueTodayEl.dataset.currencySymbol = this.dataset.currencySymbol;\n\n      const dueTodayContainer = this.shadowRoot.querySelector<HTMLElement>('#tbyb-due-today');\n      dueTodayContainer.hidden = false;\n    } else {\n      const dueTodayContainer = this.shadowRoot.querySelector<HTMLElement>('#tbyb-due-today');\n      dueTodayContainer.hidden = true;\n    }\n  }\n\n  /**\n   * Renders the cart-line-item elements into the line items slot.\n   *\n   * This does not do partial renders specific to the type of cart update. This always fully re-\n   * renders after an update. Any cart update that is in response to some action (e.g. add item,\n   * remove item) can contain any kind of state change. Each action such as add to cart obtains a\n   * new cart state where multiple things, things not directly related to the items involved, may\n   * have changed. For example, removing a line item can change the key property of other items not\n   * removed. In addition, there is no pressing need to partially render because the number of items\n   * is generally small whereas the amount of engineering effort to properly partially render is\n   * huge.\n   *\n   * Prior implementations of this logic were horrifically flawed. Please never reintroduce support\n   * for, or the concept of, partial rendering.\n   *\n   * The rows share the light DOM with everything else the liquid slots into this element, such as\n   * the promo banner and the upsells, so only the rows in the line items slot are removed.\n   */\n  private renderLineItems(parents: Item[]) {\n    // Pre-compute parent keys to enable single-pass rendering. This avoids nested loops over the\n    // full cart for each item when identifying which items have children.\n    const uniqueParentKeys = new Set();\n    for (const item of parents) {\n      if (item.parent_relationship?.parent_key) {\n        uniqueParentKeys.add(item.parent_relationship.parent_key);\n      }\n    }\n\n    const giftWithPurchaseProduct = parents.find(\n      item => item.properties._gift_with_purchase === 'true');\n\n    const fragment = document.createDocumentFragment();\n    for (const item of parents) {\n      if (item.parent_relationship?.parent_key) {\n        // Child items (e.g. GWP) are rendered nested within their parent, skip in top-level loop\n        continue;\n      }\n\n      // Skip rendering the gift with purchase product in the top-level loop; it will be rendered\n      // separately.\n      if (giftWithPurchaseProduct?.key !== item.key) {\n        const element = this.createItemElement(item);\n        element.slot = 'line-items';\n        if (uniqueParentKeys.has(item.key)) {\n          element.dataset.isParent = 'true';\n          this.appendChildItems(element, item.key, parents);\n        }\n\n        fragment.appendChild(element);\n      }\n    }\n\n    if (giftWithPurchaseProduct) {\n      const element = this.createItemElement(giftWithPurchaseProduct);\n      element.slot = 'line-items';\n      // If cart updates changed the gift with purchase eligibility, a final_price value above zero\n      // is that signal. In this case, we do not mark the item as a gift with purchase in case the\n      // automated removal request fails. Otherwise a customer would have no means to remove the\n      // line item and it would reflect an incorrect price.\n      if (giftWithPurchaseProduct.properties._gift_with_purchase === 'true' &&\n        giftWithPurchaseProduct.final_price === 0) {\n        element.dataset.isGiftWithPurchase = 'true';\n\n        // The offer view holds the gifts in stock, and with only one there is nothing to change to.\n        const giftCount = document.querySelectorAll('offer-view gift-with-purchase-product').length;\n        element.dataset.giftChangeable = giftCount > 1 ? 'true' : 'false';\n\n        // The promos app's cart transform zeroes the line's price outright rather than discounting\n        // it, so every price on the line reads 0. The gift's regular price only survives in the\n        // property saved when the gift was added. Using it as the line price makes the line show\n        // $0 with that price struck through, the same way a discounted line does.\n        const regularPrice = parseInt(giftWithPurchaseProduct.properties._gift_with_purchase_price,\n          10);\n        if (regularPrice > 0) {\n          element.dataset.linePrice = (regularPrice * giftWithPurchaseProduct.quantity).toString();\n        }\n      }\n\n      fragment.appendChild(element);\n    }\n\n    for (const row of this.querySelectorAll(':scope > [slot=\"line-items\"]')) {\n      row.remove();\n    }\n\n    this.appendChild(fragment);\n  }\n\n  /**\n   * Plays the pulse on the row of the last variant added, whether the add made a new row or raised\n   * the quantity of an existing one. Every render replaces all the rows, so the next render drops\n   * the attribute and the pulse never replays.\n   */\n  private pulseLastAdded(\n    variants: GlobalEventHandlersEventMap['cart-updated']['detail']['variants_added']) {\n    const variant = variants[variants.length - 1];\n    if (!variant) {\n      return;\n    }\n\n    const lineItem = this.querySelector<HTMLElement>(\n      `cart-line-item[data-variant-id=\"${variant.variant_id}\"]`);\n    if (lineItem) {\n      lineItem.dataset.pulse = 'true';\n    }\n  }\n\n  private appendChildItems(parentElement: HTMLElement, parentKey: string, items: Item[]) {\n    for (const item of items) {\n      if (item.parent_relationship?.parent_key === parentKey) {\n        const childElement = this.createItemElement(item);\n        childElement.classList.add('child-item');\n        parentElement.appendChild(childElement);\n      }\n    }\n  }\n\n  private createItemElement(item: Item) {\n    const lineItemEl = document.createElement('cart-line-item');\n    lineItemEl.role = 'listitem';\n    lineItemEl.dataset.countryCode = this.dataset.countryCode;\n    lineItemEl.dataset.currencyCode = this.dataset.currencyCode;\n    lineItemEl.dataset.currencySymbol = this.dataset.currencySymbol;\n    lineItemEl.dataset.imageSrc = item.image;\n    lineItemEl.dataset.imageAlt = item.title;\n    lineItemEl.dataset.isAvailable = 'true';\n    lineItemEl.dataset.lineItemId = item.key;\n    lineItemEl.dataset.variantId = item.variant_id.toString();\n    lineItemEl.dataset.title = item.title;\n    lineItemEl.dataset.quantity = item.quantity.toString();\n    lineItemEl.dataset.productType = item.product_type;\n    lineItemEl.dataset.productUrl = item.url;\n    lineItemEl.dataset.price = item.final_price.toString();\n    lineItemEl.dataset.linePrice = item.original_line_price.toString();\n    lineItemEl.dataset.linePriceAfterDiscount = item.final_line_price.toString();\n\n    if (item.parent_relationship?.parent_key) {\n      lineItemEl.dataset.parentId = item.parent_relationship.parent_key;\n    }\n\n    if (item.properties._is_upsell === 'true') {\n      lineItemEl.dataset.isUpsell = 'true';\n      lineItemEl.dataset.maxQuantity = '1';\n    }\n\n    if (item.handle) {\n      lineItemEl.dataset.productHandle = item.handle;\n    }\n\n    if (item.product_id) {\n      lineItemEl.dataset.productId = item.product_id.toString();\n    }\n\n    // Child items (e.g. GWP) are free promotional products, so subscription options don't apply\n    if (item.selling_plan_allocation && !item.parent_relationship?.parent_key) {\n      lineItemEl.dataset.sellingPlanId = item.selling_plan_allocation.selling_plan.id.toString();\n      lineItemEl.dataset.sellingPlanFrequency =\n        item.selling_plan_allocation.selling_plan.name.replace('Delivery every ', '');\n      if (item.properties['_selling_plan_options']) {\n        const select = document.createElement('select');\n        select.classList.add('selling-plan-options');\n        select.slot = 'selling-plan-options';\n\n        const options = JSON.parse(item.properties['_selling_plan_options']);\n\n        // If the parsed JSON does not conform to what is expected, we simply do not render and\n        // inject the selling plan options input\n        if (isSellingPlanOptionsArray(options)) {\n          for (const option of options) {\n            const optionEl = document.createElement('option');\n            optionEl.value = option.id.toString();\n            optionEl.textContent = option.name;\n            if (option.id === item.selling_plan_allocation.selling_plan.id) {\n              optionEl.selected = true;\n            }\n\n            select.appendChild(optionEl);\n          }\n\n          lineItemEl.appendChild(select);\n        }\n      }\n    } else if (item.properties['_selling_plan_options'] && !item.parent_relationship?.parent_key) {\n      // If not a subscription but selling plans are available - allow upgrading to a subscription\n      // using the first available selling plan. Child items are excluded since these kind of\n      // interactions are bound to their parent line item.\n      let sellingPlanId: number;\n      try {\n        const sellingPlans = <{ id: number; name: string; }[]>JSON.parse(\n          item.properties['_selling_plan_options']);\n        sellingPlanId = sellingPlans[0].id;\n      } catch (error) {\n        console.warn('Invalid _selling_plan_options', item.properties['_selling_plan_options'],\n          error);\n      }\n\n      if (sellingPlanId) {\n        const upgradeButton = document.createElement('button');\n        upgradeButton.textContent = 'Subscribe & Save';\n        // This is the plan to upgrade to, so it means something different than the line item\n        // element's own data-selling-plan-id, which is the plan the item is already on. It lives on\n        // the button rather than on the element so that offering a choice of plans later is just\n        // more buttons. The line item element reads it back off the button on click.\n        upgradeButton.dataset.sellingPlanId = sellingPlanId.toString();\n        upgradeButton.slot = 'selling-plan-options';\n        upgradeButton.classList.add('upgrade-to-subscription');\n        upgradeButton.dataset.clickTrack = 'Cart subscription upgrade';\n        lineItemEl.appendChild(upgradeButton);\n      }\n    }\n\n    const messages = generateMessages(item, this.dataset.currencySymbol);\n\n    // Child items inherit messaging context from parent, suppress duplicates to avoid UI clutter\n    if (messages.length > 0 && !item.parent_relationship?.parent_key) {\n      const ulTemplate = this.shadowRoot.querySelector<HTMLTemplateElement>('#message-list');\n      const ulClone = <HTMLElement>ulTemplate.content.cloneNode(true);\n      const ul = ulClone.querySelector('ul');\n\n      for (const message of messages) {\n        const itemTemplate = this.shadowRoot.querySelector<HTMLTemplateElement>('#message');\n        const itemClone = <HTMLElement>itemTemplate.content.cloneNode(true);\n        const itemEl = itemClone.querySelector('li');\n        itemEl.classList.add(message.type);\n\n        const itemMessage = itemEl.querySelector<HTMLSpanElement>('.message');\n        itemMessage.innerHTML = message.message;\n\n        ul.appendChild(itemEl);\n      }\n\n      lineItemEl.appendChild(ul);\n    }\n\n    lineItemEl.dataset.update = Date.now().toString();\n\n    return lineItemEl;\n  }\n\n  /**\n   * We only start listening for this event from within the connected callback so we know the DOM is\n   * ready and the backdrop element exists.\n   */\n  private onCartTransactionStarted(_event: WindowEventMap['cart-transaction-started']) {\n    this.showBackdrop();\n  }\n\n  /**\n   * We only start listening for this event from within the connected callback so we know the DOM is\n   * ready and the backdrop element exists.\n   */\n  private onCartTransactionCompleted(_event: WindowEventMap['cart-transaction-completed']) {\n    this.hideBackdrop();\n  }\n\n  /**\n   * The cart has its own loading veil: a translucent overlay with a spinner that covers the cart's\n   * contents while a cart transaction (add, remove, quantity change, discount) is in flight. Its\n   * job is to signal that work is underway and to lightly block the contents while the request is\n   * pending, alongside the disabling of individual controls. This is a different kind of backdrop\n   * than the page layers that dim everything behind a modal or drawer: its scope is only this\n   * element's own content area, and clicking it does not dismiss anything.\n   */\n  private hideBackdrop() {\n    // The loading veil is rendered as part of this element's template, so it lives in the shadow\n    // tree and must be looked up through the shadow root. A light-DOM lookup silently finds nothing\n    // and the toggle below quietly does nothing.\n    const backdrop = this.shadowRoot.querySelector<HTMLElement>('#cart-backdrop');\n    if (backdrop) {\n      backdrop.hidden = true;\n    }\n  }\n\n  private showBackdrop() {\n    // The loading veil is rendered as part of this element's template, so it lives in the shadow\n    // tree and must be looked up through the shadow root. A light-DOM lookup silently finds nothing\n    // and the toggle below quietly does nothing.\n    const backdrop = this.shadowRoot.querySelector<HTMLElement>('#cart-backdrop');\n    if (backdrop) {\n      backdrop.hidden = false;\n    }\n  }\n\n  private onShowDiscountInputClicked(_event: MouseEvent) {\n    const discountInput = this.shadowRoot.querySelector<HTMLSlotElement>(\n      'slot[name=\"discount-input\"]');\n    discountInput.hidden = false;\n\n    // hide the button after clicking it\n    const showDiscountInput = this.shadowRoot.querySelector<HTMLElement>('.show-discount-input');\n    showDiscountInput.hidden = true;\n  }\n}\n\nfunction isMBG(item: Item) {\n  return item.properties?.['_is_mbg'] === 'true';\n}\n\ninterface SellingPlanOptions {\n  id: number;\n  name: string;\n}\n\ninterface LineItemMessage {\n  message: string;\n  type: 'discount' | 'gift' | 'information' | 'warning';\n}\n\n/**\n * Builds the messages to show for a line item.\n */\nfunction generateMessages(item: Item, currencySymbol: string) {\n  const messages: LineItemMessage[] = [];\n\n  // A gift line only counts as free once the cart transform has zeroed its price. That is the same\n  // test the cart uses to mark the line item a gift with purchase.\n  if (item.properties._gift_with_purchase === 'true' && item.final_price === 0) {\n    messages.push({ type: 'gift', message: 'Free gift' });\n  }\n\n  // Render the discounts allocated to the line. It is possible for allocations to allocate 0\n  // dollars of a discount application to a line. This is pretty common with discounts such as BXGY\n  // discounts that do not reduce the price of the items used in the BX part of the discount, only\n  // the GY part. The discounts are allocated to the line as $0 off, as the lines participate in the\n  // discount but are not discounted themselves. We only want to render reductions.\n\n  for (const allocation of item.line_level_discount_allocations) {\n    if (allocation.discount_application.title && allocation.amount > 0) {\n      // The allocation amount is in subunits (e.g. cents in USD), so we divide by 100. We use\n      // toFixed to ensure we show 2 significant digits\n      const value = (allocation.amount / 100).toFixed(2);\n\n      messages.push({ type: 'discount',\n        message: `${allocation.discount_application.title} (${currencySymbol}${value})` });\n    } else if (allocation.amount === 0 && /bogo/i.test(allocation.discount_application.title)) {\n      const code = allocation.discount_application.title;\n\n      let message: string;\n      if (item.quantity === 1) {\n        message = `This product qualifies another product for ${code}`;\n      } else {\n        message = `This product qualifies ${item.quantity} other products for ${code}`;\n      }\n\n      messages.push({ type: 'information', message });\n    }\n  }\n\n  if (item.properties._contains_alcohol === 'true') {\n    messages.push({ type: 'information',\n      message: 'This item may only ship to the contiguous U.S and Canada.' });\n  }\n\n  if (item.properties._compatibility === 'soleil') {\n    messages.push({ type: 'warning',\n      message: 'Only compatible with <a href=\"/products/soleil\">Soleil</a>' });\n  }\n\n  if (item.properties._compatibility === 'max-high-volume') {\n    messages.push({ type: 'warning',\n      message: 'TOOL SOLD SEPARATELY. Requires <a href=\"/products/multi-volume-series\">' +\n        'Multi-Volume Series</a> brush dryer handle to work' });\n  }\n\n  if (item.properties._final_sale === 'true') {\n    messages.push({ type: 'warning', message: 'This item is final sale, not eligible for return' });\n  }\n\n  if (item.properties._backorder_message) {\n    messages.push({ type: 'information', message: item.properties._backorder_message });\n  }\n\n  if (item.properties._is_tbyb) {\n    messages.push({ type: 'information', message: item.properties._is_tbyb });\n  }\n\n  return messages;\n}\n\nfunction isSellingPlanOptionsArray(value: any): value is SellingPlanOptions[] {\n  return Array.isArray(value) && value.filter(item => item.id && item.name).length === value.length;\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'cart-element': CartElement;\n  }\n}\n\nif (!customElements.get('cart-element')) {\n  customElements.define('cart-element', CartElement);\n}\n","import type {} from '@integrabeauty/shopify-browser-types';\n\n/**\n * Represents the value of a cart attribute. Ordinarily values are strings. However, Shopify also\n * supports storing nested objects as values.\n */\nexport type AttributeValue = Record<string, string> | string;\n\n/**\n * The type of the value of `Cart['attributes']`\n */\nexport type Attributes = Record<string, AttributeValue>;\n\n/**\n * This is the stored cart, which is what a get cart answers with. A mutation answers with a\n * superset, so see the response body types alongside each mutation for the extra properties those\n * carry.\n *\n * @todo selling plan properties\n * @todo endpoint request body response body types\n *\n * @see https://shopify.dev/docs/api/ajax/reference/cart\n */\nexport interface Cart {\n  /**\n   * An object where the key is the attribute name and the value is the attribute value.\n   */\n  attributes: Attributes;\n\n  /**\n   * A discount application represents a type of discount.\n   *\n   * When a discount is applied to one or more line items, it is said to be _allocated_ to the line\n   * items. That is why line level discount allocation objects have a nested application object that\n   * will correlate with one of the applications in this array.\n   *\n   * When a discount is applied to the whole cart, it will only be present in this array because it\n   * is not allocated to particular lines.\n   */\n  cart_level_discount_applications: DiscountApplication[];\n\n  /**\n   * The ISO currency code for the cart.\n   *\n   * @example \"USD\"\n   */\n  currency: string;\n\n  /**\n   * Discount codes that have been applied to the cart.\n   *\n   * Not all codes are applicable. It is possible for a code to be applied but inapplicable.\n   *\n   * This does not provide information about the amount of each discount. Refer to the cart level\n   * discount applications or line level discount allocations for that information.\n   *\n   * This array will include shipping discounts. A shipping discount will be present in the array if\n   * applied but in the inapplicable state while in the cart. The discount will appear as applicable\n   * at checkout automatically.\n   */\n  discount_codes: DiscountCode[];\n\n  /**\n   * Total quantity of items in a cart\n   */\n  item_count: number;\n\n  /**\n   * The cart's line items.\n   *\n   * A cart will always have a defined items array, even if the cart is empty.\n   */\n  items: Item[];\n\n  items_subtotal_price: number;\n\n  /**\n   * An optional textual note that may be applied to the cart. If a cart becomes a checkout, and\n   * then the checkout becomes an order, the cart note will carry over to the order note.\n   *\n   * An order's note, and an order's note attributes, are two completely separate concepts.\n   */\n  note: string;\n\n  original_total_price: number;\n\n  /**\n   * This is true when at least one item in the cart requires shipping.\n   *\n   * This is false when no items in the cart require shipping.\n   *\n   * Shopify supports digital-only carts. These are carts that when purchased do not require the\n   * creation of a fulfillment. For example, a cart that consists only of a digital gift card.\n   */\n  requires_shipping: boolean;\n\n  /**\n   * This property is defined when there are cart mutations that involve the use of liquid-rendered\n   * sections.\n   *\n   * @see https://shopify.dev/docs/api/ajax/reference/cart#request-sections\n   */\n  sections: Record<string, string>;\n\n  /**\n   * This is the publicly visible cart id.\n   *\n   * If a checkout is created from this cart, the checkout's cart token will be equal to this value.\n   *\n   * Never write logic that parses this value. The format can change at any time, and has changed\n   * several times in the past.\n   *\n   * The value may contain url-like values, e.g. \"abc?key=123\"\n   */\n  token: string;\n\n  /**\n   * The total reduction in price in currency subunits (e.g. in cents for USD).\n   *\n   * @example 5010\n   */\n  total_discount: number;\n\n  /**\n   * Total price in currency subunits. For example, if the currency is USD, this is in cents.\n   *\n   * @example 1050\n   */\n  total_price: number;\n\n  /**\n   * The total weight of the cart (in grams?). This is a simple sum of the weight of the items of\n   * the cart (unit weight * quantity).\n   *\n   * @example 123.456\n   */\n  total_weight: number;\n}\n\n/**\n * Represents a discount that has been applied to an item.\n */\nexport interface Discount {\n  amount: number;\n  title: string;\n}\n\n/**\n * Represents a discount application that has been allocated to a line item.\n */\nexport interface DiscountAllocation {\n  /**\n   * The amount of the application that was allocated to a particular line item.\n   *\n   * When a discount application is allocated to several line items, this is a partial value of the\n   * full discount application amount that is allocated to this particular line. When the discount\n   * application is only allocated to a single line, this value will be equal to the total value of\n   * the discount application.\n   *\n   * For example, let's say we have cart with 2 line items and 10 dollar off discount application.\n   * The total value of the discount application is 10 dollars. Let's say the discount is allocated\n   * to both lines. The value of each allocation is 5 dollars. For each line this amount will\n   * contain the value 500. Within the nested application object, the application's total amount\n   * will be 1000. This is because each nested application object is a copy of the one application\n   * that happens to have multiple allocations (the discount is applied to multiple lines).\n   *\n   * The amount is in currency subunits. For example, for USD, this is the value of cents. For\n   * example, a discount of $2.50 is 250.\n   *\n   * @example 1050\n   */\n  amount: number;\n\n  /**\n   * The corresponding discount application that was allocated to a particular line item.\n   */\n  discount_application: DiscountApplication;\n}\n\n/**\n * A discount application represents a discount of some kind. There are automatically applied\n * discount applications, discount code applications, and script applied discount applications.\n */\nexport interface DiscountApplication {\n  allocation_method: string;\n  created_at: string;\n  description: string;\n  key: string;\n  target_selection: string;\n  target_type: string;\n  title: string;\n  total_allocated_amount: number;\n  type: string;\n  value: string;\n  value_type: string;\n}\n\n/**\n * The details of an image resource, such as the hero image for a product.\n */\nexport interface Image {\n  alt: string;\n  aspect_ratio: number;\n  height: number;\n  url: string;\n  width: number;\n}\n\n/**\n * Represents a line item in a cart.\n */\nexport interface Item {\n  /**\n   * The price of the line item after discounts\n   */\n  discounted_price: number;\n\n  /**\n   * Discount codes applied to the line item.\n   *\n   * @deprecated\n   */\n  discounts: Discount[];\n\n  /**\n   * The main image for the product\n   */\n  featured_image: Image;\n\n  /**\n   * The combined price, in the currency's subunit, of all of the items in the line item. This\n   * includes any line-level discounts.\n   *\n   * The value is equal to line_item.final_price multiplied by line_item.quantity. It's output in\n   * the customer's local (presentment) currency.\n   *\n   * For currencies without subunits, such as JPY and KRW, tenths and hundredths of a unit are\n   * appended. For example, 1000 Japanese yen is output as 100000.\n   */\n  final_line_price: number;\n\n  /**\n   * The price of the line item in the currency's subunit. This includes any line-level discounts.\n   *\n   * The value is output in the customer's local (presentment) currency.\n   *\n   * For currencies without subunits, such as JPY and KRW, tenths and hundredths of a unit are\n   * appended. For example, 1000 Japanese yen is output as 100000.\n   */\n  final_price: number;\n\n  /**\n   * Returns true if the product associated with the line item is a gift card. Returns false if not.\n   */\n  gift_card: boolean;\n\n  /**\n   * The weight of the line item in the store's default weight unit.\n   */\n  grams: number;\n\n  handle: string;\n\n  has_components: boolean;\n\n  /**\n   * The ID of the line item. The ID differs depending on the context. The following table outlines\n   * the possible contexts and their associated values:\n   *\n   * Context Value\n   *\n   * * cart.items The ID of the line item's variant. This ID isn't unique, and can be shared by\n   *   multiple items with the same variant.\n   * * checkout.line_items A temporary unique hash generated for the checkout.\n   * * order.line_items A unique integer ID.\n   */\n  id: number;\n\n  image: string;\n\n  /**\n   * Added in conjunction with parent_relationship to support determining whether a line item can be\n   * removed or have its quantity updated when the line item is a child of another line item, e.g.\n   * part of a bundle or value set or promotional gift.\n   */\n  instructions: {\n    can_remove: boolean;\n    can_update_quantity: boolean;\n  };\n\n  key: string;\n\n  /**\n   * The discount allocations that apply to the line item.\n   */\n  line_level_discount_allocations: DiscountAllocation[];\n\n  /**\n   * The total amount of any discounts applied to the line item in the currency's subunit.\n   *\n   * The value is output in the customer's local (presentment) currency.\n   *\n   * For currencies without subunits, such as JPY and KRW, tenths and hundredths of a unit are\n   * appended. For example, 1000 Japanese yen is output as 100000.\n   */\n  line_level_total_discount: number;\n\n  /**\n   * @deprecated https://shopify.dev/docs/api/liquid/objects/line_item\n   */\n  line_price: number;\n\n  /**\n   * The name and value pairs for each option of the variant associated with the line item.\n   *\n   * The array is never empty because variants with no options still have a default option. Because\n   * of this, you should use line_item.product.has_only_default_variant to check whether there's any\n   * information to output.\n   */\n  options_with_values: Option[];\n\n  /**\n   * The combined price of all of the items in a line item in the currency's subunit, before any\n   * discounts have been applied.\n   *\n   * The value is equal to line_item.original_price multiplied by line_item.quantity. It's output in\n   * the customer's local (presentment) currency.\n   *\n   * For currencies without subunits, such as JPY and KRW, tenths and hundredths of a unit are\n   * appended. For example, 1000 Japanese yen is output as 100000.\n   */\n  original_line_price: number;\n\n  /**\n   * The price of the line item in the currency's subunit, before discounts have been applied.\n   *\n   * The value is output in the customer's local (presentment) currency.\n   *\n   * For currencies without subunits, such as JPY and KRW, tenths and hundredths of a unit are\n   * appended. For example, 1000 Japanese yen is output as 100000.\n   */\n  original_price: number;\n\n  /**\n   * The parent relationship of the line item. This is only present if the line item is a child of\n   * another line item. For example, when the line item is part of a bundle or value set or\n   * promotional gift.\n   *\n   * The parent relationship must be established by adding the \"parent_id\" property to the add to\n   * bag request.\n   *\n   * @see https://shopify.dev/docs/apps/build/product-merchandising/nested-cart-lines/create-nested-cart-lines#cart-ajax-api\n   */\n  parent_relationship: {\n    parent_key: string;\n  };\n\n  presentment_price: number;\n\n  /**\n   * @deprecated https://shopify.dev/docs/api/liquid/objects/line_item\n   */\n  price: number;\n\n  product_description: string;\n\n  product_has_only_default_variant: boolean;\n\n  /**\n   * The ID of the line item's product.\n   */\n  product_id: number;\n\n  product_title: string;\n\n  product_type: string;\n\n  properties: Record<string, string>;\n  quantity: number;\n\n  quantity_rule: QuantityRule;\n\n  requires_shipping: boolean;\n\n  /**\n   * The selling plan allocation of the line item.\n   */\n  selling_plan_allocation: SellingPlanAllocation;\n\n  /**\n   * The SKU of the variant in the line item.\n   *\n   * Stock Keeping Unit.\n   */\n  sku: string;\n\n  successfully_fulfilled_quantity: number;\n\n  taxable: boolean;\n\n  /**\n   * The title of the line item. The title is a combination of line_item.product.title and\n   * line_item.variant.title, separated by a hyphen.\n   *\n   * In most contexts, the line item title appears in the customer's preferred language. However, in\n   * the context of an order, the line item title appears in the language that the customer checked\n   * out in. The title can receive an override value from the Cart Transform API. Overrides take\n   * precedence over translations.\n   *\n   * #### Line item title history\n   *\n   * When referencing line item, product, and variant titles in the context of an order, the\n   * following changes might result in a different output than you expect:\n   * * A product or variant being deleted\n   * * A product or variant title being edited\n   *\n   * When line_item.title is referenced for an order, the line item title at the time of the order\n   * is returned. However, when line_item.product.title and line_item.variant.title are referenced,\n   * the current value for each title is returned.\n   */\n  title: string;\n\n  /**\n   * @deprecated https://shopify.dev/docs/api/liquid/objects/line_item\n   */\n  total_discount: number;\n\n  unit_price_measurement: {\n    measured_type: string;\n    quantity_unit: string;\n    quantity_value: string;\n    reference_unit: string;\n    reference_value: number;\n  };\n\n  untranslated_product_title: string;\n\n  untranslated_variant_title: string;\n\n  /**\n   * The relative URL of the variant associated with the line item.\n   */\n  url: string;\n\n  /**\n   * The ID of the line item's variant.\n   */\n  variant_id: number;\n\n  variant_options: string[];\n\n  variant_title: string;\n\n  /**\n   * The vendor of the variant associated with the line item.\n   */\n  vendor: string;\n}\n\nexport interface SellingPlanAllocation {\n  compare_at_price: number;\n\n  per_delivery_price: number;\n\n  /**\n   * @example 900\n   */\n  price: number;\n\n  price_adjustments: SellingPlanAllocationPriceAdjustment[];\n  selling_plan: SellingPlan;\n}\n\nexport interface SellingPlanAllocationPriceAdjustment {\n  /**\n   * @example 1\n   */\n  position: number;\n\n  /**\n   * @example 900\n   */\n  price: number;\n}\n\nexport interface SellingPlan {\n  description: unknown;\n\n  fixed_selling_plan: boolean;\n\n  id: number;\n  name: string;\n\n  options: SellingPlanOption[];\n\n  price_adjustments: SellingPlanPriceAdjustment[];\n\n  recurring_deliveries: boolean;\n}\n\nexport interface SellingPlanOption {\n  name: string;\n  position: number;\n  value: string;\n}\n\nexport interface SellingPlanPriceAdjustment {\n  order_count: unknown;\n  position: number;\n  value: number;\n  value_type: string;\n}\n\nexport interface Option {\n  name: string;\n  value: string;\n}\n\nexport interface QuantityRule {\n  increment: number;\n  max: number;\n  min: number;\n}\n\nexport interface DiscountCode {\n  /**\n   * Whether the discount is currently applicable. It is possible to apply a discount code to a cart\n   * but have it not yet be applicable. When not applicable, there will not be any associated\n   * discount applications or allocations. The moment someone performs an action that makes the\n   * discount code applicable, Shopify will update the property to true.\n   */\n  applicable: boolean;\n\n  /**\n   * The discount code string.\n   *\n   * This is NOT guaranteed to be uppercase. Discount codes entered at checkout are captured in\n   * whatever case was used during input.\n   *\n   * @example \"MONEY1234\"\n   * @example \"Abc123def\"\n   */\n  code: string;\n}\n\n/**\n * Returns the request id header from the ajax cart api response. Capturing this value is important\n * when helping Shopify support investigate issues.\n *\n * This does not error. If there is a problem, this returns null or undefined.\n */\nfunction getRequestId(response: Response) {\n  try {\n    return response.headers.get('X-Request-Id');\n  } catch (error) {\n    // ignore\n  }\n}\n\n/**\n * Returns whether the given value is shaped like a fetch Response.\n *\n * The return type of fetch is a contract, not a verified fact. A third party that overrides the\n * global fetch can resolve it to null, undefined, or some unrelated value, in which case the very\n * next property access throws an opaque error with no indication that a bad override was the cause.\n * This checks structurally for the members this module reads rather than with an instanceof, so a\n * conforming fetch polyfill that returns its own Response class still passes while a missing or\n * malformed value is trapped at the point it is received.\n *\n * This type check does not technically prove that the value is a genuine Response object, only that\n * it has the expected shape. We are intentionally lying with types here, so that we can leverage\n * the type inference to support the subset of use cases this module actually relies on. More over,\n * this lying is ok, as the call to `response = await fetch` is also lying because it overly trusts\n * that fetch returns value of type Response. We are going to keep trying to \"make fetch happen\"\n * regardless.\n *\n * ## War Story\n *\n * As much as it seems like this is an insane and irrationally paranoid check, you would be\n * surprised. For example, in the wild, we observed a third party vendor that hooked global fetch,\n * had an error in their hook, and simultaneously, another vendor's third party polyfill that only\n * defined fetch without also defining Response, leading to even more obscure cases when older\n * browsers were involved. Then, to make matters worse, a fix was implemented leveraging the clean\n * global fetch using an iframe content window hack to work around the inability to cleanly capture\n * the native fetch before it was erroneously hooked, and this broke certain expressions that use\n * instanceof because instanceof has unintuitive behavior when checking instanceof of a value from a\n * different global context, e.g. from an iframe. A web of lies!\n */\nfunction isResponse(value: any): value is Response {\n  if (value === null || typeof value !== 'object') {\n    return false;\n  }\n\n  if (typeof value.ok !== 'boolean') {\n    return false;\n  }\n\n  if (typeof value.status !== 'number') {\n    return false;\n  }\n\n  if (typeof value.text !== 'function') {\n    return false;\n  }\n\n  if (typeof value.json !== 'function') {\n    return false;\n  }\n\n  return true;\n}\n\n/**\n * Attaches a single piece of context onto an error before it is thrown.\n *\n * The target may be a value caught upstream rather than one constructed here, and such a value can\n * carry a non-writable property, such as a DOMException whose code is a read-only accessor, or be\n * frozen outright, in which case assigning to it throws. Decorating an error must never itself\n * fail, so a rejected assignment is swallowed. This context is best effort metadata, so the\n * property is skipped when it cannot be set, leaving the error otherwise intact to be thrown and\n * handled by the caller.\n */\nfunction decorateError(error: any, key: string, value: unknown) {\n  try {\n    error[key] = value;\n  } catch (error) {\n    // The target rejects this property. Skip it rather than letting decoration fail.\n  }\n}\n\n/**\n * Returns an absolute url for the given path.\n *\n * It would be nice to just use a relative url in fetch. However, we see real world errors about\n * requests including credentials. Visitors might have buggy extensions inserting document.baseURI\n * or they are browsing the site in some weird way. To work around this, we build up a full url that\n * omits the credentials.\n *\n * Unfortunately, specifying the base url means we have to also solve the problem of determining it.\n * There is currently a desire to avoid location.origin due to browser support. we do not want to\n * touch the document base uri. Use host over hostname since that includes port.\n */\nfunction resolveUrl(path: string) {\n  return new URL(path, `${location.protocol}//${location.host}`);\n}\n\n/**\n * An item to add to the cart\n */\nexport interface AddItem {\n  /**\n   * Variant id\n   *\n   * Shopify says that it supports variant ids as numbers, but we have witnessed strange errors when\n   * using numbers so we impose an artificial limitation on the type here and require strings.\n   */\n  id: string;\n\n  /**\n   * The id of the line item to add the item as a child of. This is used for adding items as\n   * children of value sets and bundles. If not specified, the item is added as a top level line\n   * item.\n   *\n   * @see https://shopify.dev/docs/apps/build/product-merchandising/nested-cart-lines/create-nested-cart-lines#cart-ajax-api\n   */\n  parent_id?: number | string;\n\n  /**\n   * Line item properties to include.\n   *\n   * If the variant does not already exist in the cart, then the logic is straightforward. This will\n   * result in a new line item with these properties.\n   *\n   * If the variant exists in the cart, then the logic is convoluted. If the specified properties\n   * are identical to the existing properties, then the quantity of the existing line item\n   * increments. If the specified properties are different, then a new line item is created with\n   * these properties, resulting in the same variant being present in multiple line items.\n   */\n  properties?: Attributes;\n\n  /**\n   * The amount of the variant to add.\n   *\n   * @see https://shopify.dev/docs/api/ajax/reference/cart#post-locale-cart-add-js\n   */\n  quantity: number;\n\n  selling_plan?: number | string;\n}\n\n/**\n * The response body of a cart add.\n *\n * This one is not a cart at all, unlike the response body of a change or an update. It carries only\n * the items the request asked for, so there is no token, no totals, and no view of the rest of the\n * cart. See the notes on the add function for why the body is not worth reading.\n *\n * None of the functions here parse this body, because a caller that wants the resulting state\n * performs its own get cart afterward. So this type exists to describe what the endpoint answers\n * with rather than to annotate a return value.\n */\nexport interface CartAddResponseBody {\n  /**\n   * The line items the add produced, one per item in the request.\n   *\n   * These look like ordinary cart line items and are typed as such, though a few of the rarer line\n   * item properties have not been seen on one, so treat the shape as the usual line item rather\n   * than a guaranteed match.\n   */\n  items: Item[];\n}\n\n/**\n * Adds items to the cart. At least one item should be specified.\n *\n * This does not fetch the response body. The raw response body does not contain the full cart\n * state. The raw response body may not be the latest state of cart items. The raw response body may\n * contain zero-quantity items still present after prior removal.\n *\n * This validates the response status. If the status is not ok this throws an error.\n *\n * This specifies fetch mode no-store so that the call is not unintentionally cached.\n *\n * Adding an item the cart does not necessarily mean a new line item is created. For each item in\n * the input, Shopify makes a server side determination of whether to update the quantity of an\n * existing line item or add a new line item. The factors in that decision are related to how a line\n * item key is determined, which is poorly documented. Basically, if an item has some different\n * properties, e.g. line item properties, or pricing, then the item will always be added as a new\n * line item, even though the variant id is the same as an existing line item.\n *\n * The line item scripts run on every mutation. The scripts will remove any orphaned value set line\n * items implicitly. Keep this in mind when debugging.\n *\n * The content type header must be specified in the request or Shopify interprets the request as a\n * form submission, which we are not doing here, which leads to an error.\n *\n * Shopify's injected front end code (e.g. Trekkie, Monorail) triggers a console warning when it\n * detects that the number of items requested to be added is less than the number that was added.\n * This is not necessarily an error. It can indicate that the line item scripts removed one or more\n * line items during the add mutation.\n *\n * @see https://shopify.dev/docs/api/ajax/reference/cart#post-locale-cart-add-js\n * @see https://help.shopify.com/en/manual/checkout-settings/script-editor/shopify-scripts\n */\nexport async function cartAdd(items: AddItem[]) {\n  const url = resolveUrl(Shopify.routes.root + 'cart/add.js');\n\n  const init: RequestInit = {\n    body: JSON.stringify({ items }),\n    headers: {\n      'Content-Type': 'application/json'\n    },\n    cache: 'no-store',\n    method: 'POST'\n  };\n\n  let response;\n  try {\n    response = await fetch(url, init);\n  } catch (error) {\n    decorateError(error, 'items', items);\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!isResponse(response)) {\n    const error: any = new Error('fetch did not return a response adding to cart');\n    decorateError(error, 'items', items);\n    decorateError(error, 'response', response);\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!response.ok) {\n    const error: any = new Error(`Received status ${response.status} adding to cart`);\n    decorateError(error, 'items', items);\n    decorateError(error, 'request_id', getRequestId(response));\n    decorateError(error, 'response_status', response.status);\n\n    try {\n      const responseBody = await response.json();\n      decorateError(error, 'response_body', responseBody);\n    } catch (responseConsumeAndParseBodyError) {\n      // ignore\n    }\n\n    // For certain error responses, Shopify provides a clearer error message in the response body.\n    // On some 422 responses a more specific description is present alongside the generic message,\n    // so prefer the description, fall back to the generic message, and otherwise leave the default\n    // received-status message in place. For example, adding a bundle whose component is unavailable\n    // yields a body like the following, where description is meaningful and message is generic:\n    //\n    // { \"description\": \"The bundle product 'Product Title' cannot be added to the cart.\",\n    // \"message\": \"Cart Error\", \"status\": 422 }\n    //\n    // The status property of the response body mirrors the HTTP status code (empirical).\n\n    let message;\n    if (response.status === 422 && typeof error.response_body?.description === 'string') {\n      message = error.response_body.description;\n    } else if (typeof error.response_body?.message === 'string') {\n      message = error.response_body.message;\n    }\n\n    if (message) {\n      decorateError(error, 'message', message);\n    }\n\n    throw error;\n  }\n\n  return response;\n}\n\n/**\n * Either id or line must be specified.\n */\nexport interface CartChangeInput {\n  /**\n   * Can be either the variant id or the line item key.\n   */\n  id?: number | string;\n\n  /**\n   * A cart can have multiple line items that share the same variant_id. For example, when variants\n   * have different line item properties, or automatic discounts create variants at different\n   * prices. To account for this, you can use the line property when updating the cart.\n   *\n   * The value of line is the 1-based index position of the item in the cart.\n   */\n  line?: number;\n\n  /**\n   * The properties property sets the line item properties. Its value must be an object of key-value\n   * pairs.\n   *\n   * Whenever a POST request includes properties, it overwrites the entire properties object. Any\n   * key-value pairs that were already in the properties object are erased.\n   *\n   * It's not possible to set a line item's properties property to an empty object once a value is\n   * set. If you need to remove all line item properties, then you need to remove the entire line\n   * item.\n   *\n   * You can visually hide line item properties at checkout by creating private properties. This\n   * technique might be an alternative to removing a line item when managing properties.\n   */\n  properties?: Record<string, any>;\n\n  /**\n   * The value of the quantity property represents the new quantity you want for the line item. It\n   * must be an integer.\n   *\n   * You can remove a line item by setting the quantity to 0.\n   *\n   * It is not clear from the documentation, but based on the description of the selling plan\n   * property, it looks like quantity is actually optional and defaults to 1 when not specified.\n   */\n  quantity?: number;\n\n  /**\n   * The selling_plan property sets the line item selling plan. Its value must be one of the\n   * following:\n   *\n   * * The selling plan ID: To set a specific selling plan for the line item.\n   * * null or an empty string: To remove any selling plan from the line item.\n   *\n   * When specifying the selling_plan property, consider the following:\n   * * You can use only the line property for identifying the line item.\n   * * You should always specify the quantity property. If the quantity isn't specified, then it's\n   *   assumed to be 1.\n   */\n  selling_plan?: number;\n}\n\n/**\n * The response body of a cart change.\n *\n * The two extra arrays are not part of the stored cart. Neither one is present in a get cart\n * response, and a cart update answers with a different extra property instead, so these describe\n * what the change did rather than what the cart holds.\n *\n * None of the functions here parse this body, because a caller that wants the resulting state\n * performs its own get cart afterward. So this type exists to describe what the endpoint answers\n * with rather than to annotate a return value.\n */\nexport interface CartChangeResponseBody extends Cart {\n  /**\n   * The line items the change brought into the cart.\n   *\n   * Nothing has been observed to put an entry in here, including a change that raised a quantity,\n   * so whether an entry looks like a removed one is unconfirmed.\n   */\n  items_added: CartChangeItem[];\n\n  /**\n   * The line items the change took out of the cart.\n   *\n   * A removal driven from a cart drawer, which identifies the line by its position rather than by\n   * variant, does report the removed line here. A removal that identifies the line by variant id\n   * has come back empty, so what qualifies is not settled.\n   */\n  items_removed: CartChangeItem[];\n}\n\n/**\n * An entry in one of the two arrays a cart change response carries.\n *\n * This is undocumented and the shape here is what has been observed on a removal. It is close\n * enough to a line item to be mistaken for one and it is not one, so a few differences are worth\n * knowing. The id is not the variant id, the line key arrives under a different property name, and\n * the price is a decimal string rather than an integer in subunits. It also carries no line item\n * properties, no weight, no discount details, and no parent relationship.\n */\nexport interface CartChangeItem {\n  /**\n   * An opaque identifier for the entry, formatted as a uuid.\n   *\n   * This is not the variant id, unlike the id of a cart line item.\n   */\n  id: string;\n\n  image: string;\n\n  presentment_price: number;\n\n  /**\n   * The price as a decimal string, in the presentment currency.\n   *\n   * Note that this is not an integer in currency subunits, which is how a price is expressed\n   * everywhere else in the cart.\n   *\n   * @example \"289.00\"\n   */\n  price: string;\n\n  product_id: number;\n\n  /**\n   * The title of the product, which here includes the variant.\n   *\n   * Note that a cart line item excludes the variant from this property and only combines the two in\n   * its title.\n   *\n   * @example \"TurboStyle - Blush\"\n   */\n  product_title: string;\n\n  product_type: string;\n  quantity: number;\n  sku: string;\n  title: string;\n  untranslated_product_title: string;\n  untranslated_variant_title: string;\n  url: string;\n  variant_id: number;\n  variant_title: string;\n  vendor: string;\n\n  /**\n   * The line item key, which a cart line item exposes as its key instead.\n   *\n   * @example \"47267055599716:fa232aa1a0b5b250164c9843d7a2ee6c\"\n   */\n  view_key: string;\n}\n\n/**\n * Use the /{locale}/cart/change.js endpoint to change the quantity, properties, and selling_plan\n * properties of a cart line item. Only items already in your cart can be changed, and you can\n * change only one line item at a time.\n *\n * The POST data requires either an id or line property to identify the line item to be changed.\n *\n * The value of the id property can be one of the following:\n *\n * * The line item's variant_id\n * * The line item's key\n *\n * A cart can have multiple line items that share the same variant_id. For example, when variants\n * have different line item properties, or automatic discounts create variants at different prices.\n * Because of this, it's recommended to use the line item key to ensure that you're only changing\n * the intended line item.\n *\n * If the item that you're attempting to change isn't already in the cart, then\n * /{locale}/cart/change.js doesn't add it. Instead, it returns a 404 error.\n *\n * The documentation states that the response body is the JSON of the cart. This exposes the\n * response object but does not fetch the response body.\n *\n * ### Removing out of stock items\n * It is unclear but based on a conversation with Shopify support, prefer to use cart/change instead\n * of cart/update to avoid issues with removing an out of stock item from the cart. Using update\n * results in a 422 error but supposedly cart/change does not.\n */\nexport async function cartChange(input: CartChangeInput) {\n  const url = resolveUrl(Shopify.routes.root + 'cart/change.js');\n\n  const init: RequestInit = {\n    body: JSON.stringify(input),\n    cache: 'no-store',\n    headers: {\n      'Content-Type': 'application/json'\n    },\n    method: 'POST'\n  };\n\n  let response;\n  try {\n    response = await fetch(url, init);\n  } catch (error) {\n    decorateError(error, 'input', input);\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!isResponse(response)) {\n    const error: any = new Error('fetch did not return a response changing cart');\n    decorateError(error, 'request_body', input);\n    decorateError(error, 'response', response);\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!response.ok) {\n    const error: any = new Error(`Received status ${response.status} changing cart`);\n    decorateError(error, 'request_body', input);\n    decorateError(error, 'request_id', getRequestId(response));\n\n    try {\n      decorateError(error, 'response_body', await response.json());\n    } catch (consumeError) {\n      // ignore\n    }\n\n    decorateError(error, 'response_status', response.status);\n    decorateError(error, 'response_url', response.url);\n\n    // For certain error responses, Shopify provides a clearer error message in the response body.\n    // On some 422 responses a more specific description is present alongside the generic message,\n    // so prefer the description, fall back to the generic message, and otherwise leave the default\n    // received-status message in place. Shopify's generic message on these is literally \"Cart\n    // Error\", so promoting it over the description would discard the only meaningful text.\n    //\n    // The status property of the response body mirrors the HTTP status code (empirical).\n\n    let message;\n    if (response.status === 422 && typeof error.response_body?.description === 'string') {\n      message = error.response_body.description;\n    } else if (typeof error.response_body?.message === 'string') {\n      message = error.response_body.message;\n    }\n\n    if (message) {\n      decorateError(error, 'message', message);\n    }\n\n    throw error;\n  }\n\n  return response;\n}\n\n/**\n * Use the GET /{locale}/cart.js endpoint to get the cart as JSON.\n *\n * All monetary properties are returned in the customer's presentment currency. To check the\n * customer's presentment currency, you can use the currency field in the response.\n *\n * @see https://shopify.dev/docs/api/ajax/reference/cart#get-locale-cart-js\n */\nexport async function cartFetch() {\n  const url = resolveUrl(Shopify.routes.root + 'cart.js');\n\n  let response;\n  try {\n    response = await fetch(url, { cache: 'no-store' });\n  } catch (error) {\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!isResponse(response)) {\n    const error: any = new Error('fetching cart did not return a valid response');\n    decorateError(error, 'response', response);\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!response.ok) {\n    const error: any = new Error(`Received status ${response.status} fetching cart`);\n    decorateError(error, 'request_id', getRequestId(response));\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n\n    try {\n      decorateError(error, 'response_body', await response.json());\n    } catch (consumeError) {\n      // ignore\n    }\n\n    throw error;\n  }\n\n  // Using response.json() obscures errors and prevents us from being able to see what the raw\n  // response body looks like. The usual cases of errant response bodies include:\n  //\n  // - default response bodies for errors, which are using plain text or html\n  // - firewall interceptions that produce html as a response, usually html\n  // - wifi connect interstitial page load, usually html\n  //\n  // So we are intentionally using response.text() and JSON.parse.\n\n  let responseBodyText;\n  try {\n    responseBodyText = await response.text();\n  } catch (error) {\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n    throw error;\n  }\n\n  let responseBodyObject;\n  try {\n    responseBodyObject = JSON.parse(responseBodyText);\n  } catch (error) {\n    decorateError(error, 'response_body', responseBodyText);\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n    throw error;\n  }\n\n  if (!isCart(responseBodyObject)) {\n    const error: any = new Error('Unexpected get cart response body');\n    decorateError(error, 'response_body', responseBodyObject);\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n    throw error;\n  }\n\n  return responseBodyObject;\n}\n\nexport interface CartUpdateInput {\n  /**\n   * New cart attributes. Existing cart attributes are overwritten.\n   */\n  attributes?: Attributes;\n\n  /**\n   * Beta shopify feature. Essentially works like Storefront discount apply mutation.\n   *\n   * Pass an empty string to remove discount(s).\n   */\n  discount?: string;\n\n  /**\n   * Cart note.\n   */\n  note?: string;\n\n  /**\n   * Even though Shopify supports specifying variant ids as numbers, we impose the restriction that\n   * the property names of the updates object must be strings due to some strange looking errors\n   * when specifying numbers.\n   */\n  updates?: Record<string, any>;\n}\n\n/**\n * The response body of a cart update.\n *\n * The changelog is not part of the stored cart. It is absent from a get cart response, and a cart\n * change answers with two differently named arrays instead, so it describes what the update did\n * rather than what the cart holds.\n *\n * None of the functions here parse this body, because a caller that wants the resulting state\n * performs its own get cart afterward. So this type exists to describe what the endpoint answers\n * with rather than to annotate a return value.\n */\nexport interface CartUpdateResponseBody extends Cart {\n  /**\n   * A summary of what the update brought into the cart.\n   *\n   * This is present on every successful update, including one that only writes attributes or a\n   * note, in which case the arrays inside it are empty.\n   */\n  items_changelog: ItemsChangelog;\n}\n\n/**\n * The summary of what an update brought into the cart.\n *\n * This is undocumented, so the shape here is only what has been observed. The added array is the\n * only property seen so far. A removed counterpart would be the obvious sibling but no update\n * response has produced one, not even one that set a quantity to zero, so it is left out rather\n * than guessed at.\n */\nexport interface ItemsChangelog {\n  /**\n   * The variants the update brought into the cart, and the quantity each one landed at.\n   *\n   * Only a variant that was not in the cart beforehand shows up here. Raising or lowering the\n   * quantity of a line that already exists leaves this empty, as does removing one, so this reports\n   * newly created lines rather than every quantity change.\n   */\n  added: ItemsChangelogAddition[];\n}\n\n/**\n * An entry in the added array of an update's changelog.\n *\n * This is not a line item. It carries only the variant and the quantity, so it cannot stand in for\n * the corresponding entry in the cart's items array.\n */\nexport interface ItemsChangelogAddition {\n  /**\n   * The quantity the line item landed at, not the amount by which it changed.\n   */\n  quantity: number;\n\n  /**\n   * Note that this is a number here, unlike elsewhere in this module where a variant id is\n   * restricted to a string on the request side.\n   */\n  variant_id: number;\n}\n\n/**\n * Performs a cart update.\n *\n * This does not return the cart updated state. You will need to do a followup call to fetch the\n * updated state of the cart. One reason this does also return the updated state is that we must\n * perform a second call to fetch the updated state but not all callers are interested in doing so.\n *\n * This does not return the response body of the call. There is no need to fetch the body because\n * the body does not contain useful data. The response body can contain 0 quantity items and other\n * kinds of strange things.\n *\n * This must specify the content type header. Shopify supports both a JSON request body and a form\n * data request body. Shopify defaults to interpreting requests as form submissions. This uses the\n * JSON format so the header is required.\n *\n * Backend scripts may affect the output.\n *\n * @see https://shopify.dev/docs/api/ajax/reference/cart#post-locale-cart-update-js\n * @see https://shopify.dev/docs/api/ajax/reference/cart#update-line-item-quantities\n * @see https://shopify.dev/docs/api/ajax/reference/cart#private-cart-attributes\n */\nexport async function cartUpdate(input: CartUpdateInput) {\n  const url = resolveUrl(Shopify.routes.root + 'cart/update.js');\n\n  const init: RequestInit = {\n    body: JSON.stringify(input),\n    cache: 'no-store',\n    headers: {\n      'Content-Type': 'application/json'\n    },\n    method: 'POST'\n  };\n\n  let response;\n  try {\n    response = await fetch(url, init);\n  } catch (error) {\n    decorateError(error, 'input', input);\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!isResponse(response)) {\n    const error: any = new Error('fetch did not return a response updating cart');\n    decorateError(error, 'request_body', input);\n    decorateError(error, 'response', response);\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!response.ok) {\n    const error: any = new Error(`Received status ${response.status} updating cart`);\n    decorateError(error, 'request_body', input);\n    decorateError(error, 'request_id', getRequestId(response));\n    decorateError(error, 'response_status', response.status);\n\n    try {\n      const parsedErrorResponseBody = await response.json();\n      decorateError(error, 'response_body', parsedErrorResponseBody);\n    } catch (consumeError) {\n      // ignore\n    }\n\n    // For certain error responses, Shopify provides a clearer error message in the response body.\n    // On some 422 responses a more specific description is present alongside the generic message,\n    // so prefer the description, fall back to the generic message, and otherwise leave the default\n    // received-status message in place. Shopify's generic message on these is literally \"Cart\n    // Error\", so promoting it over the description would discard the only meaningful text.\n    //\n    // The status property of the response body mirrors the HTTP status code (empirical).\n\n    let message;\n    if (response.status === 422 && typeof error.response_body?.description === 'string') {\n      message = error.response_body.description;\n    } else if (typeof error.response_body?.message === 'string') {\n      message = error.response_body.message;\n    }\n\n    if (message) {\n      decorateError(error, 'message', message);\n    }\n\n    throw error;\n  }\n\n  return response;\n}\n\n/**\n * Returns whether the error indicates the add failed because one of the products is out of stock\n * (not available for sale). In this case Shopify provides an error response body that looks like\n * this:\n *\n * ```json\n * {\n *   \"description\": \"All 0 <product title> are in your cart\",\n *   \"message\": \"Cart Error\",\n *   \"status\": 422\n * }\n * ```\n *\n * ```json\n * {\n *   \"description\": \"The product '<title>' is already sold out.\",\n *   \"message\": \"Cart Error\",\n *   \"status\": 422\n * }\n * ```\n *\n * This error can happen both when adding an item or updating the quantity of an item.\n *\n * This assumes error messages are in English.\n */\nexport function isUnavailableError(error: any) {\n  if (error instanceof Error && 'response_body' in error && isObject(error.response_body) &&\n    error.response_body.status === 422) {\n    const description = error.response_body.description;\n    if (typeof description === 'string') {\n      // this one is only present in the add error response\n      if (description.startsWith('All 0') && description.includes('in your cart')) {\n        return true;\n      }\n\n      // this string is sometimes present in the update error response, not just the add response\n      if (description.includes('already sold out')) {\n        return true;\n      }\n    }\n  }\n\n  return false;\n}\n\n/**\n * Returns whether the error indicates the add failed because the shopper already holds the item's\n * per-order purchase limit in their cart. Shopify provides an error response body that looks like\n * this:\n *\n * ```json\n * {\n *   \"description\": \"The maximum quantity of this item is already in your cart.\",\n *   \"message\": \"The maximum quantity of this item is already in your cart.\",\n *   \"status\": 422\n * }\n * ```\n *\n * This assumes error messages are in English.\n */\nexport function isMaxQuantityError(error: any) {\n  if (error instanceof Error && 'response_body' in error && isObject(error.response_body) &&\n    error.response_body.status === 422) {\n    const description = error.response_body.description;\n    if (typeof description === 'string') {\n      const normalizedDescription = description.toLowerCase();\n      if (normalizedDescription.includes('maximum quantity of this item is already in your cart')) {\n        return true;\n      }\n    }\n  }\n\n  return false;\n}\n\n/**\n * Returns whether the error indicates the add failed because the request mixed a bundle product and\n * an add-on in the same `cart/add.js` call, which Shopify does not allow. Shopify provides an error\n * response body that looks like this:\n *\n * ```json\n * {\n *   \"description\": \"Bundle products and add-ons cannot be mixed\",\n *   \"message\": \"Bundle products and add-ons cannot be mixed\",\n *   \"status\": 422\n * }\n * ```\n *\n * This assumes error messages are in English.\n */\nexport function isBundleAddonMixError(error: any) {\n  if (error instanceof Error && 'response_body' in error && isObject(error.response_body) &&\n    error.response_body.status === 422) {\n    const description = error.response_body.description;\n    if (typeof description === 'string') {\n      const normalizedDescription = description.toLowerCase();\n      if (normalizedDescription.includes('bundle products and add-ons cannot be mixed')) {\n        return true;\n      }\n    }\n  }\n\n  return false;\n}\n\n/**\n * Returns whether the error indicates the add failed because the selling plan sent with the item is\n * not one Shopify will accept for that variant. This happens when a rendered page carries a selling\n * plan ID (for example a pre-order deferred-payment plan) that has since become stale: the variant\n * came back in stock, or the plan was edited or removed, after the page was rendered but before the\n * shopper clicked add. Shopify responds with a body that looks like this:\n *\n * ```json\n * {\n *   \"description\": \"Cannot apply selling plan to variant\",\n *   \"message\": \"Cart Error\",\n *   \"status\": 422\n * }\n * ```\n *\n * This assumes error messages are in English.\n */\nexport function isInvalidSellingPlanError(error: any) {\n  if (error instanceof Error && 'response_body' in error && isObject(error.response_body) &&\n    error.response_body.status === 422) {\n    const description = error.response_body.description;\n    if (typeof description === 'string') {\n      if (description.includes('Cannot apply selling plan to variant')) {\n        return true;\n      }\n    }\n  }\n\n  return false;\n}\n\n/**\n * Returns whether the given value looks like a cart object.\n *\n * This is not a complete validation. This is a shallow check that examines whether the value is an\n * object containing an items property that is an array. Any value that meets that criteria\n * satisfies the meaning of a cart.\n */\nexport function isCart(value: any): value is Cart {\n  return isObject(value) && Array.isArray(value.items);\n}\n\nfunction isObject(value: any): value is Record<string, any> {\n  return typeof value === 'object' && value !== null;\n}\n","import type { Cart } from '@integrabeauty/shopify-ajax-api';\n\n/**\n * Returns a general object consisting of only those properties from the cart that might be useful\n * during triage. This avoids the lazy capture of the full cart, which is often full of lots of data\n * irrelevant to analysis, and often results in truncation that obscures the values that are very\n * important to analysis.\n *\n * The output type here is intentionally vaguely typed so as to not bake in any future guarantees of\n * property names. This output is not meant for library usage. It is only meant for relaying to\n * other downstream systems that operate on unknown bags of data.\n *\n * This is written defensively to never throw so that it is safe to call from critical paths.\n */\nexport function digest(cart: Partial<Cart>) {\n  if (cart === null) {\n    const output: Record<string, any> = { cart_input_type: 'null' };\n    return output;\n  }\n\n  if (typeof cart !== 'object') {\n    const output: Record<string, any> = { cart_input_type: typeof cart };\n    return output;\n  }\n\n  try {\n    const output: Record<string, any> = {\n      token: cart.token,\n      attribute_keys: Object.keys(cart.attributes ?? {}),\n      discount_codes: cart.discount_codes,\n      cart_level_discount_titles: cart.cart_level_discount_applications?.map(\n        application => application?.title),\n      items: cart.items?.map(item => ({\n        key: item?.key,\n        variant_id: item?.variant_id,\n        quantity: item?.quantity,\n        properties: item?.properties,\n        discounts: item?.line_level_discount_allocations?.map(\n          allocation => allocation?.discount_application?.title)\n      }))\n    };\n    return output;\n  } catch (error) {\n    return { digest_failed: `${error}` };\n  }\n}\n","import { type Cart, isCart } from '@integrabeauty/shopify-ajax-api';\n\n/**\n * Returns whether two carts are equal. This is an approximate check that only considers line items.\n *\n * This does not consider line item properties.\n *\n * This does not consider cart attributes.\n *\n * This does not consider discount applications and allocations.\n *\n * This function exists separate from a full cart equality check because we need a way to consider\n * only certain state changes as meaningful. One of the reasons this is needed is because we have\n * little to no control over third party changes to cart state. There are apps that inject code that\n * does its own cart modifications, e.g. adjusting the cart's attributes on page load, and short of\n * uninstalling them we cannot prevent this. There are even mobile apps that inject content scripts\n * that assume control over the cart state. The decision making to install apps does not always\n * appreciate the complexity of the state comparison case. Given that, we need a way to treat only\n * certain state changes as meaningful, which means being deliberate about which parts of the api\n * contract we honor. This function therefore only considers line items and ignores other state\n * changes because it is intentionally only adhering to one clause of the Shopify cart contract.\n *\n * This is complicated because Shopify's cart mutations generally do not return the updated cart\n * state. They often return junk. The only way to reliably determine the updated cart state is to\n * fetch it separately, which always involves fetching the entire state. Which unfortunately is\n * often the first time that we learn that something else has modified the cart. We have to\n * reconcile the difference between the edits we issued and the actual updated cart state,\n * accounting for unexpected changes. In other words, we are comparing projections of the state\n * rather than the actual state. Since the endpoints do not support projection (e.g. give me only\n * these fields), we do this by selectively comparing only certain cart properties.\n */\nexport function hasSimilarItems(cart1: Cart, cart2: Cart) {\n  // Sanity check the inputs. This should never happen. However, it does allow the caller to be less\n  // careful with guaranteeing null values. Also, we have seen runtime state diverge from statically\n  // typed state in rare cases of bugs and unexpected third party interference, so even though this\n  // is redundant type checking at compile time, it is not redundant at runtime.\n\n  if (!isCart(cart1)) {\n    return false;\n  }\n\n  if (!isCart(cart2)) {\n    return false;\n  }\n\n  // Seeing a new token is unlikely to ever happen and is more of a sanity check. One plausible\n  // situation is cart expiration resulting in cart replacement.\n\n  // We have seen cases where we have partial cart state and the token is missing, so this only does\n  // token comparison when both tokens are known, to still allow for other kinds of cart state\n  // comparison when either is unknown.\n\n  if (cart1.token && cart2.token && cart1.token !== cart2.token) {\n    return false;\n  }\n\n  // The same number of line items should be present in both carts.\n\n  if (cart1.items.length !== cart2.items.length) {\n    return false;\n  }\n\n  // The lines should have the same keys with the same quantities. If any one line does not, break.\n  // Because there are the same number of line items in both carts, finding that every item from\n  // cart1 exists in cart2 implies that every item from cart2 exists in cart1, so we only need to\n  // examine state in one direction.\n\n  // Watch out for careless type inconsistencies, e.g. key as a number on input but key as a string\n  // on output. It is best to only compare output types because those are consistent. The cart api\n  // unfortunately has some Postel's Law like behavior. Avoid comparing a manually constructed cart\n  // to a fetched cart.\n\n  for (const item1 of cart1.items) {\n    let found = false;\n    for (const item2 of cart2.items) {\n      if (item1.key === item2.key && item1.quantity === item2.quantity) {\n        found = true;\n        break;\n      }\n    }\n\n    if (!found) {\n      return false;\n    }\n  }\n\n  return true;\n}\n","/**\n * Copies a single piece of supplementary context onto an error before it is thrown or reported.\n *\n * Use this when decorating a value the calling code did not construct, such as a caught value or an\n * error handed in by another module. An error the caller just built with new is always writable, so\n * plain assignment is fine there. A value from elsewhere may be a foreign object this code does not\n * own. Such a value can carry a non-writable property, such as a DOMException whose code is a read-\n * only accessor, or be frozen outright, and assigning to it then throws. Attaching context must\n * never itself fail, so a rejected assignment is swallowed and the remaining context is still\n * attempted. Guarding each property in isolation means a single read-only slot, the common case,\n * costs only that one property rather than all of them.\n *\n * In practice the value that triggers this is almost always a DOMException that reached the caller\n * from a fetch the browser cut short, such as an abort when an in-flight request is interrupted.\n * The platform defines a DOMException's code, name, and message as read-only attributes, so\n * assigning a code onto one throws under the strict mode that all bundled module code runs in. It\n * surfaces from Safari far more than elsewhere for two reasons. The reported wording, attempted to\n * assign to readonly property, is JavaScriptCore's phrasing for this failure, so the same bug in\n * another engine is worded differently and groups as a separate report, which makes this one look\n * Safari specific even though the read-only rule is not. And iOS Safari abandons in-flight fetches\n * aggressively when a tab is backgrounded or a connection drops, which is when such a DOMException\n * tends to be produced and handed to code that assumed an ordinary writable error.\n *\n * A swallowed failure is logged at a level low enough that it is recorded as a breadcrumb for\n * monitoring rather than escalated into its own captured event. That keeps the lost context visible\n * alongside the eventual report without adding fresh noise of its own. The breadcrumb names the\n * property that could not be attached, the kind of value that rejected it, and why, which is enough\n * to recognize a read-only or frozen caught value after the fact.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Read-only\n * @see https://developer.mozilla.org/en-US/docs/Web/API/DOMException\n * @see https://webidl.spec.whatwg.org/#dfn-read-only\n */\nexport function decorateError(error: any, key: string, value: unknown) {\n  try {\n    error[key] = value;\n  } catch (assignmentError) {\n    let reason: string;\n    if (assignmentError instanceof Error) {\n      reason = assignmentError.message;\n    } else {\n      reason = `${assignmentError}`;\n    }\n\n    console.log('Failed to assign error property', {\n      key,\n      name: error?.name,\n      tag: Object.prototype.toString.call(error),\n      reason\n    });\n  }\n}\n","import type { Cart } from '@integrabeauty/shopify-ajax-api';\nimport { decorateError } from '../decorate-error.js';\nimport type { Transaction } from './transaction.js';\n\ntype CartUpdateErredEvent = CustomEvent<{\n  /**\n   * Cart state.\n   *\n   * This may be undefined or null or invalid because almost nothing is guaranteed.\n   */\n  cart?: Cart;\n\n  /**\n   * An error code that describes the context of the error or the type of the error.\n   */\n  code: string;\n\n  /**\n   * The error that occurred. This will almost always be of type `Error`. However, this is set to\n   * the value of whatever error was caught. Values not of type `Error` can be thrown. We are not\n   * checking that the value is an instance of an `Error` first. It is not safe to assume this is of\n   * type `Error` particularly because of code minimization and third party modifications.\n   */\n  error: any;\n\n  /**\n   * A mix of misc properties that various cart transactions will include that help provide more\n   * information about the inputs to the code that led to the error.\n   */\n  inputs?: Record<string, any>;\n\n  /**\n   * If true, this means the event was emitted by a multi-fetch operation where some of the fetches\n   * succeeded. For example, consider the addItems call, which has to do two fetches in its virtual\n   * transaction. This will be true when the fetch to add the item succeeded but the fetch to get\n   * the new cart state after the add failed. This flag exists so that callers can choose to react\n   * to this situation differently, e.g. by retrying just the cart state fetch, or changing what\n   * error message is displayed (a failure to add versus a partially successful add).\n   */\n  partial?: boolean;\n\n  /**\n   * An optional programmatic code or human-readable description of the context in which some cart\n   * update was taken. This is primarily for analytics, but can also be used for ui logic.\n   *\n   * For example:\n   * * distinctly identify two different add to cart buttons\n   * * distinctly identify a particular ui element used to apply a discount\n   */\n  scenario?: string;\n\n  /**\n   * The transaction in which the error occurred.\n   */\n  transaction: Transaction;\n}>;\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    'cart-update-erred': CartUpdateErredEvent;\n  }\n}\n\n/**\n * Broadcasts a cart failure and, when it is a genuine programmer error, surfaces it to the global\n * error handler so that monitoring records it.\n *\n * The cart helpers deliberately catch their own failures so that the code awaiting them is not\n * forced to handle a promise rejection. Every failure is broadcast as an event for the interface to\n * react to. That catch keeps the promise from rejecting, but for an unexpected failure it does not\n * mean the problem was actually dealt with. It is still a real error that would have reached the\n * top level had it never been caught. The escalation step re-throws those so that they do, leaving\n * error monitoring to record whatever uncaught errors arrive with no knowledge of the cart.\n */\nexport function report(dispatcher: EventTarget | null, detail: CartUpdateErredEvent['detail']) {\n  dispatchCartUpdateErred(dispatcher, detail);\n  escalateCartError(detail);\n}\n\n/**\n * Dispatches the failure as an event for the interface to react to.\n *\n * The dispatcher is the target the event is dispatched from. It is required but may be null, and it\n * may be any event target, not necessarily an element or window. The target is used as given, with\n * two exceptions that exist only to keep the event reaching window level listeners: a null target\n * dispatches from window, and so does a detached element.\n *\n * A detached element is swapped for window because of how bubbling works. We want to guarantee the\n * event bubbles up to window so that window level listeners are notified. A detached element only\n * bubbles up to its detached subtree root, which is not window, so dispatching from it directly\n * would not reach those listeners.\n */\nfunction dispatchCartUpdateErred(dispatcher: EventTarget | null,\n  detail: CartUpdateErredEvent['detail']) {\n  // Dispatch the failure from whatever target the caller designates; this forms no opinion about\n  // what kind of target it is. The only special handling exists to keep the event reaching the\n  // global listeners: a detached element has no live ancestor chain to bubble through, so it is\n  // swapped for the global scope, and a null target means the global scope outright. Any other\n  // target, including a connected element, is used as given. Callers should still prefer the\n  // element most directly responsible for the failure, so a connected element carries truthful\n  // provenance and bubbles, crossing shadow boundaries, up to the global listeners.\n\n  let target: EventTarget;\n  if (dispatcher instanceof Element) {\n    if (dispatcher.isConnected) {\n      target = dispatcher;\n    } else {\n      target = window;\n    }\n  } else if (dispatcher) {\n    target = dispatcher;\n  } else {\n    target = window;\n  }\n\n  const event = new CustomEvent<typeof detail>('cart-update-erred', {\n    detail,\n    bubbles: true,\n    composed: true\n  });\n\n  target.dispatchEvent(event);\n}\n\n/**\n * Surfaces a genuine programmer error to the global error handler so that monitoring records it.\n *\n * Some failures are expected outcomes with their own user facing messaging rather than programmer\n * errors: a shopper adding an out of stock item, or mistyping a discount code. Those are never\n * escalated to monitoring. Each such failure is identified by its code, the same classification the\n * cart helpers attach for the interface, so the list here must keep pace with the codes those\n * helpers produce.\n *\n * The throw is deferred to a later task so that it escapes the surrounding promise chain instead of\n * rejecting it, preserving the rejection-free contract of the cart helpers. The error keeps the\n * stack captured when it was first created, so the report still points at the origin of the failure\n * rather than at this deferred throw. However, beware that this does mean that the error event\n * properties will reflect this throw context, not the original error context, which means that the\n * instrumenting code must prioritize the original error properties over the error event properties.\n *\n * When available, the throw is routed through the reportError api, which surfaces the value to the\n * global error handler without the extra task hop. Older engines that lack it fall back to a\n * deferred throw, which reaches the same handler.\n *\n * The context describing the failure is attached to the error so that monitoring which serializes\n * the non-standard properties of an error can include it. A value that is not an error is wrapped\n * so that the context still has somewhere to live.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/reportError\n */\nfunction escalateCartError(detail: CartUpdateErredEvent['detail']) {\n  // These are expected outcomes with their own user facing messaging rather than programmer errors.\n  //\n  // This list of codes to ignore deliberately excludes invalid selling plan codes such as\n  // ADD_ITEMS_INVALID_SELLING_PLAN, ADD_ITEMS_INVALID_BACKORDER_SELLING_PLAN, and\n  // ADD_ITEMS_INVALID_TBYB_SELLING_PLAN. Unlike the ephemeral shopper conditions, a rejected\n  // selling plan is a plan we attached to the item, so it signals that something is misconfigured\n  // rather than a transient outcome. We want these to continue reaching error diagnostics so that\n  // misconfiguration is visible.\n\n  const ignoredCodes = [\n    'APPLY_DISCOUNTS',\n    'ADD_ITEMS_OOS',\n    'ADD_ITEMS_BUNDLE_ADDON_MIX',\n    'ADD_ITEMS_MAX_QUANTITY',\n    'CHANGE_CART_OOS',\n    'INIT_TIMEOUT',\n    'UPDATE_CART_OOS'\n  ];\n\n  if (detail && !ignoredCodes.includes(detail.code)) {\n    let error: any;\n\n    if (detail.error instanceof Error) {\n      error = detail.error;\n    } else {\n      error = new Error(`${detail.error}`);\n    }\n\n    decorateError(error, 'code', detail.code);\n    decorateError(error, 'inputs', detail.inputs);\n    decorateError(error, 'scenario', detail.scenario);\n    decorateError(error, 'cart', detail.cart);\n    decorateError(error, 'transaction_id', detail.transaction?.id);\n    decorateError(error, 'transaction_name', detail.transaction?.name);\n\n    if (typeof reportError === 'function') {\n      reportError(error);\n    } else {\n      setTimeout(raise, 0, error);\n    }\n  }\n}\n\nfunction raise(value: unknown) {\n  throw value;\n}\n","/**\n * Throws an assertion error if the given condition is falsy.\n *\n * @param expression any expression, although one that evaluates to true or false is preferred by\n * convention\n * @param message optional error text\n */\nexport function assert(expression: unknown, message?: string) {\n  if (!expression) {\n    throw new AssertionError(message);\n  }\n}\n\nexport class AssertionError extends Error {\n  constructor(message = 'Assertion error') {\n    super(message);\n    this.name = this.constructor.name;\n  }\n}\n","const rnds8 = new Uint8Array(16);\nexport default function rng() {\n    return crypto.getRandomValues(rnds8);\n}\n","import validate from './validate.js';\nconst byteToHex = [];\nfor (let i = 0; i < 256; ++i) {\n    byteToHex.push((i + 0x100).toString(16).slice(1));\n}\nexport function unsafeStringify(arr, offset = 0) {\n    return (byteToHex[arr[offset + 0]] +\n        byteToHex[arr[offset + 1]] +\n        byteToHex[arr[offset + 2]] +\n        byteToHex[arr[offset + 3]] +\n        '-' +\n        byteToHex[arr[offset + 4]] +\n        byteToHex[arr[offset + 5]] +\n        '-' +\n        byteToHex[arr[offset + 6]] +\n        byteToHex[arr[offset + 7]] +\n        '-' +\n        byteToHex[arr[offset + 8]] +\n        byteToHex[arr[offset + 9]] +\n        '-' +\n        byteToHex[arr[offset + 10]] +\n        byteToHex[arr[offset + 11]] +\n        byteToHex[arr[offset + 12]] +\n        byteToHex[arr[offset + 13]] +\n        byteToHex[arr[offset + 14]] +\n        byteToHex[arr[offset + 15]]).toLowerCase();\n}\nfunction stringify(arr, offset = 0) {\n    const uuid = unsafeStringify(arr, offset);\n    if (!validate(uuid)) {\n        throw TypeError('Stringified UUID is invalid');\n    }\n    return uuid;\n}\nexport default stringify;\n","import rng from './rng.js';\nimport { unsafeStringify } from './stringify.js';\nfunction v4(options, buf, offset) {\n    if (!buf && !options && crypto.randomUUID) {\n        return crypto.randomUUID();\n    }\n    return _v4(options, buf, offset);\n}\nfunction _v4(options, buf, offset) {\n    options = options || {};\n    const rnds = options.random ?? options.rng?.() ?? rng();\n    if (rnds.length < 16) {\n        throw new Error('Random bytes length must be >= 16');\n    }\n    rnds[6] = (rnds[6] & 0x0f) | 0x40;\n    rnds[8] = (rnds[8] & 0x3f) | 0x80;\n    if (buf) {\n        offset = offset || 0;\n        if (offset < 0 || offset + 16 > buf.length) {\n            throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);\n        }\n        for (let i = 0; i < 16; ++i) {\n            buf[offset + i] = rnds[i];\n        }\n        return buf;\n    }\n    return unsafeStringify(rnds);\n}\nexport default v4;\n","/**\n * Returns a deep clone of the given value.\n *\n * Prefers structuredClone and falls back to a JSON round trip. The fallback covers both an\n * environment without structuredClone and one whose structuredClone cannot be trusted. On the\n * storefront a compromised browser can leave a monkey-patched structuredClone that returns\n * undefined for a real object rather than cloning it, which otherwise surfaces as confusing\n * property-access failures far from the cause. The values cloned through here are plain data that\n * round trips faithfully through JSON, so the fallback is equivalent rather than a degradation.\n */\nexport function copy<T>(value: T) {\n  if (typeof structuredClone === 'function') {\n    const cloned = structuredClone(value);\n\n    // A healthy structuredClone returns a nullish result only for a nullish input, so a nullish\n    // clone of a non-nullish value means the primitive cannot be trusted. Fall through to the\n    // serialization clone in that case.\n\n    const inputPresent = value !== null && value !== undefined;\n    const cloneMissing = cloned === null || cloned === undefined;\n\n    let trustworthy = true;\n    if (inputPresent && cloneMissing) {\n      trustworthy = false;\n    }\n\n    if (trustworthy) {\n      return cloned;\n    }\n  }\n\n  return <T>JSON.parse(JSON.stringify(value));\n}\n","import type { Cart } from '@integrabeauty/shopify-ajax-api';\nimport { containsJavascriptWord } from '../../untrusted-value.js';\n\n/**\n * Returns whether a discount code string is well-formed. This does not guarantee the discount code\n * is valid and in the applicable state. That is why this function is not named isValid.\n *\n * This tolerates any kind of input so as to provide a convenient API. The caller is not obligated\n * to check that the input is a string beforehand. This function exists to avoid that boilerplate.\n * However, callers should be providing values that are strings.\n */\nexport function isWellFormed(value: any) {\n  if (typeof value !== 'string') {\n    return false;\n  }\n\n  if (value.length === 0) {\n    return false;\n  }\n\n  // This catches abuse of path parameters\n  if (value.includes('/') || value.includes('\\\\')) {\n    return false;\n  }\n\n  if (value.includes(' ')) {\n    return false;\n  }\n\n  const upper = value.toUpperCase();\n\n  // This catches common js bugs in the calling code\n  if (upper === 'UNDEFINED' || upper === 'NULL') {\n    return false;\n  }\n\n  if (containsJavascriptWord(upper)) {\n    return false;\n  }\n\n  return true;\n}\n\n/**\n * Returns normalized array of discount code strings currently applied to the cart.\n *\n * This filters out codes from the cart's discount_codes array property where the applicable value\n * is not true. That is what distinguishes this function from {@link getDiscountCodes}.\n *\n * This normalizes the codes. Shopify does not guarantee the code strings are uppercase. This will\n * uppercase.\n *\n * Tolerates null/undefined cart. Returns an empty array. This is safe to use without checking\n * whether cart is defined.\n *\n * Tolerates invalid discount_codes property. This supports a rare case where the cart was SSRed\n * correctly, but then not fetched. The SSR state is missing the property. In this case an empty\n * array is returned.\n */\nexport function getApplicableDiscountCodes(cart: Cart) {\n  if (!cart) {\n    return [];\n  }\n\n  if (!Array.isArray(cart.discount_codes)) {\n    return [];\n  }\n\n  return cart.discount_codes\n    .filter(code => code.applicable)\n    .map(code => code.code)\n    .map(code => code.toUpperCase());\n}\n\n/**\n * Returns all codes currently in the cart's discount codes array.\n *\n * This includes applicable as well as inapplicable codes. This inclusion of inapplicable codes is\n * what distinguishes this from the function {@link getApplicableDiscountCodes}.\n *\n * The returned codes are always uppercase. This must uppercase the codes returned because Shopify\n * does not guarantee the codes are upper case. Codes applied at checkout are saved in whatever\n * input case was used. The logic that applies codes while shopping can guarantee the input codes\n * are in uppercase but the logic at checkout cannot because the checkout logic is managed by\n * Shopify.\n *\n * It is possible that the discount_codes array property in the input cart is undefined. Shopify\n * does not support rendering the discount_codes array in liquid. There is a situation where the\n * cart state was server side rendered without discount_codes and then we failed to update the state\n * and therefore failed to set the property dynamically during init. This tolerates that so as to\n * guarantee that an array is always returned.\n */\nexport function getDiscountCodes(cart: Cart) {\n  if (!cart) {\n    return [];\n  }\n\n  if (!Array.isArray(cart.discount_codes)) {\n    return [];\n  }\n\n  return cart.discount_codes.map(code => code.code).map(code => code.toUpperCase());\n}\n\n/**\n * Totals up the discount amounts the cart has allocated and returns them as a map. Values are in\n * cents.\n *\n * The keys are discount application titles in uppercase rather than discount codes. For an ordinary\n * code those are the same string, which is what lets a caller match this map against the cart's\n * discount codes. They are not guaranteed to agree though, because Shopify treats the title as the\n * customer facing name of the discount rather than as the code. So a caller can end up holding a\n * title here that matches none of the codes on the cart.\n *\n * A code missing from this map has not necessarily done nothing. It means the cart allocated no\n * amount to it. A free shipping code behaves that way. It sits among the cart's discount codes as\n * applicable while appearing in neither array read here, because shipping is not priced until\n * checkout.\n *\n * Tolerates a null or undefined cart, and a cart missing any of the arrays this reads. Returns an\n * empty map. This is safe to use without checking whether the cart is defined.\n */\nexport function getDiscountAmounts(cart: Cart) {\n  // Shopify keeps discounts in two separate places, split by scope, so we read both to find all of\n  // them. A discount covering the cart as a whole lands in cart_level_discount_applications. A\n  // discount covering particular products lands on each line item holding one of those products, in\n  // that item's line_level_discount_allocations.\n  //\n  // The two do not overlap, which is what makes it safe to add both into one map without counting a\n  // discount twice. We checked this in real carts from both directions. A cart wide code showed up\n  // only in the cart level array with the line allocations empty, and a product code showed up only\n  // on the lines with the cart level array empty.\n  //\n  // A product discount is allocated separately to every line it touches, so its total is the sum of\n  // those per line amounts. That is what the running total is for, and it is also why the same code\n  // can appear many times while we walk the lines.\n  //\n  // Note which field each loop reads, because the two arrays do not name their amounts the same\n  // way. The line loop reads the allocation's amount, which is the share allocated to that one\n  // line. The application sitting inside that allocation carries a total_allocated_amount too, and\n  // in the carts we looked at it happened to equal the same share, but it describes the application\n  // rather than the line, so we do not read it there.\n  //\n  // We skip anything that is not a discount code. Automatically applied discounts and script\n  // applied discounts land in these same arrays, but they carry no code for a caller to match\n  // against, so they do not belong in a map keyed this way.\n\n  const discountAmounts = new Map<string, number>();\n\n  if (!cart) {\n    return discountAmounts;\n  }\n\n  // The isArray checks in the following code are considered acceptable paranoia. We want this to\n  // work in bad environments with partial carts, which we have surprisingly encountered.\n\n  // Discounts covering the cart as a whole\n\n  if (Array.isArray(cart.cart_level_discount_applications)) {\n    for (const application of cart.cart_level_discount_applications) {\n      if (application.type === 'discount_code') {\n        const code = application.title.toUpperCase();\n        const discount = application.total_allocated_amount;\n        const currentTotal = discountAmounts.get(code) || 0;\n        discountAmounts.set(code, currentTotal + discount);\n      }\n    }\n  }\n\n  // Discounts covering particular products, spread over the lines holding those products\n\n  if (Array.isArray(cart.items)) {\n    for (const lineItem of cart.items) {\n      if (!Array.isArray(lineItem.line_level_discount_allocations)) {\n        continue;\n      }\n\n      for (const allocation of lineItem.line_level_discount_allocations) {\n        if (allocation.discount_application.type === 'discount_code') {\n          const code = allocation.discount_application.title.toUpperCase();\n          const discount = allocation.amount;\n          const currentTotal = discountAmounts.get(code) || 0;\n          discountAmounts.set(code, currentTotal + discount);\n        }\n      }\n    }\n  }\n\n  return discountAmounts;\n}\n","/**\n * Returns whether a string contains the word \"javascript\" anywhere. This is a lightweight heuristic\n * for rejecting obviously-junk externally-sourced values before they enter state, not real XSS\n * defense, which happens more strictly at the actual sinks. The broad match suits values from a\n * constrained alphabet, such as discount codes, where the word never legitimately appears.\n */\nexport function containsJavascriptWord(value: any) {\n  return typeof value === 'string' && /javascript/i.test(value);\n}\n\n/**\n * Returns whether a string contains the \"javascript:\" URI scheme. This is a lightweight heuristic\n * for rejecting the classic scheme-injection vector before a value enters state, not real XSS\n * defense, which happens more strictly at the actual sinks. Unlike {@link containsJavascriptWord}\n * this tolerates the bare word, so a value that is itself an arbitrary external URL, such as a\n * referrer, is not falsely rejected merely for mentioning javascript in a path.\n */\nexport function containsJavascriptScheme(value: any) {\n  return typeof value === 'string' && /javascript:/i.test(value);\n}\n","import { assert } from '@integrabeauty/assert';\nimport { type Cart, cartFetch as cartFetchOnce, isCart } from '@integrabeauty/shopify-ajax-api';\nimport { v4 as uuidv4 } from 'uuid';\nimport { copy } from '../object.js';\nimport { getApplicableDiscountCodes } from './discount/discount.js';\nimport type { Transaction } from './transaction.js';\n\n/**\n * This event is generated when the state of the cart changes. The event includes the old state and\n * the new state and some deltas.\n *\n * Various cart api calls can result in unexpected behavior. For example, adding an item to cart\n * could cause the line key of unrelated cart lines to change. Therefore, this does not generate\n * deltas based on line key. In addition, each time cart state is obtained, there is no guarantee\n * that the state only changed because of the specified change. For example, a visitor interacting\n * with the same cart in multiple tabs may update the state in one tab, but because the deltas are\n * based on the prior state per tab, the next update in one of the tabs would diff against older\n * state. Therefore, listeners should refrain from operating on deltas and always treat each cart\n * update holistically.\n */\ntype CartUpdatedEvent = CustomEvent<{\n  /**\n   * The new cart state.\n   */\n  cart: Cart;\n\n  /**\n   * Discount codes present in the new cart state that are not present in the old cart state.\n   *\n   * This only includes discount codes. This does not include automatically applied discounts or\n   * script applied discounts.\n   */\n  discount_codes_added: string[];\n\n  /**\n   * Discount codes present in the old cart state that are not present in the new cart state.\n   *\n   * This only includes discount codes. This does not include automatically applied discounts or\n   * script applied discounts.\n   */\n  discount_codes_removed: string[];\n\n  /**\n   * A unique event id. This is useful if tracking this event client side and server side and there\n   * is a need to correlate front end and backend events.\n   */\n  event_id: string;\n\n  /**\n   * Whether this event represents a cart initialization.\n   *\n   * The diffs for an initial update are not useful. In this case the transition is from the empty\n   * state. Do not treat an initial update as a non-initial update.\n   */\n  is_initial?: boolean;\n\n  /**\n   * The previous state of the cart.\n   *\n   * This may be null. For example, when initializing the cart.\n   */\n  previous: Cart;\n\n  /**\n   * Certain transactions provide a description of the context in which the transaction occurred.\n   * For example, two add to cart elements might specify two scenarios when adding to cart, which\n   * then generates an update event with different scenarios, which can then be relayed downstream\n   * to analytics to differentiate in reporting.\n   *\n   * This is primarily an analytics label, but be aware that some consumers also branch on specific\n   * values as behavior, for example suppressing UI reactions to updates the shopper did not\n   * initiate in this tab. Certain values are therefore effectively part of behavior, not only\n   * reporting, so renaming one can change how the UI reacts, not just how a change is reported.\n   *\n   * This is intentionally a broad type. This is not meant to be a type safe enumeration. There are\n   * no heavy safeguards here.\n   */\n  scenario?: string;\n\n  /**\n   * Details of the cart service transaction that triggered this update event.\n   */\n  transaction: {\n    /**\n     * Transaction id\n     */\n    id: number;\n\n    /**\n     * Transaction name\n     */\n    name: string;\n  };\n\n  /**\n   * An array of variant units added to cart. This includes both variants added as new line items\n   * and quantity increases of existing line items.\n   *\n   * A product variant in this array does not contain all the properties of a cart item because we\n   * cannot trivially discern which lines changed when a cart updates.\n   */\n  variants_added: ProductVariant[];\n\n  /**\n   * Variants removed from cart. This can be removal of lines or quantity decreases.\n   *\n   * A product variant does not contain all the properties of a cart item because we cannot\n   * trivially discern which lines changed as a result of a state change.\n   */\n  variants_removed: ProductVariant[];\n}>;\n\n/**\n * Represents a product variant. A Shopify product has one or more variants. A Shopify product with\n * no explicitly configured variant still has a default variant.\n *\n * This should not be confused with a canonical product variant that exists in a single line item in\n * a cart object. This is an amalgamation, a composition of the variant as it exists in one or more\n * line items in a cart. This assumes that price and other properties are the same, apart from the\n * special merged_properties property, which combines the line item properties of the lines\n * containing the variant together.\n */\ninterface ProductVariant {\n  /**\n   * Path to variant image\n   *\n   * @example \"//langehair.com/cdn/shop/products/filename.jpg?v=1680740790\"\n   */\n  image_url: string;\n\n  /**\n   * The combined line item properties of the line items containing this variant\n   *\n   * The key of this object is the property name.\n   *\n   * The values for each key in this object are the distinct values that were merged together.\n   */\n  merged_properties: Record<string, string[]>;\n\n  price: number;\n  product_id: number;\n  product_title: string;\n  product_type: string;\n  quantity: number;\n  sku: string;\n\n  /**\n   * Product variant url (relative)\n   *\n   * @example \"/products/salt-sea-texturizing-spray?variant=41345560477796\"\n   */\n  url: string;\n\n  variant_id: number;\n  variant_title: string;\n}\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    'cart-updated': CartUpdatedEvent;\n  }\n}\n\n/**\n * Sets the current state of the cart. This is the in memory state that is stored as the value of a\n * global variable, not the server side state.\n *\n * In memory state is PER TAB PER WINDOW/APP. If someone is browsing in multiple tabs concurrently\n * the state may conflict. The cart token corresponds to a server side cart id and is stored in a\n * cookie that is shared among all tabs, so it is possible for multiple tabs to refer to the same\n * cart object.\n *\n * Always use this method to update the state of the global cart variable. Do not update the state\n * of the global cart variable directly.\n *\n * Throws an error if the given cart is not a cart. This also indicates a more insidious problem\n * where there is no longer a guarantee that client side state reflects server side state, so\n * consider some error handling.\n *\n * This saves a copy of the cart so that future updates by reference to the cart object do not\n * modify the global cart state.\n *\n * Emit a cart updated event that contains some of the differences. This emits the updated event\n * even when there are no changes detected.\n *\n * Any update can result in unrelated cart changes. Changing one line's properties could cause line\n * merging. Also, prior state comes from page state, which is per tab, so interactions in multiple\n * tabs can produce strange behavior.\n *\n * The dispatcher is the target the updated event is dispatched from. It is required but may be\n * null, and it may be any event target, not necessarily an element or window. The target is used as\n * given, with two exceptions that exist only to keep the event reaching window level listeners: a\n * null target dispatches from window, and so does a detached element. A detached element only\n * bubbles up to its detached subtree root, which is not window, so dispatching from it directly\n * would not reach those listeners. Callers should prefer the element most directly responsible for\n * the change, so a connected element carries truthful provenance and bubbles, crossing shadow\n * boundaries, up to the global listeners. Genuinely ambient updates with no originating element,\n * such as init or a cross tab sync, pass window (or null).\n */\nexport function write(dispatcher: EventTarget | null, cart: Cart, transaction: Transaction,\n  scenario?: string, initial = false) {\n  const previous = initial ? null : read();\n\n  // Update global state.\n  assert(isCart(cart), 'Cannot write invalid cart');\n  cart_lifecycle.cart = copy(cart);\n\n  // Prepare for diffing. We group by variant because line keys change in unexpected ways.\n  const oldVariants = groupByVariant(previous);\n  const newVariants = groupByVariant(cart);\n\n  // For discount code deltas, we only examine deltas in codes that both exist and are in the\n  // applicable state in the cart's discount_codes array.\n\n  const prevCodes = getApplicableDiscountCodes(previous);\n  const currCodes = getApplicableDiscountCodes(cart);\n\n  // Emit an event with diffs\n\n  type Detail = CartUpdatedEvent['detail'];\n  const event = new CustomEvent<Detail>('cart-updated', {\n    detail: {\n      cart,\n      discount_codes_added: complement(currCodes, prevCodes),\n      discount_codes_removed: complement(prevCodes, currCodes),\n      event_id: uuidv4(),\n      is_initial: initial,\n      previous,\n      scenario,\n      variants_added: findVariantsAdded(oldVariants, newVariants),\n      variants_removed: findVariantsRemoved(oldVariants, newVariants),\n      transaction: {\n        id: transaction.id,\n        name: transaction.name\n      }\n    },\n    bubbles: true,\n    composed: true\n  });\n\n  // Dispatch the update from whatever target the caller designates; this forms no opinion about\n  // what kind of target it is. The only special handling exists to keep the event reaching the\n  // global listeners: a detached element has no live ancestor chain to bubble through, so it is\n  // swapped for the global scope, and a null target means the global scope outright. Any other\n  // target, including a connected element, is used as given, so it carries truthful provenance and\n  // bubbles, crossing shadow boundaries, up to the global listeners.\n\n  let target: EventTarget;\n  if (dispatcher instanceof Element) {\n    if (dispatcher.isConnected) {\n      target = dispatcher;\n    } else {\n      target = window;\n    }\n  } else if (dispatcher) {\n    target = dispatcher;\n  } else {\n    target = window;\n  }\n\n  target.dispatchEvent(event);\n}\n\n/**\n * Returns a copy of the global cart state.\n *\n * Throws an error if the global cart object is not a cart object.\n */\nexport function read() {\n  assert(isCart(cart_lifecycle.cart), 'Global cart state is invalid on read');\n  return copy(cart_lifecycle.cart);\n}\n\ninterface FetchOptions {\n  /**\n   * The total number of requests to make, counting the first one.\n   */\n  attempts?: number;\n\n  /**\n   * How long to wait in milliseconds before each retry, one entry per retry.\n   */\n  delays?: number[];\n\n  /**\n   * How long in milliseconds to wait on a single attempt before counting it as failed. Leaving it\n   * out means each attempt waits as long as the request takes.\n   */\n  timeout?: number;\n}\n\n/**\n * Reads the server side cart state.\n *\n * This wraps the ajax api call of the same name and is what cart code here should reach for\n * instead. The only things it adds are a few attempts and an optional time limit on each one, so it\n * behaves the same otherwise.\n *\n * A cart read is idempotent and it sits in the middle of things the shopper is waiting on, like\n * adding to cart, so a single failed request is worth another go rather than turning into an error\n * the shopper sees. We make up to a few attempts, waiting a little longer before each retry. If\n * every attempt fails then the error from the last one is thrown and the caller decides what that\n * means. Some callers treat it as fatal, some stay quiet about it, so this forms no opinion.\n *\n * Do not use this to react to a mutation you just made unless you are fine with a repeat request,\n * because each attempt sends a fresh one.\n *\n * Callers can pass options to change how many attempts are made, how long to wait between them, and\n * how long a single attempt gets before it counts as failed. Leaving them out gets the defaults\n * below, and no time limit.\n *\n * There is no limit on the total time across all attempts. The caller is responsible for picking\n * options that keep the worst case acceptable, which is every attempt using its full time limit\n * plus every wait between them.\n */\nexport async function cartFetch(options: FetchOptions = {}) {\n  // How many times we are willing to ask the server before giving up. The first attempt counts, so\n  // this is the total number of requests and not the number of retries on top of one.\n  const limit = options.attempts || 3;\n\n  // How long we wait before each retry. Shopify throttles cart reads, so going right back at it\n  // just piles more requests onto something that already told us to slow down. These stay short as\n  // a balance between waiting long enough to avoid another 429 and waiting so long the user\n  // perceives the UI as broken.\n  const delays = options.delays || [100, 250];\n\n  let lastError: Error;\n\n  for (let attempts = 0; attempts < limit; attempts++) {\n    if (attempts > 0) {\n      const delay = delays[attempts - 1];\n      await resolveAfterTimeout(delay);\n    }\n\n    try {\n      const cart = await cartFetchOnceWithTimeout(options.timeout);\n      return cart;\n    } catch (error) {\n      lastError = error;\n    }\n  }\n\n  throw lastError;\n}\n\n/**\n * Thrown when a single cart read takes longer than its time limit. Callers can check for it to tell\n * a hung read apart from one that failed outright.\n */\nexport class CartFetchTimeoutError extends Error {\n  public declare timeout: number;\n\n  constructor(timeout: number) {\n    super('Timed out fetching cart');\n    this.name = this.constructor.name;\n    this.timeout = timeout;\n  }\n}\n\n/**\n * Reads the cart once, giving up after the given number of milliseconds. Leaving out the timeout\n * means there is no limit.\n *\n * Giving up does not stop the request. It keeps going in the background and whatever it eventually\n * returns is ignored. That is fine because reading the cart changes nothing, but it means this must\n * never be used for a cart write, since a write that times out here can still land later.\n */\nasync function cartFetchOnceWithTimeout(timeout: number) {\n  if (!timeout) {\n    return cartFetchOnce();\n  }\n\n  let timer: ReturnType<typeof setTimeout>;\n  const timeoutPromise = new Promise<never>((_resolve, reject) => {\n    const error = new CartFetchTimeoutError(timeout);\n    timer = setTimeout(reject, timeout, error);\n  });\n\n  try {\n    const cart = await Promise.race([cartFetchOnce(), timeoutPromise]);\n    return cart;\n  } finally {\n    clearTimeout(timer);\n  }\n}\n\n/**\n * Returns a promise which resolves after the given timeout\n */\nfunction resolveAfterTimeout(milliseconds: number) {\n  return new Promise(resolve => {\n    setTimeout(resolve, milliseconds);\n  });\n}\n\ntype ProductVariantMap = Record<string, ProductVariant>;\n\n/**\n * Aggregates cart line items by variant. Returns the aggregated variants as an object where each\n * key of the object is a variant id and each value is the corresponding variant object.\n *\n * This does some complex stuff. One key thing to constantly point out is that a variant can appear\n * in more than one line item in a cart. There are several reasons why. Shopify generally refers to\n * this as a variant split. Variant splits happen for reasons such as having different line item\n * properties, or different prices as a result of allocated discount applications, or because of\n * more rare reasons like product subscriptions. Because we are grouping by variant id, we have to\n * invent some novel ways of combining the different values of the line items containing the same\n * variant.\n *\n * To combine the line item properties, we invent a new property, merged_properties, that contains\n * the properties of each line item. In each merged properties value, there is an array of values.\n * When appending to the values array, we only append unique values (using exact value comparison).\n *\n * There are many downstream concerns. Here is one example. When relaying an add to cart event to a\n * third party analytics system, we sometimes want the product's image url. This function's output\n * is the input to the variants_added array of the cart updated event emitted as a result of a cart\n * write. So we have to embed the image url here in the variant object so that it can be accessed\n * without having to do some kind of later lookup.\n *\n * @param cart the input cart to aggregate\n */\nfunction groupByVariant(cart: Cart) {\n  const output: ProductVariantMap = {};\n\n  // It is important to return null here, and not the empty object, so that later functions that use\n  // this output can trivially distinguish between transitions from null state and transitions from\n  // empty cart state. The transition from null state happens on init.\n\n  if (!cart) {\n    return null;\n  }\n\n  for (const item of cart.items) {\n    const variant = output[item.variant_id.toString()];\n    if (variant) {\n      // Merge the item's properties into the variant's properties.\n      for (const property in item.properties || {}) {\n        variant.merged_properties[property] = variant.merged_properties[property] || [];\n        const value = item.properties[property];\n        if (!variant.merged_properties[property].includes(value)) {\n          variant.merged_properties[property].push(value);\n        }\n      }\n\n      variant.quantity += item.quantity;\n    } else {\n      // Merge in the initial properties, where each value is a one value array.\n      const properties: Record<string, string[]> = {};\n      for (const key in item.properties || {}) {\n        properties[key] = [item.properties[key]];\n      }\n\n      output[item.variant_id.toString()] = {\n        image_url: item.image,\n        merged_properties: properties,\n        // eslint-disable-next-line @typescript-eslint/no-deprecated\n        price: item.price,\n        product_id: item.product_id,\n        product_title: item.product_title,\n        product_type: item.product_type,\n        quantity: item.quantity,\n        sku: item.sku,\n        url: item.url,\n        variant_id: item.variant_id,\n        variant_title: item.variant_title\n      };\n    }\n  }\n\n  return output;\n}\n\n/**\n * Returns an array of variant data objects that were added as a result of the write mutation.\n *\n * This tolerates nulls\n *\n * @param c0map variants in old cart state\n * @param c1map variants in new cart state\n * @returns variants added array\n */\nfunction findVariantsAdded(c0map: ProductVariantMap, c1map: ProductVariantMap) {\n  // When the cart transitions from the null state, there are no previous variants. This means that\n  // all variants in the new state have been added. Transitioning from null can mean multiple\n  // things. It can mean either that cart state was initialized on page load, or that cart state was\n  // recovered from error. Because of the ambiguity we are not emitting adds for transitions from\n  // the null state.\n\n  if (!c0map) {\n    return [];\n  }\n\n  // Figure out which variants had quantity increases in any of the line items containing those\n  // variants. Any variant that had a quantity increase is referred to here as an increment. In\n  // addition, look for any variants that are present in the new state but not in the old state, in\n  // which case their full quantity is the quantity added.\n\n  const increments: ProductVariantMap = {};\n  for (const key in c1map) {\n    if (c0map[key]) {\n      // In this branch, the variant was present in the old map and is present also in the new map.\n\n      if (c1map[key].quantity > c0map[key].quantity) {\n        increments[key] = copy(c1map[key]);\n        increments[key].quantity = c1map[key].quantity - c0map[key].quantity;\n      } else {\n        // ignore 0 quantity change and negative quantity change (decrements).\n      }\n    } else {\n      // In this branch, the variant is present in the new map but not in the old map.\n\n      increments[key] = copy(c1map[key]);\n    }\n  }\n\n  return Object.keys(increments).map(key => increments[key]);\n}\n\nfunction findVariantsRemoved(c0map: ProductVariantMap, c1map: ProductVariantMap) {\n  if (!c0map) {\n    return [];\n  }\n\n  const decrements: ProductVariantMap = {};\n  for (const key in c0map) {\n    if (c1map[key]) {\n      if (c1map[key].quantity < c0map[key].quantity) {\n        decrements[key] = copy(c1map[key]);\n        decrements[key].quantity = c0map[key].quantity - c1map[key].quantity;\n      } else {\n        // ignore 0 quantity change\n      }\n    } else {\n      // if the old variant is not found in the updated cart, all quantity was removed\n      decrements[key] = copy(c0map[key]);\n    }\n  }\n\n  return Object.keys(decrements).map(key => decrements[key]);\n}\n\n/**\n * Return all values present in array a that are not present in array b.\n *\n * The complement of a set is the set that includes all the elements of the universal set that are\n * not present in the given set. Here, a is the universal set and b is the given set.\n */\nfunction complement(a: string[], b: string[]) {\n  assert(Array.isArray(a), 'complement parameter \"a\" is not an array');\n  assert(Array.isArray(b), 'complement parameter \"b\" is not an array');\n  return a.filter(value => !b.includes(value));\n}\n","import { measure, now } from '../perf.js';\n\ntype CartTransactionStartedEvent = CustomEvent<Transaction>;\ntype CartTransactionCompletedEvent = CustomEvent<Transaction>;\n\n/**\n * The bookkeeping shared by every copy of this module. Each entry bundles its own copy, so the\n * numbers and the timeline have to sit on the window for the copies to agree on them.\n */\ninterface State {\n  /** The number of currently active cart service transactions. */\n  activeCount: number;\n\n  /** Number of cart operations started so far, and the source of transaction ids. */\n  count: number;\n\n  /** A rolling log of recent transaction start and end entries. */\n  timeline: Record<string, any>[];\n}\n\ndeclare global {\n  var cart_transaction: State;\n\n  interface WindowEventMap {\n    'cart-transaction-completed': CartTransactionCompletedEvent;\n    'cart-transaction-started': CartTransactionStartedEvent;\n  }\n}\n\n/**\n * Obtain the shared state, initializing it as needed.\n *\n * We do not assume the global state is reliably present and instead access state via this function\n * every time because we have seen very surprising errors in the wild to which we wish to remain\n * resilient, even at the cost of generating artificial and inaccurate state.\n */\nfunction getSharedState() {\n  window.cart_transaction = window.cart_transaction || { activeCount: 0, count: 0, timeline: [] };\n\n  const shared = window.cart_transaction;\n\n  if (!Number.isInteger(shared.activeCount) || shared.activeCount < 0) {\n    shared.activeCount = 0;\n  }\n\n  if (!Number.isInteger(shared.count)) {\n    shared.count = 0;\n  }\n\n  if (!Array.isArray(shared.timeline)) {\n    shared.timeline = [];\n  }\n\n  return shared;\n}\n\n/**\n * A transaction represents an action carried out by the cart service. For example, an add to cart\n * transaction or a remove from cart transaction.\n *\n * A transaction is a plain record minted by the start function and completed through the handle it\n * carries. The bookkeeping fields live on the record because completion is a module function that\n * operates on the record rather than a method that closes over it.\n */\nexport interface Transaction {\n  /** The arguments to the start of the transaction */\n  args: any[];\n\n  /** Complete this transaction. Idempotent, and reachable only through the handle. */\n  complete?(didTimeOut?: boolean): void;\n\n  /** Whether completion has already run, so a second call is a no-op */\n  completed: boolean;\n\n  /** The transaction start time in milliseconds */\n  epoch: number;\n\n  /** Transaction unique global id */\n  id: number;\n\n  /** Transaction name, e.g. function name */\n  name: string;\n\n  /**\n   * The transaction start time on the monotonic clock, used only to compute duration.\n   *\n   * This is separate from epoch, which stays wall-clock so it still answers when the transaction\n   * happened. The two clocks have different origins, so never mix them in one subtraction.\n   */\n  startMillis: number;\n\n  /** Timeout timer id */\n  timer?: ReturnType<typeof setTimeout>;\n}\n\n/**\n * Whether any cart transaction is currently in flight. A transaction stops counting once it\n * completes or times out, so this cannot stay true for longer than the transaction timeout.\n *\n * Interface elements that are created partway through a transaction never saw its started event, so\n * they have to ask this when they connect. The add to cart button relies on it to stay disabled\n * when it is rebuilt during an add. Changing what this reports, for example counting only some\n * kinds of transactions, changes when shoppers can tap add to cart. See issue 7022.\n */\nexport function isActive() {\n  return getSharedState().activeCount > 0;\n}\n\n/**\n * Start a cart service transaction and return its handle. Use the handle's complete method to end\n * the transaction.\n */\nexport function start(name: string, ...args: any[]) {\n  const shared = getSharedState();\n\n  // Increment the shared transaction id. A simple monotonic counter ensures that no two\n  // transactions have the same id (but see the caveats mentioned in getting shared state).\n\n  shared.count++;\n\n  const transaction: Transaction = {\n    args,\n    epoch: Date.now(),\n    id: shared.count,\n    name,\n    completed: false,\n    startMillis: now()\n  };\n\n  // Expose complete as a \"method\" of the returned object. Callers are expected to complete the\n  // transaction via its own method. This is better than exporting the top level complete function\n  // as a free function where callers could mistakenly complete the wrong transaction.\n\n  transaction.complete = (didTimeOut = false) => complete(transaction, didTimeOut);\n\n  // Increment the number of active transactions. This should be a transition from 0 to 1.\n\n  shared.activeCount++;\n\n  // If as a result of the increment there is now more than one active transaction, report the\n  // problem but continue. The goal is not to error. Many transactions are able to reconcile from\n  // overlapping flights.\n\n  if (shared.activeCount > 1) {\n    reportConcurrency(transaction);\n  }\n\n  // Record the transaction in the active transactions timeline. This is a fresh object instead of\n  // the original transaction so that it snapshots.\n\n  append({ epoch: transaction.epoch, id: transaction.id, name: transaction.name, type: 'start' });\n\n  // Start a timer that ensures the transaction settles eventually. Ensuring this is important\n  // because the upper layers typically lock down the UI during a transaction, and we want to\n  // prevent locking the UI indefinitely.\n\n  transaction.timer = setTimeout(complete, 30 * 1000, transaction, true);\n\n  console.log('Cart transaction started', transaction.name);\n\n  type Detail = CartTransactionStartedEvent['detail'];\n  const event = new CustomEvent<Detail>('cart-transaction-started', { detail: transaction });\n  dispatchEvent(event);\n\n  return transaction;\n}\n\n/**\n * Complete a transaction.\n *\n * Appends the transaction to the transaction history log.\n *\n * Emits a transaction completed event. Various transactions should complete when done to signal the\n * completion to listeners.\n *\n * This runs either manually whenever the transaction completes, or automatically, if the\n * transaction did not complete quickly enough (e.g. because of an error, omission, or a genuinely\n * long operation that timed out).\n *\n * Transactions should be short-lived. Transactions are automatically considered complete after a\n * certain amount of time so as to guarantee completion. Higher layer interfaces rely on transaction\n * state to lock interfaces for periods of time. Locking must be definite.\n *\n * Completion is idempotent. The timeout safety net and the caller's own completion can both fire\n * for one transaction when a tab is suspended long enough for the operation to outlive the timeout,\n * as happens on a backgrounded mobile tab that resumes. Whichever fires first records the outcome\n * and any later call is a no-op, so the active transaction count decrements exactly once per start\n * and cannot drift negative and defeat the interface locks that read it.\n */\nfunction complete(transaction: Transaction, didTimeOut = false) {\n  // A transaction that already timed out lands here when its operation finally finishes, so we log\n  // how long it really took. That tells a stalled request that eventually came back apart from one\n  // that never did.\n\n  if (transaction.completed) {\n    const duration = Math.round(now() - transaction.startMillis);\n    console.log('Cart transaction already completed', transaction.name, didTimeOut, duration);\n    return;\n  }\n\n  transaction.completed = true;\n\n  clearTimeout(transaction.timer);\n  transaction.timer = undefined;\n\n  append({\n    epoch: Date.now(),\n    id: transaction.id,\n    name: transaction.name,\n    timeout: didTimeOut,\n    type: 'end'\n  });\n\n  const shared = getSharedState();\n\n  if (shared.activeCount > 0) {\n    shared.activeCount--;\n  }\n\n  if (didTimeOut) {\n    console.log('Cart transaction timed out', transaction.name);\n  }\n\n  console.log('Cart transaction completed', transaction.name);\n\n  // Capture performance data representing each transaction's duration.\n\n  measure(`cart-${transaction.name}-${transaction.id}`, transaction.startMillis, now(), {\n    id: transaction.id,\n    name: transaction.name,\n    timeout: didTimeOut\n  });\n\n  type Detail = CartTransactionCompletedEvent['detail'];\n  const event = new CustomEvent<Detail>('cart-transaction-completed', { detail: transaction });\n  dispatchEvent(event);\n}\n\n/**\n * Capture a concurrent transaction in a way that observers can see it\n */\nfunction reportConcurrency(transaction: Transaction) {\n  const state = getSharedState();\n\n  // Snapshot the state. The reason for this is because the state that is rendered in the devtools\n  // console is the state of the live reference when the value is viewed in the log, which is after\n  // the time it was recorded. Snapshotting saves the value at the time of recording it.\n  //\n  // This is a shallow snapshot. Since the timeline is append only, we are basically just recording\n  // the timeline up to the time of the snapshot, e.g. basically this is just a partial view of an\n  // array that may have more entries by the time it is expanded but those entries are hidden.\n\n  const snapshot = [...state.timeline];\n\n  console.warn('Concurrent transaction started', {\n    id: transaction.id,\n    name: transaction.name,\n    timeline: snapshot\n  });\n}\n\n/**\n * Add a timeline entry.\n *\n * The type of entry here is intentionally general because this has a relaxed opinion on what gets\n * stored.\n */\nfunction append(entry: Record<string, any>) {\n  const state = getSharedState();\n  const timeline = state.timeline;\n\n  timeline.push(entry);\n\n  const capacity = 50;\n\n  // Keep only the most recent entries so the timeline does not grow without bound over a long\n  // session. Removing from the front drops the oldest; the clamp guards against a negative removal\n  // count while the timeline is still under the cap.\n  // TODO: the clamp may be unnecessary, since splice already treats a negative delete count as\n  // zero, so it is likely defensive rather than required. Verify and drop it if so.\n  timeline.splice(0, Math.max(timeline.length - capacity, 0));\n}\n","import { type CartChangeInput, type Cart, cartChange, isUnavailableError } from '@integrabeauty/shopify-ajax-api';\nimport { digest } from './digest.js';\nimport { hasSimilarItems } from './equality.js';\nimport { report } from './error.js';\nimport { cartFetch, read, write } from './state.js';\nimport { start } from './transaction.js';\n\n/**\n * Change the cart's line item quantities, note, selling plan allocation or attributes.\n *\n * @param target the element most directly responsible for the change, used to originate any failure\n * event with truthful provenance instead of at global scope\n * @param input cart change input\n * @param scenario optional description or code describing the context in which this takes place\n *\n * @see https://shopify.dev/docs/api/ajax/reference/cart#post-locale-cart-change-js\n */\nexport async function change(target: EventTarget | null, input: CartChangeInput,\n  scenario?: string) {\n  const transaction = start('changeCart', input);\n\n  let previous: Cart;\n  let remoteCart: Cart;\n\n  try {\n    // It is possible that the client side cart state is stale, so we need to grab it in order to\n    // compare it to the remote state.\n    previous = read();\n\n    // Grab the remote state\n    remoteCart = await cartFetch();\n\n    if (!hasSimilarItems(previous, remoteCart)) {\n      // Update local state to reflect the latest remote state.\n      write(target, remoteCart, transaction, scenario);\n\n      // Define a custom error code when throwing this error so that calling code can extract a more\n      // specific error code.\n\n      const error: any = new Error('Cart state out of sync during changeCart');\n      error.error_code = 'CHANGE_ITEMS_DESYNC';\n      throw error;\n    }\n\n    await cartChange(input);\n    const cart = await cartFetch();\n    write(target, cart, transaction, scenario);\n  } catch (error) {\n    report(target, {\n      code: determineErrorCode(error),\n      error,\n      inputs: {\n        input,\n        previous_cart: digest(previous),\n        fetched_cart: digest(remoteCart)\n      },\n      scenario,\n      transaction\n    });\n  } finally {\n    transaction.complete();\n  }\n}\n\nfunction determineErrorCode(error: any) {\n  if (isUnavailableError(error)) {\n    return 'CHANGE_CART_OOS';\n  }\n\n  if ('error_code' in error) {\n    return error.error_code;\n  }\n\n  return 'CHANGE_CART';\n}\n","import { assert } from '@integrabeauty/assert';\nimport { type Cart, cartChange } from '@integrabeauty/shopify-ajax-api';\nimport { digest } from './digest.js';\nimport { hasSimilarItems } from './equality.js';\nimport { report } from './error.js';\nimport { cartFetch, read, write } from './state.js';\nimport { start } from './transaction.js';\n\n/**\n * Removes items from the cart.\n *\n * Each id in the array can be either a line key or a variant id. A cart can have multiple line\n * items that share the same variant id. For example, when variants have different line item\n * properties, or automatic discounts create variants at different prices. Because of this, it is\n * recommended to use the line item key to ensure that you're only changing the intended line item.\n *\n * If you want to also change other cart properties in the same call, use the more general\n * updateCart call instead while specifying lines with quantity 0 to effectively remove. This\n * removeItems call is just a convenient but limited way of doing a cart update.\n *\n * Even though Shopify permits specifying a variant id as a number, we require that it is a string\n * because we have seen strange, undocumented errors when specifying variant ids as numbers, such as\n * some kind of server side Ruby error with parsing numbers.\n *\n * @param target the element most directly responsible for the removal, used to originate any\n * failure event with truthful provenance instead of at global scope\n * @param lineKeys the line keys or variant ids of the items to remove\n * @param scenario optional description or code describing the context in which this takes place\n *\n * @see https://shopify.dev/docs/api/ajax/reference/cart#update-line-item-quantities\n */\nexport async function remove(target: EventTarget | null, lineKeys: string[], scenario?: string) {\n  const transaction = start('removeItems', lineKeys, scenario);\n  let errorCode = 'REMOVE_ITEMS';\n\n  let previous: Cart;\n  let remoteCart: Cart;\n\n  try {\n    assert(lineKeys.length > 0, 'At least one item must be specified');\n\n    // Even though shopify supports variant ids as change item ids as numbers, this function does\n    // not support using variant id. This function only operates on line keys, which must always be\n    // strings. Even though we have type safety checks, we still want a runtime check, as the\n    // calling code is typically less diligent.\n    assert(lineKeys.every(isString), 'One or more invalid line keys');\n\n    // Ensure the caller did not provide duplicates\n    const uniqueKeySet = new Set(lineKeys);\n    assert(uniqueKeySet.size === lineKeys.length, 'One or more duplicate line keys');\n\n    // cart/update supports removing multiple items at once, but cart/change only supports changing\n    // a single item at once. While it would be ideal to use cart/update, cart/update has an\n    // undocumented bug where attempting to set the quantity of an out of stock line item to 0\n    // results in a 422 error. We reached out to Shopify support, and they stated that we should use\n    // cart/change instead. So now removing multiple items at once gets complicated, because we have\n    // no real transactional safety. In fact, we should probably drop support for removing multiple\n    // items at once and fully migrate the use cases for removing multiple items to other solutions,\n    // e.g. use the Shopify cart transforms for bulk add/remove/update, such as what the Shopify\n    // bundles app does. In the short term we are going to attempt to support bulk removal by\n    // sequentially removing items.\n\n    // Removing multiple items at once, or at least attempting to support the invented abstraction\n    // of an atomic mutation on the cart that is actually a compound mutation, is further\n    // complicated by how cart/change behaves. Another undocumented feature of cart/change is that\n    // changing any one item's quantity to 0 can cause other line items to change their keys. The\n    // keys of other lines change in an undocumented manner. It is not clear if affecting a single\n    // line can change any other line, or only lines that have a later position. I think what is\n    // happening is that the keys of later lines shift when the quantity of an earlier line is\n    // changed to 0. So, the idea is that if we remove lines in reverse order, we avoid the issue\n    // with shifting keys per change.\n\n    // Clarified, removing any one line can impact the keys of all other lines. So we cannot remove\n    // by key in a batch, because each single removal affects the rest.\n\n    // The next problem then is that the calling code is not obligated to provide us line keys in\n    // natural cart order. So we have to find a way to get the natural order. In which case the next\n    // problem is that in order to figure that out we need the current cart state, which is not\n    // provided as input. So we have to get the current cart state and assume it is authoritative.\n\n    previous = read();\n\n    // Verify that the cart state is not stale by fetching the latest server side cart state prior\n    // to mutation.\n\n    remoteCart = await cartFetch();\n\n    if (!hasSimilarItems(previous, remoteCart)) {\n      // Update local state to reflect the latest remote state.\n      write(target, remoteCart, transaction, scenario);\n\n      errorCode = 'REMOVE_ITEMS_DESYNC';\n      throw new Error('Cart state out of sync during removeItems');\n    }\n\n    // Verify that the number of lines to remove is less than or equal to the number of items in the\n    // cart. It is possible that the visitor is looking at a stale cart state and then trying to\n    // remove one or more items from a stale cart state. One thing we can detect is that when the\n    // cart state is stale, there should still always be enough items in the cart to remove. This is\n    // somewhat redundant with a later assertion but it still gives us a more exact error.\n    assert(lineKeys.length <= previous.items.length, 'Too many line keys');\n\n    const lineIndicesToRemove = [];\n    for (let i = 0; i < previous.items.length; i++) {\n      const item = previous.items[i];\n      if (lineKeys.includes(item.key)) {\n        lineIndicesToRemove.push(i + 1);\n      }\n    }\n\n    assert(lineIndicesToRemove.length === lineKeys.length, 'One or more line keys not found');\n\n    // Reorder from last to first, as each removal changes the position of later ones\n    lineIndicesToRemove.reverse();\n\n    // Then perform the removals. We cannot perform the removals concurrently. We have to perform\n    // the removals sequentially as racing them concurrently carries the risk that the removals\n    // occur in the order we did not intend.\n\n    for (const line of lineIndicesToRemove) {\n      await cartChange({ line, quantity: 0 });\n    }\n\n    // Because we intentionally ignore the output of each cart/change, obtain the updated cart state\n    // through yet another request.\n\n    const cart = await cartFetch();\n    write(target, cart, transaction, scenario);\n  } catch (error) {\n    report(target, {\n      code: errorCode,\n      error,\n      inputs: {\n        line_keys: lineKeys,\n        previous_cart: digest(previous),\n        fetched_cart: digest(remoteCart)\n      },\n      scenario,\n      transaction\n    });\n  } finally {\n    transaction.complete();\n  }\n}\n\nfunction isString(value: any): value is string {\n  return typeof value === 'string';\n}\n","import { type CartUpdateInput, type Cart, cartUpdate, isUnavailableError } from '@integrabeauty/shopify-ajax-api';\nimport { digest } from './digest.js';\nimport { hasSimilarItems } from './equality.js';\nimport { report } from './error.js';\nimport { cartFetch, read, write } from './state.js';\nimport { start } from './transaction.js';\n\n/**\n * Update the cart's line item quantities, note, or attributes.\n *\n * @param target the element most directly responsible for the update, used to originate any failure\n * event with truthful provenance instead of at global scope\n * @param input cart update input\n * @param scenario optional description or code describing the context in which this takes place\n *\n * @see https://shopify.dev/docs/api/ajax/reference/cart#post-locale-cart-update-js\n */\nexport async function update(target: EventTarget | null, input: CartUpdateInput,\n  scenario?: string) {\n  const transaction = start('updateCart', input, scenario);\n\n  let previous: Cart;\n  let remoteCart: Cart;\n\n  try {\n    // It is possible that the client side cart state is stale, so we need to grab it in order to\n    // compare it to the remote state.\n    previous = read();\n\n    // Grab the remote state\n    remoteCart = await cartFetch();\n\n    if (!hasSimilarItems(previous, remoteCart)) {\n      // Update local state to reflect the latest remote state.\n      write(target, remoteCart, transaction, scenario);\n\n      // Define a custom error code when throwing this error so that calling code can extract a more\n      // specific error code.\n\n      const error: any = new Error('Cart state out of sync during updateCart');\n      error.error_code = 'UPDATE_ITEMS_DESYNC';\n      throw error;\n    }\n\n    await cartUpdate(input);\n    const cart = await cartFetch();\n    write(target, cart, transaction, scenario);\n  } catch (error) {\n    report(target, {\n      code: determineErrorCode(error),\n      error,\n      inputs: {\n        input,\n        previous_cart: digest(previous),\n        fetched_cart: digest(remoteCart)\n      },\n      scenario,\n      transaction\n    });\n  } finally {\n    transaction.complete();\n  }\n}\n\nfunction determineErrorCode(error: any) {\n  if (isUnavailableError(error)) {\n    return 'UPDATE_CART_OOS';\n  }\n\n  if ('error_code' in error) {\n    return error.error_code;\n  }\n\n  return 'UPDATE_CART';\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport type { CartChangeInput, CartUpdateInput } from '@integrabeauty/shopify-ajax-api';\nimport { change } from '../../../lib/cart/change.js';\nimport { remove } from '../../../lib/cart/remove.js';\nimport { read } from '../../../lib/cart/state.js';\nimport { update } from '../../../lib/cart/update.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders a cart line item, with the ability for the user to adjust quantities, and remove the\n * item.\n */\nclass CartLineItem extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return [\n      'data-is-available',\n      'data-max-quantity',\n      'data-line-item-id',\n      'data-line-price-after-discount',\n      'data-line-price',\n      'data-price',\n      'data-image-src',\n      'data-image-alt',\n      'data-title',\n      'data-quantity',\n      'data-variant-id',\n      'data-parent-id',\n      'data-product-handle',\n      'data-product-id',\n      'data-product-type',\n      'data-update',\n      'data-selling-plan-id',\n      'data-is-upsell'\n    ];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Country ISO code. Required.\n     *\n     * @example \"US\"\n     */\n    countryCode: string;\n\n    /**\n     * Currency ISO Code. Required\n     */\n    currencyCode: string;\n\n    /**\n     * Currency Symbol. Required\n     */\n    currencySymbol: string;\n\n    /**\n     * \"true\" on the gift with purchase line when there is more than one gift to pick from, which\n     * shows a Change link. Set by the cart when it builds the line.\n     */\n    giftChangeable: string;\n\n    /**\n     * Image alternate text.\n     */\n    imageAlt: string;\n\n    /**\n     * Image url.\n     */\n    imageSrc: string;\n\n    /**\n     * Whether the item has inventory that is sellable.\n     */\n    isAvailable: string;\n\n    /**\n     * Whether the item is a gift with purchase.\n     */\n    isGiftWithPurchase: string;\n\n    /**\n     * Boolean string indicating this is a parent line item with child items (e.g. GWP). Parent line\n     * items have controls for quantity and deletion, and child line items are visually indented\n     * with no quantity or delete controls because they are bound to the parent line item via the\n     * Shopify API.\n     */\n    isParent: 'false' | 'true';\n\n    /**\n     * Whether the item is an upsell item that qualifies for special pricing or logic.\n     */\n    isUpsell: string;\n\n    /**\n     * The id of the item in the cart\n     */\n    lineItemId: string;\n\n    /**\n     * The price before discounts. This could be either the MSRP (the compare at price) or it could\n     * be the marked down price (the sales price).\n     *\n     * This is for the entire line, not per unit, so this takes into account quantity.\n     */\n    linePrice: string;\n\n    /**\n     * This is the price after discounts. This is for the entire line, not per unit, so this also\n     * takes into account quantity.\n     */\n    linePriceAfterDiscount: string;\n\n    /**\n     * The maximum quantity of the item that can be added to cart. This controls whether certain\n     * features, such as a UI element for incrementing quantity, are enabled. Some items have a\n     * limitation that they can only be included once in a cart because they are offered at a\n     * reduced price under specific conditions.\n     */\n    maxQuantity: string;\n\n    /**\n     * If present, this line item is a child of another line item (e.g. GWP). The parentId is the\n     * Shopify line item key of the parent (format: \"variantId:propertiesHash\"). Child items have\n     * controls hidden because Shopify API automatically replicates parent actions (quantity\n     * changes, deletions) to all children.\n     */\n    parentId: string;\n\n    /**\n     * The price of the item (per unit)\n     */\n    price: string;\n\n    /**\n     * The Shopify url slug of the product in the line item.\n     */\n    productHandle: string;\n\n    /**\n     * The Shopify product id\n     */\n    productId: string;\n\n    /**\n     * The Shopify product type\n     *\n     * @example \"hair-care\"\n     */\n    productType: string;\n\n    /**\n     * A url for the PDP of the product.\n     */\n    productUrl: string;\n\n    /**\n     * Boolean string. Setting it to \"true\" pulses a faint outline on the product image once. The\n     * stylesheet does all of it, so removing the attribute and setting it again plays it again.\n     */\n    pulse: 'false' | 'true';\n\n    /**\n     * The number of items\n     */\n    quantity: string;\n\n    /**\n     * String representation of the selling plan. This is used to display the selling plan delivery\n     * frequency in the UI.\n     */\n    sellingPlanFrequency: string;\n\n    /**\n     * If the item has an associated selling plan, e.g. it is a subscription item, then this is the\n     * id of the related selling plan that has all of the specifics of the subscription.\n     */\n    sellingPlanId: string;\n\n    /**\n     * The line item display title, which is typically the product title.\n     */\n    title: string;\n\n    /**\n     * Setting this attribute to a new value triggers a render. This exists so that we can trivially\n     * change several attributes without triggering a render per attribute change, then set this one\n     * attribute to trigger a single render.\n     */\n    update: string;\n\n    /**\n     * The id of the variant represented by the cart line item\n     */\n    variantId: string;\n  };\n\n  private onCartTransactionStartedBound = this.onCartTransactionStarted.bind(this);\n  private onCartTransactionCompletedBound = this.onCartTransactionCompleted.bind(this);\n\n  private onSellingPlanChangedBound = this.onSellingPlanChanged.bind(this);\n  private onUpgradeButtonClickBound = this.onUpgradeButtonClick.bind(this);\n  private onRemoveLineItemClickBound = this.onRemoveLineItemClick.bind(this);\n  private onChangeGiftClickBound = this.onChangeGiftClick.bind(this);\n  private onQuantityChangedBound = this.onQuantityChanged.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    const removeButton = this.shadowRoot.querySelector<HTMLElement>('.remove-line-item');\n    removeButton.addEventListener('click', this.onRemoveLineItemClickBound);\n\n    const changeGiftButton = this.shadowRoot.querySelector<HTMLElement>('.change-gift');\n    changeGiftButton.addEventListener('click', this.onChangeGiftClickBound);\n\n    const quantity = this.shadowRoot.querySelector<HTMLSelectElement>('.quantity');\n    quantity.addEventListener('change', this.onQuantityChangedBound);\n\n    addEventListener('cart-transaction-started', this.onCartTransactionStartedBound);\n    addEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    const removeButton = this.shadowRoot.querySelector<HTMLElement>('.remove-line-item');\n    removeButton?.removeEventListener('click', this.onRemoveLineItemClickBound);\n\n    const changeGiftButton = this.shadowRoot.querySelector<HTMLElement>('.change-gift');\n    changeGiftButton?.removeEventListener('click', this.onChangeGiftClickBound);\n\n    const quantity = this.shadowRoot.querySelector<HTMLSelectElement>('.quantity');\n    quantity.removeEventListener('change', this.onQuantityChangedBound);\n\n    // These two live in our light DOM and only one of them is ever there, depending on whether the\n    // item is already a subscription. render() puts the listener on whichever one it finds, so we\n    // take it back off here to match.\n    const sellingPlanSelect = this.querySelector<HTMLSelectElement>('.selling-plan-options');\n    sellingPlanSelect?.removeEventListener('change', this.onSellingPlanChangedBound);\n\n    const upgradeButton = this.querySelector<HTMLButtonElement>('.upgrade-to-subscription');\n    upgradeButton?.removeEventListener('click', this.onUpgradeButtonClickBound);\n\n    removeEventListener('cart-transaction-started', this.onCartTransactionStartedBound);\n    removeEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n  }\n\n  public attributeChangedCallback(_name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && oldValue !== newValue) {\n      this.render();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onQuantityChanged(event: Event) {\n    console.debug('Cart line quantity changed');\n\n    if (!(event.target instanceof HTMLSelectElement)) {\n      return;\n    }\n\n    const quantity = parseInt(event.target.value, 10);\n\n    const updates = {\n      [this.dataset.lineItemId]: quantity\n    };\n\n    const previous = read();\n\n    // Child quantities are bound to parent via Shopify API, but we explicitly include them here for\n    // optimistic UI updates before the API response completes\n    for (const item of previous.items) {\n      if (item.parent_relationship?.parent_key === this.dataset.lineItemId) {\n        updates[item.key] = quantity;\n      }\n    }\n\n    const input: CartUpdateInput = {\n      updates\n    };\n    const scenario = 'cart-line-item-quantity-changed';\n    const dispatcher = this;\n    update(dispatcher, input, scenario).catch(console.warn);\n  }\n\n  private onChangeGiftClick(_event: MouseEvent) {\n    type Detail = GlobalEventHandlersEventMap['offer-view-requested']['detail'];\n    const event = new CustomEvent<Detail>('offer-view-requested', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        reason: 'change'\n      }\n    });\n    this.dispatchEvent(event);\n  }\n\n  private onRemoveLineItemClick(event: MouseEvent) {\n    console.debug('Cart line remove clicked', { trusted: event.isTrusted });\n\n    if (event.target instanceof HTMLButtonElement && !event.target.disabled) {\n      const lineKeys = [this.dataset.lineItemId];\n      const scenario = 'cart-line-item-remove-click';\n      const dispatcher = this;\n      remove(dispatcher, lineKeys, scenario).catch(console.warn);\n    }\n  }\n\n  private onCartTransactionStarted(_event: WindowEventMap['cart-transaction-started']) {\n    const actionElements = this.shadowRoot.querySelectorAll<HTMLButtonElement | HTMLSelectElement>(\n      '.cart-actions');\n    for (const actionElement of actionElements) {\n      actionElement.disabled = true;\n    }\n\n    const select = this.querySelector<HTMLSelectElement>('.selling-plan-options');\n    if (select) {\n      select.disabled = true;\n    }\n\n    const upgradeButton = this.querySelector<HTMLButtonElement>('.upgrade-to-subscription');\n    if (upgradeButton) {\n      upgradeButton.disabled = true;\n    }\n  }\n\n  private onCartTransactionCompleted(_event: WindowEventMap['cart-transaction-completed']) {\n    const actionElements = this.shadowRoot.querySelectorAll<HTMLButtonElement | HTMLSelectElement>(\n      '.cart-actions');\n    for (const actionElement of actionElements) {\n      actionElement.disabled = false;\n    }\n\n    const select = this.querySelector<HTMLSelectElement>('.selling-plan-options');\n    if (select) {\n      select.disabled = false;\n    }\n\n    const upgradeButton = this.querySelector<HTMLButtonElement>('.upgrade-to-subscription');\n    if (upgradeButton) {\n      upgradeButton.disabled = false;\n    }\n  }\n\n  private render() {\n    const removeButton = this.shadowRoot.querySelector<HTMLElement>('.remove-line-item');\n\n    const lineItemEl = this.shadowRoot.querySelector('.line-item');\n    lineItemEl.id = this.dataset.variantId;\n\n    const primaryImageElement =\n      this.shadowRoot.querySelector<HTMLElementTagNameMap['image-element']>('#image');\n\n    if (!primaryImageElement.dataset.src) {\n      primaryImageElement.dataset.alt = '';\n      primaryImageElement.dataset.src = this.dataset.imageSrc;\n    }\n\n    // Child items (e.g. GWP) use smaller images to create visual hierarchy\n    if (this.dataset.parentId) {\n      primaryImageElement.dataset.width = '90';\n    }\n\n    // Possibly 'undefined' as a string type when passed an undefined value via data attribute\n    if (!this.dataset.imageSrc || this.dataset.imageSrc === 'undefined') {\n      console.warn('Product missing image', this.dataset.variantId, this.dataset.title);\n    }\n\n    if (this.dataset.productUrl) {\n      const featuredImageAnchor = this.shadowRoot.querySelector('clickable-area');\n      if (featuredImageAnchor) {\n        featuredImageAnchor.dataset.url = this.dataset.productUrl;\n      }\n    }\n\n    const lineItemTitle = this.shadowRoot.querySelector<HTMLAnchorElement>('#title');\n\n    if (this.dataset.productUrl) {\n      lineItemTitle.href = this.dataset.productUrl;\n    } else {\n      lineItemTitle.title = 'This product does not have a page to view';\n    }\n\n    // Although we can use textContent to assign the title text to the anchor element, we\n    // intentionally use innerHTML to ensure any HTML entities (i.e. &amp;) are decoded.\n    lineItemTitle.innerHTML = this.dataset.title;\n\n    const linePriceAfterDiscountEl =\n      this.shadowRoot.querySelector<HTMLElement>('#line-price-after-discount');\n    linePriceAfterDiscountEl.dataset.currencyCode = this.dataset.currencyCode;\n    linePriceAfterDiscountEl.dataset.currencySymbol = this.dataset.currencySymbol;\n    linePriceAfterDiscountEl.dataset.subunits = this.dataset.linePriceAfterDiscount.toString();\n\n    // element with id line-price is the \"compare at price\"\n    const linePriceEl = this.shadowRoot.querySelector<HTMLElement>('#line-price');\n    linePriceEl.dataset.currencyCode = this.dataset.currencyCode;\n    linePriceEl.dataset.currencySymbol = this.dataset.currencySymbol;\n    linePriceEl.dataset.subunits = this.dataset.linePrice.toString();\n\n    if (parseInt(this.dataset.linePrice, 10) >\n      parseInt(this.dataset.linePriceAfterDiscount, 10)) {\n      this.shadowRoot.querySelector('.price').classList.remove('no-discount');\n      linePriceEl.dataset.ariaLabelPrefix = 'Regular price:';\n      linePriceAfterDiscountEl.dataset.ariaLabelPrefix = 'Sale price:';\n    } else {\n      this.shadowRoot.querySelector('.price').classList.add('no-discount');\n      linePriceEl.dataset.ariaLabelPrefix = '';\n      linePriceAfterDiscountEl.dataset.ariaLabelPrefix = 'Price:';\n    }\n\n    if (this.dataset.isAvailable === 'false') {\n      this.shadowRoot.querySelector<HTMLElement>('#out-of-stock').hidden = false;\n    }\n\n    if (this.dataset.sellingPlanId) {\n      // if this is a subscription item handle changing the selling plan\n      const select = this.querySelector<HTMLSelectElement>('.selling-plan-options');\n      if (select) {\n        select.addEventListener('change', this.onSellingPlanChangedBound);\n      }\n    } else {\n      // if not a subscription but selling plans are available - allow upgrading to a subscription\n      const upgradeButton = this.querySelector<HTMLButtonElement>('.upgrade-to-subscription');\n      if (upgradeButton) {\n        upgradeButton.addEventListener('click', this.onUpgradeButtonClickBound);\n      }\n    }\n\n    // Child items have quantity/delete controls hidden - Shopify API replicates all parent actions\n    // (quantity changes, deletions) to children automatically\n    if (this.dataset.parentId) {\n      const controlsRow = this.shadowRoot.querySelector<HTMLElement>('.line-item-bottom');\n      controlsRow.hidden = true;\n      return;\n    }\n\n    removeButton?.setAttribute('aria-label', ['remove', this.dataset.title, 'from cart'].join(' '));\n\n    this.renderQuantity();\n  }\n\n  /**\n   * When a user clicks the upgrade button, this sets the selling plan of the line item to the one\n   * specified in the button's selling plan id attribute.\n   *\n   * The upgrade button is not part of this element's template. The cart element creates it, sets\n   * data-selling-plan-id (the plan to upgrade to, which is not the same thing as our own data-\n   * selling-plan-id), and slots it into our light DOM. We read that attribute back here, hence the\n   * untyped dataset access. The comment where the parent sets it has the why.\n   *\n   * The parent creates the button as an HTMLButtonElement, and we read the current target rather\n   * than the target, so the instanceof check should always pass.\n   *\n   * Event listeners cannot be async, and promises must have explicit error handling. The underlying\n   * changeCart call emits errors via a side-channel error event instead of throwing. Therefore we\n   * basically ignore the error in the promise chain since it only happens in extreme edge cases.\n   */\n  private onUpgradeButtonClick(event: MouseEvent) {\n    if (event.currentTarget instanceof HTMLButtonElement) {\n      const input: CartChangeInput = {\n        id: this.dataset.lineItemId,\n        // Shopify resets the line to a quantity of 1 when the change leaves quantity out\n        quantity: parseInt(this.dataset.quantity, 10),\n        selling_plan: parseInt(event.currentTarget.dataset.sellingPlanId, 10)\n      };\n      const scenario = 'cart-line-item-upgrade-click';\n      const dispatcher = this;\n      change(dispatcher, input, scenario).catch(console.warn);\n    }\n  }\n\n  /**\n   * There is no more specific type of event for change events, so Event is the correct type. React\n   * has a virtual ChangeEvent type but that is not applicable here.\n   */\n  private onSellingPlanChanged(event: Event) {\n    // Documenting a previous bug for posterity. This previously was hard casting event.target to\n    // HTMLOptionElement. That is incorrect because change events are always fired on the select\n    // element, not its options. It was obscured because both HTMLSelectElement and\n    // HTMLOptionElement have a value property. Second, this now uses event.currentTarget instead of\n    // event.target. Both happened to be the same select here (the listener is on the select, and\n    // change does not bubble from the options), so this was not a live bug, but currentTarget is\n    // the more robust expression of intent: it is by definition the element the listener is bound\n    // to. Third, we prefer a runtime check over a hard cast precisely because it avoids this class\n    // of bugs.\n\n    if (event.currentTarget instanceof HTMLSelectElement) {\n      const input: CartChangeInput = {\n        id: this.dataset.lineItemId,\n        quantity: parseInt(this.dataset.quantity, 10),\n        selling_plan: parseInt(event.currentTarget.value, 10)\n      };\n      const scenario = 'cart-line-item-selling-plan-changed';\n      const dispatcher = this;\n\n      // We intentionally omit error handling here because the underlying cart module will handle\n      // it.\n      change(dispatcher, input, scenario).catch(console.warn);\n    }\n  }\n\n  private renderQuantity() {\n    const quantityElement = this.shadowRoot.querySelector<HTMLSelectElement>('.quantity');\n\n    if (this.dataset.isUpsell === 'true') {\n      // Only one item of an upsell product can be added, so no need to render the quantity selector\n\n      quantityElement.hidden = true;\n      return;\n    }\n\n    if (this.dataset.isAvailable === 'false') {\n      quantityElement.disabled = true;\n    }\n\n    // Since switching the quantity control to a select element, we should have predefined options\n    // in the HTML. For now, we've agreed to let users add up to 5 items, but this is negotiable.\n    const defaultMaxQuantity = 10;\n    const quantity = parseInt(this.dataset.quantity, 10);\n\n    let maxQuantity = defaultMaxQuantity;\n    if (this.dataset.maxQuantity) {\n      maxQuantity = parseInt(this.dataset.maxQuantity, 10);\n    }\n\n    if (maxQuantity < quantity) {\n      maxQuantity = quantity;\n    }\n\n    if (maxQuantity !== defaultMaxQuantity) {\n      // Currently, we set data-max-quantity to a value of 1 for the cart line item only for upsell\n      // products. That means in most cases, we will be rendering the default options, so this check\n      // prevents updating the DOM if not needed.\n\n      let optionsHtml = '';\n      for (let i = 1; i <= maxQuantity; i++) {\n        optionsHtml += `<option value=\"${i}\">${i}</option>`;\n      }\n\n      quantityElement.innerHTML = optionsHtml;\n    }\n\n    quantityElement.value = this.dataset.quantity;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'cart-line-item': CartLineItem;\n  }\n}\n\nif (!customElements.get('cart-line-item')) {\n  customElements.define('cart-line-item', CartLineItem);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { getEventQueue, isCartUpdatedEvent, isCartUpdateErredEvent } from '../../../lib/cart-event-queue.js';\nimport styles from './index.scss';\n\ntype CartUpdatedEvent = GlobalEventHandlersEventMap['cart-updated'];\ntype CartUpdateErredEvent = GlobalEventHandlersEventMap['cart-update-erred'];\ntype ProductVariant = CartUpdatedEvent['detail']['variants_added'][number];\n\n/**\n * Renders cart messages by building and appending alert-element elements.\n */\nclass CartMessages extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-clear'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Set from outside, such as when the drawer opens, to clear the messages. Setting it again\n     * clears them again, even with the same value. Removing it clears nothing.\n     */\n    clear: string;\n  };\n\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n  private onCartUpdateErredBound = this.onCartUpdateErred.bind(this);\n  private initErrorAlert: HTMLElement;\n\n  constructor() {\n    super();\n    this.attachShadow({ mode: 'open' });\n    this.shadowRoot.innerHTML = `<style>${styles}</style>`;\n  }\n\n  public connectedCallback() {\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      } else if (isCartUpdateErredEvent(event)) {\n        try {\n          this.onCartUpdateErredBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n    addEventListener('cart-update-erred', this.onCartUpdateErredBound);\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n    removeEventListener('cart-update-erred', this.onCartUpdateErredBound);\n  }\n\n  public attributeChangedCallback(name: string, _oldValue: string, newValue: string) {\n    if (this.isConnected && name === 'data-clear' && newValue !== null) {\n      this.onClearRequested();\n    }\n  }\n\n  /**\n   * The messages describe what just happened, so they are stale once the shopper has moved on. The\n   * init error stays, because the cart is still not loaded and its button is the only way to retry\n   * without reloading the page.\n   */\n  private onClearRequested() {\n    const alerts = this.shadowRoot.querySelectorAll('alert-element');\n    for (const alert of alerts) {\n      if (alert !== this.initErrorAlert) {\n        alert.remove();\n      }\n    }\n  }\n\n  private onCartUpdateErred(event: CartUpdateErredEvent) {\n    // Clear any previous messages\n    const alerts = this.shadowRoot.querySelectorAll('alert-element');\n    for (const alert of alerts) {\n      alert.remove();\n    }\n\n    const code = event.detail.code;\n\n    // We don't check whether the shopper has cookies turned off, since we don't trust the browser's\n    // answer enough to show an error over it. A shopper with cookies blocked or unsupported\n    // probably ends up here with the generic init error instead.\n    if (code === 'INIT' || code === 'INIT_TIMEOUT') {\n      this.showInitErrorMessage();\n    }\n\n    if (code === 'ADD_ITEMS') {\n      this.showErrorMessage('Oh no! There was a problem adding the items to your bag. ' +\n        'Please try again in a few moments.');\n    }\n\n    if (code === 'ADD_ITEMS_OOS') {\n      this.showErrorMessage('Oh no! There was a problem adding the item(s) to your bag. ' +\n        'One or more of the products is out of stock.');\n    }\n\n    if (code === 'ADD_ITEMS_BUNDLE_ADDON_MIX') {\n      this.showErrorMessage('Oh no! There was a problem adding the item(s) to your bag. ' +\n        'A bundle and an add-on cannot be added together. Try adding them one at a time.');\n    }\n\n    if (code === 'ADD_ITEMS_MAX_QUANTITY') {\n      this.showErrorMessage('Oh no! There was a problem adding the item(s) to your bag. ' +\n        'The maximum quantity of this item is already in your cart.');\n    }\n\n    if (code === 'ADD_ITEMS_INVALID_SELLING_PLAN') {\n      this.showErrorMessage('Oh no! There was a problem adding the item(s) to your bag. ' +\n        'One or more of the products is not available for subscription.');\n    }\n\n    if (code === 'ADD_ITEMS_INVALID_BACKORDER_SELLING_PLAN') {\n      this.showErrorMessage('Oh no! There was a problem adding the item(s) to your bag. ' +\n        'The pre-order for one or more of the products is no longer available. ' +\n        'Try refreshing the page.');\n    }\n\n    if (code === 'ADD_ITEMS_INVALID_TBYB_SELLING_PLAN') {\n      this.showErrorMessage('Oh no! There was a problem adding the item(s) to your bag. ' +\n        'One or more of the products is no longer available to try before you buy. ' +\n        'Try refreshing the page.');\n    }\n\n    if (code === 'REMOVE_ITEMS' || code === 'REMOVE_ITEMS_DESYNC') {\n      this.showErrorMessage('Oh no! Something went wrong removing items from your bag! ' +\n        'Try refreshing the page and trying again.');\n    }\n\n    if (code === 'UPDATE_CART' || code === 'UPDATE_ITEMS_DESYNC' || code === 'CHANGE_CART' ||\n      code === 'CHANGE_ITEMS_DESYNC') {\n      this.showErrorMessage('Oh no! Something went wrong updating items in your bag! ' +\n        'Try refreshing the page and trying again.');\n    }\n\n    if (code === 'UPDATE_CART_OOS' || code === 'CHANGE_CART_OOS') {\n      this.showErrorMessage('Oh no! Something went wrong updating items in your bag! ' +\n        'One or more products are out of stock.');\n    }\n\n    // We only show a cart level error when the cart had no discount codes before and the code was\n    // not entered in the discount input. Otherwise the <discount-input> custom element shows its\n    // own message.\n    if (code === 'APPLY_DISCOUNTS' && Array.isArray(event.detail.inputs?.before_codes) &&\n      event.detail.inputs.before_codes.length === 0 &&\n      event.detail.scenario !== 'cart-discount-input') {\n      this.showErrorMessage('Oh no! Something went wrong applying your discount code.');\n    }\n\n    if (code === 'REMOVE_DISCOUNT') {\n      this.showErrorMessage('Oh no! Something went wrong removing your discount code.');\n    }\n  }\n\n  private onCartUpdated(event: CartUpdatedEvent) {\n    // We do not show change messages for the transition from a null cart to an initial cart state.\n    // The deltas are not meaningful.\n    //\n    // An init that failed before this one may have left its error showing, such as when this\n    // element replays the queue and finds the failure ahead of the retry that worked. The cart is\n    // here now, so we clear it.\n    if (event.detail.is_initial) {\n      const alerts = this.shadowRoot.querySelectorAll('alert-element');\n      for (const alert of alerts) {\n        alert.remove();\n      }\n\n      return;\n    }\n\n    // Skip announcements for updates the shopper did not initiate in this tab (state synced from\n    // another tab, or a bfcache restore catching up to the server). Announcing a delta they did not\n    // cause here would be wrong. This couples to specific scenario values by design.\n\n    const nonUserInitiatedScenarios = ['cross-tab-sync', 'bfcache-reconcile'];\n    if (nonUserInitiatedScenarios.includes(event.detail.scenario)) {\n      return;\n    }\n\n    // The free gift's own changes, such as adding the gift the shopper picked or removing it once\n    // the cart stops qualifying, happen on the way between views and speak for themselves. Skipping\n    // them also keeps up the message about the change the shopper made just before. An error is\n    // different, since a gift change that went through means an earlier failure, such as a gift\n    // that would not add, no longer describes the cart.\n    if (event.detail.scenario?.startsWith('gift-with-purchase')) {\n      const errors = this.shadowRoot.querySelectorAll('alert-element[data-alert-type=\"danger\"]');\n      for (const error of errors) {\n        error.remove();\n      }\n\n      return;\n    }\n\n    // We generally should not be mutating the dom one node at a time and should use some batch\n    // removal technique such as setting the parent element's innerHTML to an empty string. However\n    // there are extremely few messages so this is not a concern.\n\n    // Clear any previous messages\n    const alerts = this.shadowRoot.querySelectorAll('alert-element');\n    for (const alert of alerts) {\n      alert.remove();\n    }\n\n    // Show added items messages\n    if (event.detail.variants_added.length > 0) {\n      this.showItemsAddedMessage(event.detail.variants_added);\n    }\n\n    // Show removed items messages\n    if (event.detail.variants_removed.length > 0) {\n      const titles = event.detail.variants_removed.map(variant => variant.product_title);\n      const itemsString = titles.join(', ');\n      const message = document.createElement('div');\n\n      message.textContent = `You removed ${itemsString} from your shopping bag.`;\n      const alert = document.createElement('alert-element');\n      alert.dataset.alertType = 'success';\n      alert.appendChild(message);\n      this.shadowRoot.appendChild(alert);\n    }\n\n    this.showDiscountCodeAppliedMessages(event);\n    this.showDiscountRemovedMessages(event);\n  }\n\n  private showDiscountCodeAppliedMessages(event: CartUpdatedEvent) {\n    for (const code of event.detail.discount_codes_added) {\n      const message = `Applied discount code ${code}.`;\n      this.showSuccessMessage(message);\n    }\n  }\n\n  private showDiscountRemovedMessages(event: CartUpdatedEvent) {\n    for (const code of event.detail.discount_codes_removed) {\n      const message = `The discount ${code} was removed.`;\n      this.showSuccessMessage(message);\n    }\n  }\n\n  private showItemsAddedMessage(items: ProductVariant[]) {\n    const upsellItem = items.find(item => item.merged_properties._is_upsell);\n    if (upsellItem) {\n      this.showUpsellItemMessage(upsellItem);\n      return;\n    }\n\n    const backorderedItem = items.find(item => item.merged_properties._backorder_message);\n    if (backorderedItem) {\n      this.showBackorderItemMessage(backorderedItem);\n    }\n\n    const addedLineItemsString = items.map(item => item.product_title).join(', ');\n\n    const message = document.createElement('div');\n    message.innerHTML = `This is so great! You added ${addedLineItemsString} to your shopping` +\n      ' bag!';\n\n    const alert = document.createElement('alert-element');\n    alert.dataset.alertType = 'success';\n    alert.appendChild(message);\n    this.shadowRoot.appendChild(alert);\n  }\n\n  private showBackorderItemMessage(item: ProductVariant) {\n    let message = item.merged_properties._backorder_message[0];\n    if (!message) {\n      message =\n        'This item is currently on backorder, estimated ship date is visible on product page';\n    }\n\n    const backorderMessage = document.createElement('div');\n    backorderMessage.textContent = `Important note about: ${item.product_title}. ${message}`;\n\n    const backorderAlert = document.createElement('alert-element');\n    backorderAlert.dataset.alertType = 'warning';\n    backorderAlert.appendChild(backorderMessage);\n\n    this.shadowRoot.appendChild(backorderAlert);\n  }\n\n  private showErrorMessage(messageHtml: string) {\n    const message = document.createElement('div');\n    message.textContent = messageHtml;\n\n    const alert = document.createElement('alert-element');\n    alert.dataset.alertType = 'danger';\n    alert.appendChild(message);\n\n    this.shadowRoot.appendChild(alert);\n  }\n\n  /**\n   * Shows that the cart failed to load, with a button that asks cart init to run again so the\n   * shopper can recover without reloading the page.\n   */\n  private showInitErrorMessage() {\n    const message = document.createElement('div');\n    message.textContent = 'Something went wrong loading your shopping bag.';\n\n    const button = document.createElement('button');\n    button.type = 'button';\n    button.textContent = 'Reload my bag';\n    button.addEventListener('click', onReloadBagClick);\n\n    const alert = document.createElement('alert-element');\n    alert.dataset.alertType = 'danger';\n    alert.append(message, button);\n\n    this.shadowRoot.appendChild(alert);\n    this.initErrorAlert = alert;\n  }\n\n  private showSuccessMessage(messageHtml: string) {\n    const message = document.createElement('div');\n    message.textContent = messageHtml;\n\n    const alert = document.createElement('alert-element');\n    alert.dataset.alertType = 'success';\n    alert.appendChild(message);\n\n    this.shadowRoot.appendChild(alert);\n  }\n\n  private showUpsellItemMessage(item: ProductVariant) {\n    const message = document.createElement('div');\n    message.textContent = `${item.product_title} is a special ` +\n      'additional savings item. It can only be purchased together with a regular item and only ' +\n      'once per order.';\n\n    const alert = document.createElement('alert-element');\n    alert.dataset.alertType = 'info';\n    alert.appendChild(message);\n\n    this.shadowRoot.appendChild(alert);\n  }\n}\n\n/**\n * Asks cart init to run again. Init lives in a different bundle, so we ask through an event on\n * window rather than calling it.\n *\n * We take the error away first, and the cart shows its loading message while init runs. If init\n * fails again, the error comes back with a new button, so the shopper can tell it failed.\n */\nfunction onReloadBagClick(event: MouseEvent) {\n  const button = event.currentTarget;\n  if (button instanceof HTMLButtonElement) {\n    button.closest('alert-element')?.remove();\n  }\n\n  const requestEvent = new Event('cart-init-requested');\n  dispatchEvent(requestEvent);\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'cart-messages': CartMessages;\n  }\n}\n\nif (!customElements.get('cart-messages')) {\n  customElements.define('cart-messages', CartMessages);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport type { Cart } from '@integrabeauty/shopify-ajax-api';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders the cart's totals. Displays the subtotal of all line items, the total discount amount,\n * and the discount percentage (if any).\n */\nclass CartTotals extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return [\n      'data-estimated-total'\n    ];\n  }\n\n  declare readonly dataset: {\n    /**\n     * ISO currency code.\n     *\n     * @example \"USD\"\n     */\n    currencyCode: string;\n\n    /**\n     * Locale specific currency characters.\n     *\n     * @example \"$\"\n     */\n    currencySymbol: string;\n\n    /**\n     * The price before discounts less the price after discounts.\n     */\n    discountAmount: string;\n\n    /**\n     * The price after discounts divided by the price before discounts, as a percentage.\n     */\n    discountPercent: string;\n\n    /**\n     * The price after discounts but before taxes and shipping, which are unknown until checkout.\n     */\n    estimatedTotal: string;\n\n    /**\n     * The price before discounts.\n     */\n    subtotalAmount: string;\n  };\n\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  public attributeChangedCallback(_name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && oldValue !== newValue) {\n      this.render();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private render() {\n    // Hide discount row if discount is $0 (which is the same as subtotal = estimated total)\n    const discountRow = this.shadowRoot.querySelector<HTMLElement>('#discount-row');\n    if (this.dataset.discountAmount === '0') {\n      discountRow.hidden = true;\n    } else {\n      discountRow.hidden = false;\n    }\n\n    const subtotalEl = this.shadowRoot.querySelector<HTMLElement>('#subtotal');\n    subtotalEl.dataset.currencyCode = this.dataset.currencyCode;\n    subtotalEl.dataset.currencySymbol = this.dataset.currencySymbol;\n    subtotalEl.dataset.subunits = this.dataset.subtotalAmount;\n\n    const discountTotalEl = this.shadowRoot.querySelector<HTMLElement>('#discount');\n    discountTotalEl.dataset.currencyCode = this.dataset.currencyCode;\n    discountTotalEl.dataset.currencySymbol = this.dataset.currencySymbol;\n    discountTotalEl.dataset.subunits = this.dataset.discountAmount;\n\n    this.shadowRoot.querySelector('#discount-percentage').textContent =\n      `(${this.dataset.discountPercent}%)`;\n  }\n\n  private onCartUpdated(event: GlobalEventHandlersEventMap['cart-updated']) {\n    const cart = event.detail.cart;\n    if (cart.items.length === 0) {\n      return;\n    }\n\n    const subtotal = computeSubtotal(cart);\n\n    this.dataset.subtotalAmount = subtotal.toString();\n\n    const applied = this.shadowRoot.querySelector<HTMLElement>('#order-discounts-row');\n    applied.style.display = 'none';\n\n    let orderDiscountsValue = 0;\n    for (const discount of cart.cart_level_discount_applications) {\n      if (discount.total_allocated_amount > 0) {\n        orderDiscountsValue += discount.total_allocated_amount;\n      }\n    }\n\n    if (orderDiscountsValue > 0) {\n      // If a cart level discount is applied, we unset display none to ensure \"Order Discounts\"\n      // heading is visible.\n      applied.style.display = 'flex';\n\n      const orderDiscountsEl = this.shadowRoot.querySelector<HTMLElement>('#order-discount-total');\n      orderDiscountsEl.dataset.currencySymbol = this.dataset.currencySymbol;\n      orderDiscountsEl.dataset.currencyCode = this.dataset.currencyCode;\n      orderDiscountsEl.dataset.subunits = orderDiscountsValue.toString();\n    }\n\n    // We cannot use the original discount amount because that is the difference between the sales\n    // price and the discounted price. The sales price is the implicitly discounted MSRP. We want\n    // the difference from the MSRP.\n\n    // The total after discounts, e.g. the estimated total less taxes and shipping that someone will\n    // pay, is the total_price property. Because total_price is floating point, we want to round to\n    // two significant digits.\n\n    const discountAmount = Math.round(100 * (subtotal - cart.total_price)) / 100;\n\n    this.dataset.discountAmount = discountAmount.toString();\n\n    // The discount percent must be manipulated to be based upon our custom subtotal so that it\n    // represents the reduction of the MSRP, not the sales price. We multiply by 100 so that we get\n    // a nice visual percentage (e.g. a 10% discount). We use floor instead of round so that we\n    // never over-represent the percentage. (e.g. 5.5% off is 5% off, not 6%).\n\n    const discountPercent = Math.floor(100 * discountAmount / subtotal);\n    this.dataset.discountPercent = discountPercent.toString();\n\n    // Set this attribute last because this triggers a render\n    this.dataset.estimatedTotal = cart.total_price.toString();\n  }\n}\n\n/**\n * The subtotal should be the total of the compare at price elements. Because we are working with\n * compare at prices, we cannot use the cart's price before discounts fields. We have to calculate\n * this on our own.\n *\n * There was an implementation choice to compute this in the server side rendered cart state in\n * liquid or to compute this here. It seems better to do it here because JavaScript is much more\n * expressive and the cost of performing the calculation here is not significant and because this\n * clearly documents the logic and is easier to maintain.\n */\nfunction computeSubtotal(cart: Cart) {\n  let subtotal = 0;\n  for (const item of cart.items) {\n    const compareAtPrice = parseInt(item.properties._value_set_compare_price, 10);\n    if (Number.isInteger(compareAtPrice)) {\n      subtotal += compareAtPrice;\n    } else {\n      subtotal += item.original_line_price;\n    }\n  }\n\n  // If doing some math somehow resulted in floating point junk, remove it. This generally does not\n  // happen in browsers that we tested but you never know.\n  subtotal = Math.floor(subtotal);\n\n  return subtotal;\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'cart-totals': CartTotals;\n  }\n}\n\nif (!customElements.get('cart-totals')) {\n  customElements.define('cart-totals', CartTotals);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * This element renders an upsell item with the ability for the user to add an upsell to cart.\n */\nclass CartUpsellItem extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-variant-id', 'data-product-tags', 'data-compare-at-price'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Compare at price\n     */\n    compareAtPrice: string;\n\n    /**\n     * Country ISO code\n     */\n    country: string;\n\n    /**\n     * Currency ISO code\n     *\n     * @example \"USD\"\n     */\n    currencyCode: string;\n\n    /**\n     * Currency symbol character\n     *\n     * @example \"$\"\n     */\n    currencySymbol: string;\n\n    /**\n     * Product handle\n     */\n    handle: string;\n\n    /**\n     * Product id\n     */\n    id: string;\n\n    /**\n     * Product image url\n     */\n    image: string;\n\n    /**\n     * Product price\n     */\n    price: string;\n\n    /**\n     * CSV of product tags\n     */\n    productTags: string;\n\n    /**\n     * Product title, used together with the variant title to build the displayed upsell name.\n     */\n    productTitle: string;\n\n    /**\n     * Average product rating\n     */\n    reviewsAverage: string;\n\n    /**\n     * Number of product reviews\n     */\n    reviewsCount: string;\n\n    /**\n     * Variant SKU\n     */\n    sku: string;\n\n    /**\n     * Name of the Shopify template the upsell is being rendered within.\n     */\n    templateName: string;\n\n    /**\n     * Product title.\n     */\n    title: string;\n\n    /**\n     * Product type\n     */\n    type: string;\n\n    /**\n     * Id of the variant added to the cart when the upsell is accepted. Rendering is skipped until\n     * this is set.\n     */\n    variantId: string;\n\n    /**\n     * Variant title, appended to the product title unless it is the default variant title.\n     */\n    variantTitle: string;\n  };\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    this.render();\n  }\n\n  public attributeChangedCallback(_name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && oldValue !== newValue) {\n      this.render();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private render() {\n    if (!this.dataset.variantId) {\n      return;\n    }\n\n    const upsellLink = new URL(`products/${this.dataset.handle}`, location.origin);\n    const url = upsellLink.toString();\n    const title = this.buildTitle();\n\n    this.renderTitle(title, url);\n    this.renderImage(url);\n    this.renderPrices();\n    this.renderAddButton(title);\n    this.renderReviews(url);\n  }\n\n  private buildTitle() {\n    let title = this.dataset.productTitle;\n    if (this.dataset.variantTitle !== 'Default Title') {\n      title += ` ${this.dataset.variantTitle}`;\n    }\n\n    return title;\n  }\n\n  private renderTitle(title: string, url: string) {\n    const anchor = this.shadowRoot.querySelector<HTMLAnchorElement>('.title a');\n    anchor.href = url;\n    anchor.textContent = title;\n  }\n\n  private renderImage(url: string) {\n    const image = this.shadowRoot.querySelector<HTMLElement>('#image');\n    // the image is decorative (described by the adjacent text)\n    image.dataset.alt = '';\n    image.dataset.src = this.dataset.image;\n\n    const clickableArea = this.shadowRoot.querySelector('clickable-area');\n    clickableArea.dataset.url = url;\n  }\n\n  /**\n   * Some upsells carry their price in a product tag instead of on the variant, so we have to work\n   * out which price to actually show. The compare at price comes back empty when there is nothing\n   * to strike through.\n   */\n  private resolvePrices() {\n    let price = this.dataset.price;\n    let compareAtPrice = this.dataset.compareAtPrice;\n    const upsellTagSearch = this.dataset.currencyCode === 'CAD' ? 'UPSELL:CAD' : 'UPSELL';\n\n    // It is important to use optional chaining here because product tags is not guaranteed to be\n    // defined\n    const upsellTag = this.dataset.productTags?.split(' ').find(t => t.includes(upsellTagSearch));\n\n    if (upsellTag) {\n      const upsellPriceArray = upsellTag.split(':');\n      const upsellPriceString = this.dataset.currencyCode === 'CAD' ?\n        upsellPriceArray[2] :\n        upsellPriceArray[1];\n      const upsellPrice = parseFloat(upsellPriceString) * 100;\n\n      // Not all upsell items require their price and compareAtPrice values to be updated before\n      // being presented to the user. To prevent overwriting already properly priced products, we\n      // must confirm the extracted upsell price from the product tags is a lower value. If so we\n      // proceed with updating the compareAtPrice.\n      if (upsellPrice < parseInt(price, 10) && parseInt(price, 10) > parseInt(compareAtPrice, 10)) {\n        compareAtPrice = price;\n      }\n\n      price = upsellPrice.toString();\n    }\n\n    return { price, compareAtPrice };\n  }\n\n  private renderPrices() {\n    const prices = this.resolvePrices();\n\n    const priceEl = this.shadowRoot.querySelector<HTMLElement>('#price');\n    priceEl.dataset.currencyCode = this.dataset.currencyCode;\n    priceEl.dataset.currencySymbol = this.dataset.currencySymbol;\n    priceEl.dataset.subunits = prices.price;\n\n    const compareAtPriceEl = this.shadowRoot.querySelector<HTMLElement>('#compare-at-price');\n    compareAtPriceEl.dataset.currencyCode = this.dataset.currencyCode;\n    compareAtPriceEl.dataset.currencySymbol = this.dataset.currencySymbol;\n\n    if (prices.compareAtPrice) {\n      compareAtPriceEl.dataset.subunits = prices.compareAtPrice;\n      compareAtPriceEl.dataset.hide = 'false';\n      compareAtPriceEl.dataset.ariaLabelPrefix = 'Regular price:';\n      priceEl.dataset.ariaLabelPrefix = 'Sale price:';\n    } else {\n      // hide the compare\n      compareAtPriceEl.dataset.hide = 'true';\n      compareAtPriceEl.dataset.ariaLabelPrefix = '';\n      priceEl.dataset.ariaLabelPrefix = 'Price:';\n    }\n  }\n\n  private renderAddButton(title: string) {\n    const addButton = this.shadowRoot.querySelector('add-to-cart-button');\n    addButton.dataset.country = this.dataset.country;\n    addButton.dataset.variantTitle = title;\n\n    if (this.shadowRoot.querySelector(`variant-element[data-id=\"${this.dataset.variantId}\"]`)) {\n      // do not add\n    } else {\n      const customAttribute = document.createElement('custom-attribute');\n      customAttribute.dataset.key = '_is_upsell';\n      customAttribute.dataset.value = 'true';\n\n      const variant = document.createElement('variant-element');\n      variant.slot = 'variants';\n      variant.dataset.id = this.dataset.variantId;\n      variant.dataset.quantity = '1';\n      variant.appendChild(customAttribute);\n\n      addButton.appendChild(variant);\n    }\n\n    addButton.dataset.update = Date.now().toString();\n  }\n\n  private renderReviews(url: string) {\n    const reviews = this.shadowRoot.querySelector('product-rating-stars');\n    reviews.dataset.average = this.dataset.reviewsAverage;\n    reviews.dataset.count = this.dataset.reviewsCount;\n    reviews.dataset.href = `${url}#reviews-section`;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'cart-upsell-item': CartUpsellItem;\n  }\n}\n\nif (!customElements.get('cart-upsell-item')) {\n  customElements.define('cart-upsell-item', CartUpsellItem);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * The container for the upsell suggestions shown in the cart drawer. The individual suggestions are\n * cart upsell item children.\n *\n * This exists to keep the suggestions relevant as the cart changes. On every cart update it hides\n * any item already in the cart, shows at most four, and collapses itself when none are left to\n * suggest.\n */\nclass CartUpsellItems extends HTMLElement implements CustomElement {\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    // The animation frame ensures the element has actually rendered.\n    requestAnimationFrame(() => {\n      const event = new Event('cart-upsell-items-rendered', { bubbles: true, composed: true });\n      this.dispatchEvent(event);\n    });\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onCartUpdated(event: GlobalEventHandlersEventMap['cart-updated']) {\n    const variantIds = event.detail.cart.items.map(item => item.variant_id);\n    const elements = this.querySelectorAll('cart-upsell-item');\n    let visibleCount = 0;\n\n    for (const element of elements) {\n      const variantId = parseInt(element.dataset.variantId, 10);\n      const show = !variantIds.includes(variantId) && visibleCount < 4;\n\n      if (show) {\n        // if upsellItemElement is not included in the cart unhide it\n        element.style.display = 'flex';\n        visibleCount++;\n      } else {\n        element.style.display = 'none';\n      }\n    }\n\n    // Hide upsell items section if it is empty\n    if (visibleCount === 0) {\n      this.style.display = 'none';\n    } else {\n      this.style.display = 'block';\n    }\n  }\n}\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    /**\n     * Dispatched by the upsell items element whenever the upsell items element is rendered.\n     */\n    'cart-upsell-items-rendered': Event;\n  }\n\n  interface HTMLElementTagNameMap {\n    'cart-upsell-items': CartUpsellItems;\n  }\n}\n\nif (!customElements.get('cart-upsell-items')) {\n  customElements.define('cart-upsell-items', CartUpsellItems);\n}\n","/**\n * Currency Helpers\n * -----------------------------------------------------------------------------\n * A collection of useful functions that help with currency formatting\n *\n * Current contents\n * - formatMoney - Takes an amount in cents and returns it as a formatted dollar value.\n *\n */\n\nconst moneyFormat = '${{amount}}';\n\n/**\n * Format money values based on your shop currency settings\n * @param  {Number|string} cents - value in cents or dollar amount e.g. 300 cents\n * or 3.00 dollars\n * @param  {String} format - shop money_format setting\n * @return {String} value - formatted value\n */\nexport function formatMoney(cents, format) {\n  if (typeof cents === 'string') {\n    cents = cents.replace('.', '');\n  }\n  let value = '';\n  const placeholderRegex = /\\{\\{\\s*(\\w+)\\s*\\}\\}/;\n  const formatString = format || moneyFormat;\n\n  function formatWithDelimiters(\n    number,\n    precision = 2,\n    thousands = ',',\n    decimal = '.'\n  ) {\n    if (isNaN(number) || number == null) {\n      return 0;\n    }\n\n    number = (number / 100.0).toFixed(precision);\n\n    const parts = number.split('.');\n    const dollarsAmount = parts[0].replace(\n      /(\\d)(?=(\\d\\d\\d)+(?!\\d))/g,\n      `$1${thousands}`\n    );\n    const centsAmount = parts[1] ? decimal + parts[1] : '';\n\n    return dollarsAmount + centsAmount;\n  }\n\n  switch (formatString.match(placeholderRegex)[1]) {\n    case 'amount':\n      value = formatWithDelimiters(cents, 2);\n      break;\n    case 'amount_no_decimals':\n      value = formatWithDelimiters(cents, 0);\n      break;\n    case 'amount_with_comma_separator':\n      value = formatWithDelimiters(cents, 2, '.', ',');\n      break;\n    case 'amount_no_decimals_with_comma_separator':\n      value = formatWithDelimiters(cents, 0, '.', ',');\n      break;\n  }\n\n  return formatString.replace(placeholderRegex, value);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { formatMoney } from '@shopify/theme-currency';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * This element is responsible for showing a free shipping message. It reacts to a cart updated\n * event to calculate if the cart qualifies for free shipping.\n *\n * The free shipping is applicable when the estimated cart total reaches a certain threshold or when\n * there is a subscription item in the cart.\n */\nclass FreeShippingBanner extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * Text to show when free shipping is unlocked because of a subscription item in the cart\n     */\n    subscriptionText: string;\n\n    /**\n     * Minimum cart value for free shipping (in cents).\n     */\n    threshold: string;\n  };\n\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n\n  /**\n   * Estimated customer cart value, exact value is calculated on checkout shipping step (in cents).\n   */\n  private estimatedTotal = 0;\n\n  /**\n   * Whether there is a subscription item in the cart, which unlocks free shipping regardless of the\n   * cart value.\n   */\n  private subscriptionsInCart = false;\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onCartUpdated(event: GlobalEventHandlersEventMap['cart-updated']) {\n    const cart = event.detail.cart;\n    this.estimatedTotal = cart.total_price;\n\n    let subscriptionsInCart = false;\n    for (const item of cart.items) {\n      if (item?.selling_plan_allocation?.selling_plan?.recurring_deliveries) {\n        subscriptionsInCart = true;\n        break;\n      }\n    }\n\n    this.subscriptionsInCart = subscriptionsInCart;\n\n    this.render();\n  }\n\n  private render() {\n    const span = this.shadowRoot.querySelector('span');\n    const progressContainer = this.shadowRoot.querySelector('.progress-container');\n    const progressBar = this.shadowRoot.querySelector<HTMLElement>('.progress-bar');\n\n    span.textContent = this.getMessageText();\n\n    let percentage: number;\n    if (this.subscriptionsInCart) {\n      percentage = 100;\n    } else {\n      const threshold = parseInt(this.dataset.threshold, 10);\n      percentage = Math.min(100, (this.estimatedTotal / threshold) * 100);\n    }\n\n    percentage = Math.round(percentage);\n    if (isNaN(percentage)) {\n      percentage = 0;\n    }\n\n    progressContainer.setAttribute('aria-valuenow', percentage.toString());\n    progressBar.style.width = percentage + '%';\n  }\n\n  private getMessageText() {\n    if (this.subscriptionsInCart) {\n      return this.dataset.subscriptionText ||\n        'Free shipping unlocked with subscription item!';\n    }\n\n    const threshold = parseInt(this.dataset.threshold, 10);\n    const difference = threshold - this.estimatedTotal;\n\n    if (difference > 0) {\n      // eslint-disable-next-line no-template-curly-in-string\n      return 'Add ' + formatMoney(difference, '${{amount}}') +\n        ' to unlock Free Contiguous US Shipping!';\n    } else {\n      return 'Congratulations! You\\'ve unlocked Free Contiguous US Shipping!';\n    }\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'free-shipping-banner': FreeShippingBanner;\n  }\n}\n\nif (!customElements.get('free-shipping-banner')) {\n  customElements.define('free-shipping-banner', FreeShippingBanner);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Tells a shopper whose cart qualifies for a free gift, and has none yet, that they can still pick\n * one. Clicking it asks for the offer view. It renders hidden, and the gift with purchase progress\n * bar shows and hides every copy of it on the page as the cart changes.\n */\nclass GiftQualifiedMessage extends HTMLElement implements CustomElement {\n  private onClickBound = this.onClick.bind(this);\n\n  public connectedCallback() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n\n    this.shadowRoot.querySelector('button').addEventListener('click', this.onClickBound);\n  }\n\n  public disconnectedCallback() {\n    this.shadowRoot.querySelector('button')?.removeEventListener('click', this.onClickBound);\n  }\n\n  private onClick(_event: MouseEvent) {\n    type Detail = GlobalEventHandlersEventMap['offer-view-requested']['detail'];\n    const event = new CustomEvent<Detail>('offer-view-requested', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        reason: 'chosen'\n      }\n    });\n    this.dispatchEvent(event);\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'gift-qualified-message': GiftQualifiedMessage;\n  }\n}\n\nif (!customElements.get('gift-qualified-message')) {\n  customElements.define('gift-qualified-message', GiftQualifiedMessage);\n}\n","import { type Cart, cartUpdate } from '@integrabeauty/shopify-ajax-api';\nimport { cartFetch } from '../state.js';\nimport { getApplicableDiscountCodes, getDiscountCodes, isWellFormed } from './discount.js';\n\ntype DiscountOverride = typeof cart_lifecycle.discount_overrides[number];\n\n/**\n * Updates a cart's discount codes. This uses a specially enabled feature on the lange Shopify shop\n * that enables the use of the cart update ajax api to modify the cart's discounts. The special\n * feature enables the use of a discount property in the update call.\n *\n * New codes are partially validated before the http request is sent. If any new code is invalid,\n * this throws an error immediately.\n *\n * This will send an update request even if the discount codes have not changed. This does not check\n * whether sending the request is pointless. One of the reasons why this check is not performed is\n * because sometimes callers want to call this function even when it seems pointless because of the\n * risk that client side state is out of sync with server side state. That kind of thing can happen\n * for example when browsing the site in multiple tabs, because client side state is largely managed\n * per tab, so one tab may naively update discounts without awareness of changes made in other tabs.\n * While we attempt to keep state synchronized across tabs, there is no guarantee.\n *\n * The special discount property of the update request body has no documentation. We figured out\n * that it works similar to the checkout links discount url parameter. Multiple codes can be\n * specified as a comma separated value. This also means that codes should not contain commas. We\n * also figured out that it overwrites all discounts. This does not just add new discounts. That is\n * why this requires both the set of existing codes and the new codes.\n *\n * @param cart the state of the cart prior to update\n * @param newCodes the codes that should be applied to the cart in addition to the existing codes\n * @param overrides discount overrides, this is expected to always be defined even if there are no\n * overrides\n * @param postValidate whether to check if the new codes were actually applied after the update\n *\n * @see https://help.shopify.com/en/manual/discounts/managing-discount-codes\n */\nexport async function applyDiscountsHelper(cart: Cart, newCodes: string[],\n  overrides: DiscountOverride[], postValidate = true) {\n  validateCodesBeforeUpdate(newCodes);\n\n  const oldUppers = getCurrentCodes(cart, newCodes.length);\n  const newUppers = newCodes.map(code => code.toUpperCase());\n\n  const retainedOldUppers = oldUppers.filter(code =>\n    !overrides.some(override =>\n      override.old_code === code && newUppers.includes(override.new_code)));\n\n  const mergedUppers = new Set<string>([...retainedOldUppers, ...newUppers]);\n  const mergedCodes = [...mergedUppers];\n\n  const discount = mergedCodes.join(',');\n  await cartUpdate({ discount });\n  const updatedCart = await cartFetch();\n\n  // Only validate if post validation requested. Validation is performed by default but there are\n  // some use cases where discounts are applied even in inapplicable situations, such as when the\n  // cart is empty (the discount is applied but if you look at the discount object in the\n  // discount_codes array the applicable property is false). By only validating if requested we\n  // avoid generating routine validation errors.\n\n  if (postValidate) {\n    validateCodesAfterUpdate(updatedCart, newCodes);\n  }\n\n  return updatedCart;\n}\n\n/**\n * Obtain the existing discount codes. Generally, we want to retain all codes, including currently\n * inapplicable codes, as those codes may become applicable later and have been deemed applied even\n * though they have no effect. However, retaining all codes leads to the possible accumulation of\n * several inapplicable codes. This leads into a poorly documented issue. Once there are about 5 or\n * 6 codes applied, in either the applicable or inapplicable state, future applications fail without\n * regard to code applicability.\n *\n * Therefore, this needs to take into account the total number of discount codes that will be sent\n * as a part of a cart update. This is why the new codes array is also an input to this function\n * that has only to do with old codes.\n *\n * This is a hacky rushed fix that needs to be revisited. Expired codes stick around. Typo codes\n * stick around. Once we reach more than 5 codes (in any state), all future applications, even of\n * valid codes, incorrectly fail to apply. So in this case, we have to filter down the list. The\n * quickest thing we can do is just drop codes that are applied but in the inapplicable state. We\n * prefer not to do this because we want to support visitors who apply a code, it becomes applied\n * but inapplicable, then add a product, and it becomes applicable.\n *\n * This hacky fix is not perfect because it is not deduplicating. This is an inaccurate and quick\n * fix for \"several\" issues, not all of the issues stemming from bad codes sticking around.\n *\n * There is a limitation on the number of discount codes that can be applied to the same order,\n * including codes in the inapplicable state. According to Shopify, \"Customers can use a maximum of\n * five product or order discount codes and one shipping discount code on the same order.\"\n *\n * @see https://help.shopify.com/en/manual/discounts/combining-discounts/discount-combinations#limitations\n */\nfunction getCurrentCodes(cart: Cart, newCodesCount: number) {\n  const currentCodes = Array.isArray(cart.discount_codes) ? cart.discount_codes : [];\n\n  // We test against what would be the total number of applied codes in determining which codes\n  // should be included in the output. If adding the new codes would exceed the threshold, then we\n  // try our best to trim the number of currently applied codes down to below the threshold. Since\n  // we cannot tell which discount codes are shipping discount codes, we use the threshold of 5\n  // instead of 6.\n\n  // There is currently no reliable additional way of choosing which discount codes to remove. There\n  // is no clear documentation on the order of the codes in the array. So we have to choose an\n  // arbitrary method of ejecting some codes. For now, this basically just gets rid of any codes\n  // that are inapplicable. That will most likely remove typos and expired codes. Most visitors will\n  // not discern the missing codes.\n\n  // This does not guarantee that the output is reduced to below the threshold. If there are 5\n  // applicable codes, then no codes will be removed, and the 6th application can still fail.\n\n  // This is not trying to get down to just below the threshold. This does a whole sweep. We might\n  // want to change this to remove inapplicable codes up until the threshold is no longer exceeded.\n\n  // Drop all inapplicable codes by keeping around only those older codes that are applicable. Try\n  // to retain inapplicable shipping codes.\n\n  if (currentCodes.length + newCodesCount > 5) {\n    return currentCodes\n      .filter(code => code.applicable || /ship/i.test(code.code))\n      .map(code => code.code)\n      .map(code => code.toUpperCase());\n  }\n\n  return getDiscountCodes(cart);\n}\n\nfunction validateCodesBeforeUpdate(newCodes: string[]) {\n  if (newCodes.length < 1) {\n    throw new Error('At least one discount code must be specified');\n  }\n\n  // If any one of the input codes is invalid, reject with a specific error message that mentions\n  // the code. It is important to be explicit here because this error may be visible to users.\n\n  for (const code of newCodes) {\n    if (!isWellFormed(code)) {\n      const error: any = new Error(`Invalid discount code ${code}`);\n      error.discount_code = code;\n      error.discount_codes = newCodes;\n      throw error;\n    }\n  }\n}\n\n/**\n * When validating whether the codes applied, we only validate whether new codes applied. We do not\n * validate whether existing codes applied, because it is possible that applying a new code caused a\n * previous code to be removed, because the new code replaces the existing code, for example,\n * because the new code offers a better discount.\n *\n * If a code was applied but in the inapplicable state, then we treat the add as a failure, even\n * though the code persists in the discount_codes array. The rationale is complex. Basically, we\n * must address the UX concern that we must provide feedback that an invalid code was applied, so we\n * must trigger an error. However, some codes, such as shipping codes, can be early applied. Some\n * codes, such as shipping codes, can apply but have no cart level applications or line level\n * allocations, but still be applied, and still be valid, even though the code is not actually\n * affecting the estimated total. We do not want to treat that as an error.\n *\n * For a shipping discount that is applicable, even though it has no effect, it will appear in the\n * discount_codes array with applicable true.\n *\n * For a fictional discount that is inapplicable, it will appear in the discount_codes array with\n * applicable false.\n */\nfunction validateCodesAfterUpdate(cart: Cart, newCodes: string[]) {\n  const updatedNormalCodes = getApplicableDiscountCodes(cart);\n\n  // Downstream analytics sometimes does a breakdown on the error message. Input new codes are in\n  // mixed cases, leading to nearly duplicate messages. To make it easier on analysis, we report on\n  // the normalized discount code, not the actual characters entered by visitors.\n\n  for (const newCode of newCodes) {\n    const upperNewCode = newCode.toUpperCase();\n    if (!updatedNormalCodes.includes(upperNewCode)) {\n      const error: any = new Error(`The discount code ${upperNewCode} was not applied`);\n      error.discount_code = upperNewCode;\n      error.discount_codes = newCodes;\n      throw error;\n    }\n  }\n}\n","import { assert } from '@integrabeauty/assert';\nimport { type AddItem, isUnavailableError, cartAdd, type Cart, isInvalidSellingPlanError, isMaxQuantityError, isBundleAddonMixError } from '@integrabeauty/shopify-ajax-api';\nimport { copy } from '../object.js';\nimport { now } from '../perf.js';\nimport { isWellFormed } from './discount/discount.js';\nimport { applyDiscountsHelper } from './discount/helper.js';\nimport { report } from './error.js';\nimport { cartFetch, read, write } from './state.js';\nimport { start, type Transaction } from './transaction.js';\n\n/**\n * Adds items to the cart. Emits an event when completed.\n *\n * This fails if the request to add fails, or if the request to get the updated state fails.\n *\n * This must perform multiple http requests due to limitations in Shopify's ajax cart api. The first\n * fetch mutates, and the second fetch gets the updated state. Unfortunately, this means that it is\n * possible for the mutation to succeed but for the subsequent fetch to fail. Callers should be wary\n * of this situation. On some future successful retrieval of server side state, the added items will\n * appear, despite this function emitting an error event. This means, for example, that callers\n * should not assume that failure to add is necessarily a real failure.\n *\n * If a discount is specified to be applied on add, it is applied as a part of the same transaction.\n * However, this also introduces some unwanted complexity. This function does not emit an error if\n * applying the discount fails, because this must preserve client side to server side state\n * fidelity, and the second fetch call happens after the first, and Shopify does not support multi-\n * request transactions (e.g. batched conditional requests). There is no simple way to rollback a\n * partial commit because the rollback itself could also fail as it would be an additional request.\n *\n * The new discount is not applied if it appears as existing. This avoids an unnecessary request.\n * The cart's discount_codes array automatically updates the applicable state of applied discount\n * codes as a result of adding new items. There is no need to reapply to trigger the transition from\n * inapplicable to applicable.\n *\n * At the moment, it is unclear whether this should be fetching cart state prior to applying new\n * discounts. Local cart state is assumed to be accurate, so the existing discount codes should be\n * preserved, so there should not be a need, so this does not perform the additional fetch.\n *\n * @param target the element most directly responsible for the add, used to originate any failure\n * event with truthful provenance instead of at global scope\n * @param items the items to add\n * @param discountCodes optional discount codes to attempt to apply to the cart after adding the\n * items\n * @param scenario optional description of the context of the add to cart, e.g. the type of ui\n * element or ux experience, that is used by downstream analytics\n */\nexport async function add(target: EventTarget | null, items: AddItem[], discountCodes?: string[],\n  scenario?: string) {\n  const transaction = start('addItems', items, scenario);\n\n  try {\n    await addItemsInternal(target, transaction, items, discountCodes, scenario);\n  } catch (error) {\n    report(target, {\n      code: determineErrorCode(items, error),\n      error,\n      inputs: {\n        discount_code: discountCodes,\n        items\n      },\n      scenario,\n      transaction\n    });\n  } finally {\n    transaction.complete();\n  }\n}\n\nasync function addItemsInternal(target: EventTarget | null, transaction: Transaction,\n  items: AddItem[], discountCodes: string[], scenario: string) {\n  assert(items.length > 0, 'At least one item must be specified to add');\n\n  const previous = read();\n\n  const enrichedItems = items.map(enrich);\n\n  // An add can outlast the transaction timeout, and then nothing says which of its requests\n  // stalled. So we log each step with how long it took. When a later cart error is reported, the\n  // steps missing from its breadcrumbs point at the request that never came back.\n\n  let stepStart = now();\n  await cartAdd(enrichedItems);\n  console.log('Cart add request finished', Math.round(now() - stepStart));\n\n  // We ignore the output of cart/add.js for several reasons. One reason is that we need the full\n  // cart state, but add.js output only contains an items property. We cannot safely merge the new\n  // items with the local state because multiple changes can happen to a cart in multiple tabs, and\n  // because it is not possible to compare new line items to old line items by line item key because\n  // we have observed line item key shifting, line item splitting, and other surprises. Another\n  // reason is that the response body includes zombie items (0 quantity items) from failed adds.\n\n  // If applying a discount code on add, then we will use the output of that call as the new cart\n  // state. If applying the discount fails, we still treat the add to cart as a success, and\n  // fallback to fetching the new cart state. We treat failure to apply the discount in this case as\n  // non-fatal to add to cart because we have effectively already updated cart state and must\n  // reflect that state change even though the discount failed to apply.\n\n  stepStart = now();\n  let cart = await applyDiscountsOnAdd(previous, discountCodes || [], scenario);\n  console.log('Cart add discount step finished', Math.round(now() - stepStart));\n\n  if (!cart) {\n    stepStart = now();\n    cart = await cartFetch();\n    console.log('Cart add fetch finished', Math.round(now() - stepStart));\n  }\n\n  write(target, cart, transaction, scenario);\n}\n\n/**\n * Try to apply some discount codes to the cart.\n *\n * Returns null when there was no mutation.\n */\nasync function applyDiscountsOnAdd(beforeCart: Cart, discountCodes: string[], scenario?: string) {\n  // We tolerate pointless calls so as to reduce caller boilerplate\n\n  if (discountCodes.length < 1) {\n    return null;\n  }\n\n  // If any input discount code is malformed, do nothing. This does not throw.\n\n  for (const code of discountCodes) {\n    if (!isWellFormed(code)) {\n      const error: any = new Error('Malformed discount code');\n      error.scenario = scenario;\n      error.discount_codes = discountCodes;\n      error.discount_code = code;\n      console.warn(error);\n      return null;\n    }\n  }\n\n  // This is currently using local state. There are two modes for local state. There is the initial\n  // server side render, and there is the state after init. The states are subtly different. On\n  // initial server side render the cart is missing the discount_codes array because that is not\n  // accessible to liquid. The cart init logic was changed to do a fetch of cart state on every page\n  // load just to get the discount_codes array. Sometimes that fetch fails, leaving the server side\n  // rendered state as is, in which case the discount_codes property is undefined. This must account\n  // for that. In doing so, opt out of apply on add entirely, as any apply discounts call must\n  // specify all codes to apply, including existing codes. As we do not know what the previous codes\n  // are when init partially failed, it is better to just skip applying than to unintentionally\n  // cause existing codes to drop.\n\n  if (!Array.isArray(beforeCart.discount_codes)) {\n    return null;\n  }\n\n  // As a part of this function's contract, it returns null to signal that a new cart state was not\n  // obtained. Therefore, as tempting as it might be, do not init afterCart to beforeCart.\n\n  let afterCart: Cart = null;\n\n  const overrides = cart_lifecycle.discount_overrides;\n\n  // Try to apply. Post validation is disabled because this is a form of best effort automatic\n  // application where errors are routine and not fatal.\n\n  try {\n    afterCart = await applyDiscountsHelper(beforeCart, discountCodes, overrides, false);\n  } catch (error) {\n    console.warn(error);\n    // continue\n  }\n\n  return afterCart;\n}\n\n/**\n * Classifies a caught add-to-cart failure into a cart error code.\n *\n * Shopify rejects an invalid selling plan with the same generic description regardless of what the\n * plan was for, so the specific code is disambiguated from the items we sent rather than from the\n * error itself. At render time each line item is tagged with a type-specific property: a backorder\n * message for pre-order plans and a marker for try-before-you-buy plans. A selling-plan rejection\n * carrying neither marker keeps the generic code, because nothing on the item positively identifies\n * it as anything more specific.\n */\nfunction determineErrorCode(items: AddItem[], error: any) {\n  if (isUnavailableError(error)) {\n    return 'ADD_ITEMS_OOS';\n  }\n\n  if (isBundleAddonMixError(error)) {\n    return 'ADD_ITEMS_BUNDLE_ADDON_MIX';\n  }\n\n  if (isMaxQuantityError(error)) {\n    return 'ADD_ITEMS_MAX_QUANTITY';\n  }\n\n  if (isInvalidSellingPlanError(error)) {\n    let item: AddItem;\n    if (Array.isArray(items)) {\n      item = items.find(line => line?.selling_plan);\n    }\n\n    if (item?.properties?._backorder_message) {\n      return 'ADD_ITEMS_INVALID_BACKORDER_SELLING_PLAN';\n    }\n\n    if (item?.properties?._is_tbyb) {\n      return 'ADD_ITEMS_INVALID_TBYB_SELLING_PLAN';\n    }\n\n    return 'ADD_ITEMS_INVALID_SELLING_PLAN';\n  }\n\n  return 'ADD_ITEMS';\n}\n\nfunction enrich(input: AddItem) {\n  // Clone so that the enrichment below does not mutate the caller's input.\n  const output = copy(input);\n  output.properties = output.properties || {};\n  output.properties._url_path = output.properties._url_path || location.pathname;\n  return output;\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport type { AddItem, Item } from '@integrabeauty/shopify-ajax-api';\nimport { v4 as uuidv4 } from 'uuid';\nimport { add } from '../../../lib/cart/add.js';\nimport { remove } from '../../../lib/cart/remove.js';\nimport { update } from '../../../lib/cart/update.js';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\ntype GiftWithPurchaseProduct = HTMLElementTagNameMap['gift-with-purchase-product'];\n\n/**\n * A view of the cart drawer offering the shopper something they just qualified for. Today the only\n * offer is the gift with purchase, so the view is a free gift picker. The gift with purchase\n * progress bar asks for it right after a cart change the shopper made unlocks the gift. The message\n * saying the shopper qualified asks for it when clicked, and so does the Change link on the gift\n * line.\n *\n * The shopper picks one of the gifts, or skips. Skipping a fresh offer, or closing the drawer on\n * it, is remembered in a cart attribute the progress bar reads, so the offer does not come back on\n * every cart change. Skipping a change keeps the gift the shopper already has.\n *\n * When there is only one gift there is nothing to pick, so this adds it without showing the view.\n */\nclass OfferView extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-focus-requested'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Set to \"true\" by the cart drawer when it shows this view, asking the view to put focus on its\n     * close button. The view removes it again once it has, so the next request fires too.\n     */\n    focusRequested: string;\n\n    /**\n     * \"embedded\" on the cart page, where there is no drawer to close. Set in the liquid.\n     */\n    layout: string;\n\n    /**\n     * \"change\" when the shopper is swapping the gift they already have, or \"unlock\" when they are\n     * being offered one. Set each time the view is asked for.\n     */\n    mode: 'change' | 'unlock';\n\n    /**\n     * The view to go back to when the shopper is done here, such as \"upsell\". Set each time the\n     * view is asked for.\n     */\n    returnView: string;\n\n    /**\n     * \"true\" while the shopper's pick is being saved to the cart.\n     */\n    saving: string;\n  };\n\n  /**\n   * The gift line in the cart, if there is one.\n   */\n  private declare claimedGift: Item;\n\n  /**\n   * The variant of the gift selected in the picker.\n   */\n  private declare selectedVariantId: number;\n\n  private onCartDrawerClosedBound = this.onCartDrawerClosed.bind(this);\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n  private onCloseClickedBound = this.onCloseClicked.bind(this);\n  private onConfirmClickedBound = this.onConfirmClicked.bind(this);\n  private onGiftClickedBound = this.onGiftClicked.bind(this);\n  private onOfferViewRequestedBound = this.onOfferViewRequested.bind(this);\n  private onSkipClickedBound = this.onSkipClicked.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    this.shadowRoot.querySelector('close-button').addEventListener('click',\n      this.onCloseClickedBound);\n    this.shadowRoot.querySelector<HTMLElement>('#confirm').addEventListener('click',\n      this.onConfirmClickedBound);\n    this.shadowRoot.querySelector<HTMLElement>('#skip').addEventListener('click',\n      this.onSkipClickedBound);\n\n    for (const gift of this.querySelectorAll('gift-with-purchase-product')) {\n      gift.addEventListener('click', this.onGiftClickedBound);\n    }\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n    addEventListener('cart-drawer-closed', this.onCartDrawerClosedBound);\n    addEventListener('offer-view-requested', this.onOfferViewRequestedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    this.shadowRoot.querySelector('close-button')?.removeEventListener('click',\n      this.onCloseClickedBound);\n    this.shadowRoot.querySelector<HTMLElement>('#confirm')?.removeEventListener('click',\n      this.onConfirmClickedBound);\n    this.shadowRoot.querySelector<HTMLElement>('#skip')?.removeEventListener('click',\n      this.onSkipClickedBound);\n\n    for (const gift of this.querySelectorAll('gift-with-purchase-product')) {\n      gift.removeEventListener('click', this.onGiftClickedBound);\n    }\n\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n    removeEventListener('cart-drawer-closed', this.onCartDrawerClosedBound);\n    removeEventListener('offer-view-requested', this.onOfferViewRequestedBound);\n  }\n\n  public attributeChangedCallback(name: string, _oldValue: string, newValue: string) {\n    if (this.isConnected && name === 'data-focus-requested' && newValue === 'true') {\n      this.focusCloseButton();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  /**\n   * See the upsell view for why focus is forced visible here.\n   */\n  private focusCloseButton() {\n    delete this.dataset.focusRequested;\n    this.shadowRoot.querySelector('close-button').focus({ focusVisible: true });\n  }\n\n  private onCartUpdated(event: GlobalEventHandlersEventMap['cart-updated']) {\n    this.claimedGift = event.detail.cart.items.find(\n      item => item.properties._gift_with_purchase === 'true');\n  }\n\n  private onOfferViewRequested(event: GlobalEventHandlersEventMap['offer-view-requested']) {\n    if (this.dataset.saving === 'true') {\n      return;\n    }\n\n    const gifts = this.querySelectorAll('gift-with-purchase-product');\n    if (gifts.length === 0) {\n      return;\n    }\n\n    const drawer = this.closest('cart-drawer');\n    const view = drawer.dataset.view || 'cart';\n    const reason = event.detail?.reason;\n\n    // An unlock only interrupts a shopper who is looking at their bag. Over the upsell view it\n    // would get in the way of checkout, so the message saying they qualified waits there instead.\n    // The progress view counts as the bag, since it is up while an add from a product page runs.\n    if (reason === 'unlocked') {\n      const shown = drawer.dataset.layout === 'embedded' || drawer.dataset.modalState === 'open';\n      if (!shown || (view !== 'cart' && view !== 'progress')) {\n        return;\n      }\n    }\n\n    if (gifts.length === 1) {\n      if (!this.claimedGift) {\n        this.addGift(gifts[0]).catch(console.warn);\n      }\n\n      return;\n    }\n\n    this.dataset.mode = reason === 'change' ? 'change' : 'unlock';\n    this.dataset.returnView = view === 'upsell' ? 'upsell' : 'cart';\n    this.selectedVariantId = this.claimedGift?.variant_id ??\n      parseInt(gifts[0].dataset.variantId, 10);\n    this.render();\n\n    drawer.dataset.view = 'offer';\n  }\n\n  private onGiftClicked(event: MouseEvent) {\n    if (this.dataset.saving === 'true') {\n      return;\n    }\n\n    const gift = <GiftWithPurchaseProduct>event.currentTarget;\n    this.selectedVariantId = parseInt(gift.dataset.variantId, 10);\n    this.render();\n  }\n\n  private onConfirmClicked(_event: MouseEvent) {\n    if (this.dataset.saving === 'true') {\n      return;\n    }\n\n    if (this.claimedGift?.variant_id === this.selectedVariantId) {\n      this.showView(this.dataset.returnView);\n      return;\n    }\n\n    const gift = this.querySelector<GiftWithPurchaseProduct>(\n      `gift-with-purchase-product[data-variant-id=\"${this.selectedVariantId}\"]`);\n    if (!gift) {\n      return;\n    }\n\n    this.dataset.saving = 'true';\n    this.render();\n\n    this.saveGift(gift).catch(console.warn).finally(() => {\n      delete this.dataset.saving;\n      this.render();\n    });\n  }\n\n  private onSkipClicked(_event: MouseEvent) {\n    if (this.dataset.saving === 'true') {\n      return;\n    }\n\n    if (this.dataset.mode !== 'change') {\n      this.rememberSkip();\n    }\n\n    this.showView(this.dataset.returnView);\n  }\n\n  /**\n   * Closing the drawer on a fresh offer counts as skipping it. The drawer opens on another view\n   * next time, so the offer is only shown once.\n   */\n  private onCartDrawerClosed(_event: GlobalEventHandlersEventMap['cart-drawer-closed']) {\n    const drawer = this.closest('cart-drawer');\n    if (drawer.dataset.view === 'offer' && this.dataset.saving !== 'true' &&\n      this.dataset.mode !== 'change') {\n      this.rememberSkip();\n    }\n  }\n\n  private onCloseClicked(_event: MouseEvent) {\n    type Detail = WindowEventMap['cart-close-requested']['detail'];\n    const event = new CustomEvent<Detail>('cart-close-requested', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        animate: true,\n        source: 'close-button'\n      }\n    });\n    this.dispatchEvent(event);\n  }\n\n  /**\n   * The progress bar reads this attribute to stop offering the gift. It clears it once the cart\n   * drops below the threshold, so crossing it again is a fresh offer.\n   */\n  private rememberSkip() {\n    update(this, { attributes: { _gwp_skipped: 'true' } }, 'gift-with-purchase-skip')\n      .catch(console.warn);\n  }\n\n  /**\n   * Swapping a gift removes the old one and then adds the new one. The cart helpers report a\n   * failure as an error the cart view shows, rather than throwing, so we check the cart they leave\n   * behind instead. On a failure we land on the cart so the shopper sees the error.\n   */\n  private async saveGift(gift: GiftWithPurchaseProduct) {\n    if (this.claimedGift) {\n      await remove(this, [this.claimedGift.key], 'gift-with-purchase-change');\n      if (this.claimedGift) {\n        this.showView('cart');\n        return;\n      }\n    }\n\n    await this.addGift(gift);\n\n    const added = this.claimedGift?.variant_id === parseInt(gift.dataset.variantId, 10);\n    this.showView(added ? this.dataset.returnView : 'cart');\n  }\n\n  /**\n   * We mark the line as a gift so the cart can find it and style it. We also store the gift's\n   * regular price on the line so the cart can show what it's worth, since the line itself is free.\n   */\n  private async addGift(gift: GiftWithPurchaseProduct) {\n    const item: AddItem = {\n      id: gift.dataset.variantId,\n      quantity: 1,\n      properties: {\n        _gift_with_purchase: 'true',\n        _gift_with_purchase_price: gift.dataset.price\n      }\n    };\n\n    await add(this, [item], [], 'gift-with-purchase-add');\n\n    if (this.claimedGift?.variant_id !== parseInt(gift.dataset.variantId, 10)) {\n      return;\n    }\n\n    type Detail = GlobalEventHandlersEventMap['gift-with-purchase-claimed']['detail'];\n    const claimedEvent = new CustomEvent<Detail>('gift-with-purchase-claimed', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        event_id: uuidv4()\n      }\n    });\n    this.dispatchEvent(claimedEvent);\n  }\n\n  private showView(view: string) {\n    this.closest('cart-drawer').dataset.view = view || 'cart';\n  }\n\n  private render() {\n    const gifts = this.querySelectorAll('gift-with-purchase-product');\n    const unlock = this.dataset.mode !== 'change';\n\n    this.shadowRoot.querySelector('#title').textContent = unlock ?\n      'You unlocked a free gift!' : 'Change your free gift';\n    this.shadowRoot.querySelector('#subtitle').textContent =\n      `Pick 1 of ${gifts.length}. Free with your order.`;\n\n    for (const gift of gifts) {\n      const selected = parseInt(gift.dataset.variantId, 10) === this.selectedVariantId;\n      gift.dataset.selected = selected ? 'true' : 'false';\n    }\n\n    const saving = this.dataset.saving === 'true';\n\n    const confirm = this.shadowRoot.querySelector<HTMLButtonElement>('#confirm');\n    confirm.textContent = unlock ? 'Add my free gift' : 'Save my choice';\n    confirm.disabled = saving;\n\n    const skip = this.shadowRoot.querySelector<HTMLButtonElement>('#skip');\n    skip.textContent = unlock ? 'No thanks' : 'Keep my current gift';\n    skip.disabled = saving;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'offer-view': OfferView;\n  }\n}\n\nif (!customElements.get('offer-view')) {\n  customElements.define('offer-view', OfferView);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { formatMoney } from '@shopify/theme-currency';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * One gift the shopper can pick from inside the offer view. Renders a gift with purchase product.\n */\nclass GiftWithPurchaseProduct extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return [\n      'data-selected',\n      'data-price',\n      'data-image-url',\n      'data-title',\n      'data-subtitle',\n      'data-variant-id'\n    ];\n  }\n\n  declare readonly dataset: {\n    imageUrl: string;\n\n    /**\n     * The gift's regular price, in cents.\n     */\n    price: string;\n\n    /**\n     * Whether this is the gift selected in the picker. The offer view sets this.\n     */\n    selected: string;\n\n    subtitle: string;\n    title: string;\n    variantId: string;\n  };\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n    this.render();\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  public attributeChangedCallback(_name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && oldValue !== newValue) {\n      this.render();\n    }\n  }\n\n  private render() {\n    const imageEl = this.shadowRoot.querySelector('img');\n    if (imageEl) {\n      imageEl.src = this.dataset.imageUrl;\n      imageEl.alt = this.dataset.title;\n    }\n\n    const priceEl = this.shadowRoot.querySelector('span');\n    if (priceEl) {\n      const price = parseInt(this.dataset.price, 10);\n      // eslint-disable-next-line no-template-curly-in-string\n      priceEl.textContent = formatMoney(price, '${{amount_no_decimals}}');\n    }\n\n    const titleEl = this.shadowRoot.querySelector('h3');\n    if (titleEl) {\n      titleEl.textContent = this.dataset.title;\n    }\n\n    const subtitleEl = this.shadowRoot.querySelector('h4');\n    if (subtitleEl) {\n      subtitleEl.textContent = this.dataset.subtitle;\n    }\n\n    const selectedCheckEl = this.shadowRoot.querySelector<HTMLInputElement>('input[type=\"radio\"]');\n    if (this.dataset.selected === 'true') {\n      selectedCheckEl.checked = true;\n    } else {\n      selectedCheckEl.checked = false;\n    }\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'gift-with-purchase-product': GiftWithPurchaseProduct;\n  }\n}\n\nif (!customElements.get('gift-with-purchase-product')) {\n  customElements.define('gift-with-purchase-product', GiftWithPurchaseProduct);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport type { Item } from '@integrabeauty/shopify-ajax-api';\nimport { formatMoney } from '@shopify/theme-currency';\nimport { v4 as uuidv4 } from 'uuid';\nimport { remove } from '../../../lib/cart/remove.js';\nimport { update } from '../../../lib/cart/update.js';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Custom element for displaying the progress of gift with purchase promotions in the cart. The\n * element also tracks free shipping if both required attributes are present.\n *\n * The bar only shows progress while the cart is under the threshold. Picking the gift happens in\n * the offer view, which this element asks for right after a cart change the shopper made unlocks\n * the gift. Once the cart qualifies and has no gift, such as after the shopper skipped the offer,\n * this element shows the message saying they qualified, on the cart view and on the upsell view.\n */\nclass GiftWithPurchaseProgress extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-show'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Boolean flag indicating whether the gift with purchase threshold has been achieved. Used to\n     * update the visual state of the progress bar.\n     */\n    achieved: 'false' | 'true';\n\n    /**\n     * \"true\" while the cart holds a free gift. Used by the styles to hide the bar once there is\n     * nothing left on it to show.\n     */\n    claimed: 'false' | 'true';\n\n    /**\n     * Value representing the free shipping threshold (in cents). If left blank we can assume free\n     * shipping is not available currently. Only when this attribute is set can the free shipping\n     * progress be shown.\n     */\n    freeShippingThresholdCents: string;\n\n    /**\n     * Boolean flag to determine whether the promo goals progress bar should be shown.\n     */\n    show: 'false' | 'true';\n\n    /**\n     * Boolean flag to determine whether the free shipping progress should be shown. Requires the\n     * `data-free-shipping-threshold-cents` attribute to be set.\n     */\n    showFreeShipping: 'false' | 'true';\n\n    /**\n     * Text to show when free shipping is unlocked because of a subscription item in the cart\n     */\n    subscriptionText: string;\n\n    /**\n     * Minimum cart value for achieving the gift with purchase (in cents).\n     */\n    thresholdCents: string;\n\n    /**\n     * Minimum cart quantity for achieving the gift with purchase.\n     */\n    thresholdQuantity: string;\n\n    /**\n     * Type of goal for the promo progress bar. Can be either based on the cart quantity or the cart\n     * total. Used by loaded styles to determine the visual representation of the progress bar.\n     */\n    thresholdType: 'cart-quantity' | 'cart-total';\n  };\n\n  private onCartTransactionCompletedBound = this.onCartTransactionCompleted.bind(this);\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n\n  /**\n   * Set when a cart change the shopper made in this tab unlocked the gift. The offer view is asked\n   * for once that change's transaction completes, so the offer comes after the change rather than\n   * in the middle of it.\n   */\n  private unlockPending = false;\n\n  /**\n   * Set when the cart dropped below the threshold with a skip saved on it. The skip is cleared once\n   * the change that dropped it completes, for the same reason as the unlock above. Starting another\n   * cart change in the middle of one logs a concurrent transaction warning.\n   */\n  private skipClearPending = false;\n\n  /**\n   * Estimated customer cart value, exact value is calculated on checkout shipping step (in cents).\n   */\n  private estimatedTotal = 0;\n\n  /**\n   * Current quantity of items in the cart excluding any already added gift with purchase items.\n   */\n  private cartQuantity = 0;\n\n  /**\n   * Whether there is a subscription item in the cart, which unlocks free shipping regardless of the\n   * cart value.\n   */\n  private subscriptionsInCart = false;\n\n  /**\n   * The quantity steps are built once, since the threshold doesn't change after the page loads.\n   */\n  private quantityThresholdInitialized = false;\n\n  /**\n   * The free shipping marker is placed once, since the threshold doesn't change after the page\n   * loads.\n   */\n  private shippingInitialized = false;\n  private thresholdAchieved = false;\n\n  /**\n   * The gift line in the cart, if there is one. It can still be set for a gift that is about to be\n   * removed because the cart no longer qualifies.\n   */\n  private claimedGiftWithPurchase?: Item;\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdated(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n    addEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n    removeEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n  }\n\n  public attributeChangedCallback(name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && oldValue !== newValue) {\n      this.render();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onCartUpdated(event: GlobalEventHandlersEventMap['cart-updated']) {\n    const cart = event.detail.cart;\n    let subscriptionsInCart = false;\n    for (const item of cart.items) {\n      if (item?.selling_plan_allocation?.selling_plan?.recurring_deliveries) {\n        subscriptionsInCart = true;\n        break;\n      }\n    }\n\n    this.subscriptionsInCart = subscriptionsInCart;\n\n    // determine if a gift with purchase has already been claimed in the cart\n    this.claimedGiftWithPurchase = cart.items.find(\n      item => item.properties._gift_with_purchase === 'true');\n    this.estimatedTotal = cart.total_price;\n    this.cartQuantity = this.claimedGiftWithPurchase ? cart.item_count - 1 : cart.item_count;\n\n    this.updateAchievedState();\n\n    const fromThisTab = event.detail.scenario !== 'cross-tab-sync';\n    const skipped = cart.attributes?._gwp_skipped === 'true';\n\n    if (this.claimedGiftWithPurchase) {\n      // A Shopify transform function makes the gift free while the cart qualifies, so a gift line\n      // with a price on it means the cart dropped below the threshold. The transform and our own\n      // threshold check can disagree, so we also remove the gift when our check fails. The gift\n      // removal doc alongside this file has the details.\n      if (this.claimedGiftWithPurchase.final_price > 0 || !this.thresholdAchieved) {\n        // Leave the removal to the tab where the cart changed. That tab broadcasts the cart without\n        // the gift once its removal finishes, and this tab catches up then. Removing here too would\n        // race that tab, and because removal works by line position, the losing request removes\n        // whichever product moved into the gift's old line. The cost is that if that tab never\n        // finishes its removal, such as when it is closed or the removal fails, no other tab steps\n        // in, and the priced gift stays in the cart until the next cart change in any tab.\n        if (fromThisTab) {\n          remove(this, [this.claimedGiftWithPurchase.key], 'gift-with-purchase-locked')\n            .catch(console.warn);\n        }\n\n        return;\n      }\n    } else if (!this.thresholdAchieved && skipped && fromThisTab) {\n      // A skip only lasts while the cart qualifies, so crossing the threshold again is a fresh\n      // offer. When the cart still holds a gift on its way out, this waits for the cart the removal\n      // leaves behind, rather than run two cart changes at once.\n      this.skipClearPending = true;\n    } else if (this.thresholdAchieved && !skipped && this.dataset.show === 'true' &&\n      isShopperChange(event)) {\n      this.unlockPending = true;\n    }\n\n    this.render();\n  }\n\n  /**\n   * Works out whether the cart meets the gift threshold and sends an unlocked or locked event when\n   * that changes. We only send events while the bar is showing. A cart that already qualifies when\n   * the bar first shows sends no unlocked event, only later changes do.\n   */\n  private updateAchievedState() {\n    let achieved = false;\n    if (this.dataset.thresholdType === 'cart-quantity' && this.dataset.thresholdQuantity) {\n      if (this.cartQuantity >= parseInt(this.dataset.thresholdQuantity, 10)) {\n        achieved = true;\n      }\n    } else {\n      const giftThreshold = parseInt(this.dataset.thresholdCents, 10);\n      const total = this.estimatedTotal;\n      const percentage = Math.min(total / giftThreshold, 1);\n      achieved = percentage >= 1;\n    }\n\n    if (this.thresholdAchieved !== achieved && this.dataset.show === 'true') {\n      const eventType = achieved ? 'gift-with-purchase-unlocked' : 'gift-with-purchase-locked';\n      const event = new CustomEvent(eventType, {\n        bubbles: true,\n        composed: true,\n        detail: {\n          event_id: uuidv4()\n        }\n      });\n      this.dispatchEvent(event);\n    }\n\n    this.thresholdAchieved = achieved;\n    this.dataset.achieved = achieved ? 'true' : 'false';\n  }\n\n  private render() {\n    if (this.dataset.show !== 'true') {\n      return;\n    }\n\n    this.dataset.claimed = this.claimedGiftWithPurchase ? 'true' : 'false';\n\n    this.renderQualifiedMessages();\n    this.renderThresholdMessage();\n    this.renderShippingMessage();\n    this.renderShippingProgress();\n    if (this.dataset.thresholdType === 'cart-quantity') {\n      this.renderQuantityThresholdProgress();\n    } else {\n      this.renderCentsThresholdProgress();\n    }\n  }\n\n  /**\n   * The message lives on the cart view and on the upsell view, so we show or hide every copy on the\n   * page. It needs at least one gift in stock to offer.\n   */\n  private renderQualifiedMessages() {\n    const hasGifts = !!document.querySelector('offer-view gift-with-purchase-product');\n    const show = this.thresholdAchieved && !this.claimedGiftWithPurchase && hasGifts;\n    for (const message of document.querySelectorAll('gift-qualified-message')) {\n      message.hidden = !show;\n    }\n  }\n\n  /**\n   * Runs once the change that unlocked the gift is done. The cart can have moved on by then, such\n   * as when a gift was added in the meantime, so we check again.\n   */\n  private onCartTransactionCompleted(_event: WindowEventMap['cart-transaction-completed']) {\n    if (this.skipClearPending) {\n      this.skipClearPending = false;\n      if (!this.thresholdAchieved && !this.claimedGiftWithPurchase) {\n        update(this, { attributes: { _gwp_skipped: '' } }, 'gift-with-purchase-skip-cleared')\n          .catch(console.warn);\n      }\n    }\n\n    if (!this.unlockPending) {\n      return;\n    }\n\n    this.unlockPending = false;\n\n    if (!this.thresholdAchieved || this.claimedGiftWithPurchase) {\n      return;\n    }\n\n    type Detail = GlobalEventHandlersEventMap['offer-view-requested']['detail'];\n    const event = new CustomEvent<Detail>('offer-view-requested', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        reason: 'unlocked'\n      }\n    });\n    this.dispatchEvent(event);\n  }\n\n  private renderQuantityThresholdProgress() {\n    const container = this.shadowRoot.querySelector<HTMLElement>('.goals-container');\n    if (!this.dataset.thresholdQuantity) {\n      // current state has invalid configuration, fail gracefully\n      container.style.display = 'none';\n      return;\n    }\n\n    // restore visibility in case it was previously hidden\n    container.style.display = '';\n    const thresholdQuantity = parseInt(this.dataset.thresholdQuantity, 10);\n    const stepSize = Math.floor(100 / thresholdQuantity);\n\n    const goals = container.querySelector<HTMLElement>('.goals-progress');\n    // Initialize the quantity threshold progress steps if not already done\n    if (!this.quantityThresholdInitialized) {\n      // Clear existing shipping and cart total progress steps if current gift with purchase\n      // progress type is set to cart-quantity\n      goals.innerHTML = '';\n      const progressAchieved = document.createElement('div');\n      progressAchieved.classList.add('progress-achieved');\n      goals.appendChild(progressAchieved);\n\n      for (let i = 1; i <= thresholdQuantity; i++) {\n        const stepOffset = i * stepSize;\n        const goal = document.createElement('div');\n        goal.classList.add('quantity-goal');\n        goal.style.left = `${stepOffset}%`;\n        goal.dataset.quantityStep = i.toString();\n        goal.setAttribute('aria-label',\n          `Add ${thresholdQuantity} items to your cart to unlock the gift, currently have ${i}`);\n        goal.textContent = i.toString();\n        goals.appendChild(goal);\n      }\n\n      this.quantityThresholdInitialized = true;\n    }\n\n    for (let i = 1; i <= thresholdQuantity; i++) {\n      const step = goals.querySelector<HTMLElement>(`[data-quantity-step=\"${i}\"]`);\n      if (step) {\n        step.dataset.achieved = i <= this.cartQuantity ? 'true' : 'false';\n      } else {\n        console.warn(`Step element [data-quantity-step=\"${i}\"] not found.`);\n      }\n    }\n\n    const progressAchieved = container.querySelector<HTMLElement>('.progress-achieved');\n    if (this.cartQuantity >= parseInt(this.dataset.thresholdQuantity, 10)) {\n      progressAchieved.style.width = '100%';\n    } else {\n      const progressWidth = this.cartQuantity * stepSize;\n      progressAchieved.style.width = `${progressWidth}%`;\n    }\n  }\n\n  private renderCentsThresholdProgress() {\n    const container = this.shadowRoot.querySelector<HTMLElement>('.goals-container');\n    if (!this.dataset.thresholdCents) {\n      // current state has invalid configuration, fail gracefully\n      container.style.display = 'none';\n      return;\n    }\n\n    // restore visibility in case it was previously hidden\n    container.style.display = '';\n    const threshold = parseInt(this.dataset.thresholdCents, 10);\n    const giftGoal = container.querySelector<HTMLElement>('.gift-goal');\n\n    if (this.estimatedTotal >= threshold) {\n      this.thresholdAchieved = true;\n      giftGoal.classList.add('achieved');\n    } else {\n      this.thresholdAchieved = false;\n      giftGoal.classList.remove('achieved');\n    }\n\n    const progressAchieved = container.querySelector<HTMLElement>('.progress-achieved');\n    const total = this.estimatedTotal;\n    if (total <= 0) {\n      progressAchieved.style.width = '0%';\n      return;\n    }\n\n    const percentage = Math.min(total / threshold, 1);\n    progressAchieved.style.width = `${percentage * 100}%`;\n  }\n\n  /**\n   * Updates the free shipping icon on the progress bar. Responsible for updating whether to show\n   * the goal as achieved based on the current cart state or hide it if the threshold has not been\n   * configured.\n   */\n  private renderShippingProgress() {\n    if (this.dataset.showFreeShipping !== 'true') {\n      return;\n    }\n\n    // No shipping progress for cart-quantity threshold type as not incorporated into the design\n    if (this.dataset.thresholdType === 'cart-quantity') {\n      return;\n    }\n\n    const shippingGoal = this.shadowRoot.querySelector<HTMLElement>(\n      '.goals-progress .shipping-goal');\n\n    if (!this.dataset.freeShippingThresholdCents) {\n      // current state has invalid configuration, fail gracefully\n      shippingGoal.style.display = 'none';\n      return;\n    }\n\n    // restore visibility in case it was previously hidden\n    shippingGoal.style.display = 'flex';\n\n    const shippingThreshold = parseInt(this.dataset.freeShippingThresholdCents, 10);\n    const giftThreshold = parseInt(this.dataset.thresholdCents, 10);\n    const shippingPercentage = Math.min(shippingThreshold / giftThreshold, 1);\n\n    if (!this.shippingInitialized) {\n      this.shippingInitialized = true;\n      shippingGoal.style.left = `${shippingPercentage * 100}%`;\n    }\n\n    if (this.estimatedTotal > shippingThreshold || this.subscriptionsInCart) {\n      shippingGoal.classList.add('achieved');\n    } else {\n      shippingGoal.classList.remove('achieved');\n    }\n  }\n\n  /**\n   * Responsible for rendering the threshold messaging for free shipping progress in the div.heading\n   * element.\n   */\n  private renderShippingMessage() {\n    if (this.dataset.showFreeShipping !== 'true') {\n      return;\n    }\n\n    const shipping = this.shadowRoot.querySelector<HTMLElement>('.heading .shipping');\n    if (!this.dataset.freeShippingThresholdCents) {\n      // current state has invalid configuration, fail gracefully\n      shipping.style.display = 'none';\n      return;\n    }\n\n    // restore visibility in case it was previously hidden\n    shipping.style.display = 'flex';\n    const shippingThreshold = parseInt(this.dataset.freeShippingThresholdCents, 10);\n    const difference = shippingThreshold - this.estimatedTotal;\n    // eslint-disable-next-line no-template-curly-in-string\n    const amount = formatMoney(difference, '${{amount_no_decimals}}');\n    const shippingMessage = difference > 0 ?\n      `You're ${amount} away from free shipping` : 'You have free shipping!';\n    const shippingElement = shipping.querySelector('.copy');\n    if (shippingElement) {\n      shippingElement.textContent = shippingMessage;\n    }\n  }\n\n  /**\n   * Responsible for rendering the threshold messaging for gift with purchase progress in the\n   * div.heading element.\n   */\n  private renderThresholdMessage() {\n    const giftMessage = this.shadowRoot.querySelector<HTMLElement>('.heading .gift');\n    let message = '';\n    if (this.dataset.thresholdType === 'cart-quantity') {\n      if (!this.dataset.thresholdQuantity) {\n        // current state has invalid configuration, fail gracefully\n        giftMessage.style.display = 'none';\n        return;\n      }\n\n      // Once the cart qualifies, the qualified message or the gift line says so instead.\n      const thresholdQuantity = parseInt(this.dataset.thresholdQuantity, 10);\n      if (this.cartQuantity >= thresholdQuantity) {\n        giftMessage.style.display = 'none';\n        return;\n      }\n\n      // restore visibility in case it was previously hidden\n      giftMessage.style.display = 'flex';\n      const difference = thresholdQuantity - this.cartQuantity;\n      const plural = difference === 1 ? 'item' : 'items';\n      message += `Add ${difference} more ${plural} to unlock your free gift`;\n    } else {\n      if (!this.dataset.thresholdCents) {\n        // current state has invalid configuration, fail gracefully\n        giftMessage.style.display = 'none';\n        return;\n      }\n\n      // Once the cart qualifies, the qualified message or the gift line says so instead.\n      const threshold = parseInt(this.dataset.thresholdCents, 10);\n      if (threshold <= this.estimatedTotal) {\n        giftMessage.style.display = 'none';\n        return;\n      }\n\n      // restore visibility in case it was previously hidden\n      giftMessage.style.display = 'flex';\n      const difference = threshold - this.estimatedTotal;\n      // eslint-disable-next-line no-template-curly-in-string\n      const amount = formatMoney(difference, '${{amount_no_decimals}}');\n      message = `${amount} until free gift unlocked`;\n    }\n\n    const copy = giftMessage.querySelector('.copy');\n    copy.textContent = message;\n  }\n}\n\n/**\n * Whether the cart change came from something the shopper did in this tab. The cart loading with\n * the page, a cart synced from another tab, and the gift's own changes do not count.\n */\nfunction isShopperChange(event: GlobalEventHandlersEventMap['cart-updated']) {\n  if (event.detail.is_initial) {\n    return false;\n  }\n\n  const scenario = event.detail.scenario || '';\n  if (scenario === 'cross-tab-sync' || scenario === 'bfcache-reconcile') {\n    return false;\n  }\n\n  return !scenario.startsWith('gift-with-purchase');\n}\n\ntype GiftWithPurchaseEvent = CustomEvent<{\n  /**\n   * UUID representing a unique instance of gift with purchase event by a customer.\n   */\n  event_id: string;\n}>;\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    'gift-with-purchase-claimed': GiftWithPurchaseEvent;\n    'gift-with-purchase-locked': GiftWithPurchaseEvent;\n    'gift-with-purchase-unlocked': GiftWithPurchaseEvent;\n  }\n\n  interface HTMLElementTagNameMap {\n    'gift-with-purchase-progress': GiftWithPurchaseProgress;\n  }\n}\n\nif (!customElements.get('gift-with-purchase-progress')) {\n  customElements.define('gift-with-purchase-progress', GiftWithPurchaseProgress);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { formatMoney } from '@shopify/theme-currency';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * This element is responsible for showing multi-tiered promotion progress in the users shopping\n * cart. It reacts to a cart updated event to calculate if the cart qualifies for any of the\n * promotional goals.\n *\n * Currently, the element is being used exclusively to test free shipping, which is both applicable\n * when the estimated cart total reaches a certain threshold or when there is a subscription item in\n * the cart.\n *\n * This is the second version of the promo goals progress, which is a replacement for the original.\n * The original promo goals progress element is still available as a separate custom element, but it\n * is not currently in use.\n *\n * @todo This element currently assumes that the only promo-goal is free shipping. If we want to\n * support multiple promo-goals, we will need to update the logic to not make this assumption.\n */\nclass PromoGoalProgressV2 extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-update'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Boolean flag to determine whether the promo goals progress bar should be shown.\n     */\n    show: 'false' | 'true';\n\n    /**\n     * Text to show when free shipping is unlocked because of a subscription item in the cart\n     */\n    subscriptionText: string;\n\n    /**\n     * Minimum cart value for free shipping (in cents).\n     */\n    threshold: string;\n\n    /**\n     * Mechanism to update the promo goals progress bar after threshold updates are processed.\n     */\n    update: string;\n  };\n\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n\n  /**\n   * Estimated customer cart value, exact value is calculated on checkout shipping step (in cents).\n   */\n  private estimatedTotal = 0;\n\n  /**\n   * Whether there is a subscription item in the cart, which unlocks free shipping regardless of the\n   * cart value.\n   */\n  private subscriptionsInCart = false;\n\n  private declare promoGoals: PromoGoal[];\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  public attributeChangedCallback(name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (oldValue === newValue) {\n      return;\n    }\n\n    if (this.isConnected) {\n      this.ingestPromoGoals();\n      this.render();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onCartUpdated(event: GlobalEventHandlersEventMap['cart-updated']) {\n    const cart = event.detail.cart;\n    this.estimatedTotal = cart.total_price;\n\n    let subscriptionsInCart = false;\n    for (const item of cart.items) {\n      if (item?.selling_plan_allocation?.selling_plan?.recurring_deliveries) {\n        subscriptionsInCart = true;\n        break;\n      }\n    }\n\n    this.subscriptionsInCart = subscriptionsInCart;\n\n    this.render();\n  }\n\n  private ingestPromoGoals() {\n    const goals = this.querySelectorAll('promo-goal-v2');\n\n    this.promoGoals = [];\n    for (const goal of goals) {\n      this.promoGoals.push({\n        name: goal.dataset.offerText,\n        threshold: parseInt(goal.dataset.thresholdCents, 10)\n      });\n    }\n  }\n\n  private render() {\n    if (!this.promoGoals) {\n      this.ingestPromoGoals();\n    }\n\n    const span = this.shadowRoot.querySelector('span');\n    const progressContainer = this.shadowRoot.querySelector('.progress-container');\n    const progressBar = this.shadowRoot.querySelector<HTMLElement>('.progress-bar');\n\n    span.textContent = this.getMessageText();\n\n    let percentage: number;\n    if (this.subscriptionsInCart) {\n      percentage = 100;\n    } else {\n      const threshold = parseInt(this.dataset.threshold, 10);\n      percentage = Math.min(100, (this.estimatedTotal / threshold) * 100);\n    }\n\n    percentage = Math.round(percentage);\n    if (isNaN(percentage)) {\n      percentage = 0;\n    }\n\n    progressContainer.setAttribute('aria-valuenow', percentage.toString());\n    progressBar.style.width = percentage + '%';\n\n    const goalNamesContainer = this.shadowRoot.querySelector('.goal-names');\n    goalNamesContainer.innerHTML = '';\n\n    // Loop through promo goals to add goal names to the progress bar\n    let goalIndex = 0;\n    for (const goal of this.promoGoals) {\n      goalIndex++;\n      const nameDiv = document.createElement('div');\n      nameDiv.classList.add('name');\n\n      const spanDiv = document.createElement('span');\n      spanDiv.textContent = goal.name;\n      nameDiv.appendChild(spanDiv);\n\n      if (this.estimatedTotal >= goal.threshold) {\n        nameDiv.classList.add('achieved');\n      }\n\n      if (this.promoGoals.length === goalIndex) {\n        nameDiv.classList.add('last');\n      } else {\n        const threshold = parseInt(this.dataset.threshold, 10);\n        const percentage = Math.min(100, (goal.threshold / threshold) * 100);\n        nameDiv.style.left = `${percentage}%`;\n      }\n\n      goalNamesContainer.appendChild(nameDiv);\n    }\n\n    // Loop through promo goals to add threshold markers to the progress bar\n    const thresholdsContainer = this.shadowRoot.querySelector('.goal-thresholds');\n    thresholdsContainer.innerHTML = '';\n    goalIndex = 0;\n    for (const goal of this.promoGoals) {\n      goalIndex++;\n      const thresholdElement = document.createElement('div');\n      thresholdElement.classList.add('threshold');\n      // eslint-disable-next-line no-template-curly-in-string\n      thresholdElement.textContent = formatMoney(goal.threshold, '${{amount_no_decimals}}');\n\n      if (this.promoGoals.length === goalIndex) {\n        thresholdElement.classList.add('last');\n      } else {\n        const threshold = parseInt(this.dataset.threshold, 10);\n        const percentage = Math.min(100, (goal.threshold / threshold) * 100);\n        thresholdElement.style.left = `${percentage}%`;\n      }\n\n      thresholdsContainer.appendChild(thresholdElement);\n    }\n  }\n\n  private getMessageText() {\n    if (this.subscriptionsInCart) {\n      return this.dataset.subscriptionText ||\n        'Free shipping unlocked with subscription item!';\n    }\n\n    const threshold = parseInt(this.dataset.threshold, 10);\n    const difference = threshold - this.estimatedTotal;\n\n    if (difference > 0) {\n      // eslint-disable-next-line no-template-curly-in-string\n      return 'You\\'re ' + formatMoney(difference, '${{amount}}') +\n        ' away from Free Shipping!';\n    } else {\n      return 'You\\'ve Earned Free Contiguous US Shipping!';\n    }\n  }\n}\n\ninterface PromoGoal {\n  name: string;\n  threshold: number;\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'promo-goal-progress-v2': PromoGoalProgressV2;\n  }\n}\n\nif (!customElements.get('promo-goal-progress-v2')) {\n  customElements.define('promo-goal-progress-v2', PromoGoalProgressV2);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport type { Cart } from '@integrabeauty/shopify-ajax-api';\nimport { apply } from '../../../lib/cart/discount/apply.js';\nimport { getApplicableDiscountCodes, getDiscountAmounts, getDiscountCodes } from '../../../lib/cart/discount/discount.js';\nimport { remove } from '../../../lib/cart/discount/remove.js';\nimport { read } from '../../../lib/cart/state.js';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders a text field for someone to type in a discount code and apply it to the cart. This also\n * renders the apply button, some error messages, the currently applied code(s), and a way of\n * removing discounts.\n */\nclass DiscountInput extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * Currency code\n     */\n    currencyCode: string;\n\n    /**\n     * Currency Symbol (e.g., $)\n     */\n    currencySymbol: string;\n  };\n\n  /**\n   * The most recent cart state. Both the codes we show and the amounts we show beside them are\n   * derived from this when rendering, so the two always describe the same cart.\n   */\n  private cart: Cart = null;\n\n  /**\n   * Number of active transactions. This helps guard against applying / removing multiple codes at\n   * the same time\n   */\n  private activeTransactions = 0;\n\n  /**\n   * Last error message. If not set, the error box won't be displayed\n   */\n  private errorMessage: string | null = null;\n\n  private onCartTransactionStartedBound = this.onCartTransactionStarted.bind(this);\n  private onCartTransactionEndedBound = this.onCartTransactionEnded.bind(this);\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n  private onCartUpdateErredBound = this.onCartUpdateErred.bind(this);\n  private onDiscountFormSubmitBound = this.onDiscountFormSubmit.bind(this);\n  private onDiscountInputBound = this.onDiscountInput.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    const form = this.shadowRoot.querySelector<HTMLFormElement>('#discount-form');\n    form.addEventListener('submit', this.onDiscountFormSubmitBound);\n\n    const inputElement = this.shadowRoot.querySelector<HTMLInputElement>('#input');\n    inputElement.addEventListener('input', this.onDiscountInputBound);\n\n    // we might have missed some events while disconnected, so we reset the transactions lock\n    this.activeTransactions = 0;\n    // and the error message\n    this.errorMessage = null;\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n    addEventListener('cart-transaction-completed', this.onCartTransactionEndedBound);\n    addEventListener('cart-transaction-started', this.onCartTransactionStartedBound);\n\n    // This element applies discounts and dispatches its own failures from itself, so we listen here\n    // rather than at global scope. That way it only receives the failures it caused, without having\n    // to match on a scenario token to recognize them.\n\n    this.addEventListener('cart-update-erred', this.onCartUpdateErredBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    const form = this.shadowRoot.querySelector<HTMLFormElement>('#discount-form');\n    form?.removeEventListener('submit', this.onDiscountFormSubmitBound);\n\n    const inputElement = this.shadowRoot.querySelector<HTMLInputElement>('#input');\n    inputElement?.removeEventListener('input', this.onDiscountInputBound);\n\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n    removeEventListener('cart-transaction-completed', this.onCartTransactionEndedBound);\n    removeEventListener('cart-transaction-started', this.onCartTransactionStartedBound);\n\n    this.removeEventListener('cart-update-erred', this.onCartUpdateErredBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private render() {\n    // The cart describes its discounts in two separate places and we need both of them here. The\n    // discount_codes array is the list of codes sitting on the cart, along with whether each one\n    // actually took, and it carries no money. The discount applications carry the money, but they\n    // are labeled with a title rather than a code. So we build the list from the codes, because a\n    // code is also what the remove button has to send back to Shopify, and then we look up each\n    // amount by matching a title against a code.\n\n    const codes = getApplicableDiscountCodes(this.cart);\n    const discountAmounts = getDiscountAmounts(this.cart);\n\n    // An unmatched amount is one whose title matches none of the codes we are about to display.\n    // That happens when the application's title is not a code string, or when the cart holds an\n    // allocation for a code it does not mark applicable, since we only display applicable ones.\n    // Nothing downstream needs this, so we say it and carry on.\n    //\n    // We log rather than warn because it is routine rather than a fault, and we log rather than\n    // debug because log is the tier that becomes a Sentry breadcrumb. It is rare and it describes\n    // real cart state, so it is worth carrying along if some unrelated error is reported later. The\n    // mismatch in the other direction, a code we show with no amount to put beside it, stays at\n    // debug below because it fires for every applicable code with no amount, which is routine for a\n    // free shipping code.\n\n    const unmatched = Array.from(discountAmounts.keys()).filter(title => !codes.includes(title));\n\n    if (unmatched.length > 0) {\n      console.log('Found unmatched discount applications:', unmatched.join(', '));\n    }\n\n    const appliedDiscountsEl = this.shadowRoot.querySelector('.applied-discounts');\n    appliedDiscountsEl.innerHTML = '';\n\n    const appliedDiscountTemplate = this.shadowRoot.querySelector<HTMLTemplateElement>(\n      '#applied-discount-template');\n\n    for (const code of codes) {\n      const appliedDiscountFragment = this.createAppliedDiscount(appliedDiscountTemplate, code,\n        discountAmounts.get(code));\n      appliedDiscountsEl.appendChild(appliedDiscountFragment);\n    }\n\n    const input = this.shadowRoot.querySelector<HTMLInputElement>('#input');\n    input.disabled = this.activeTransactions > 0;\n\n    const submitButton = this.shadowRoot.querySelector<HTMLButtonElement>('#submit-button');\n    submitButton.disabled = input.value.length < 3 || this.activeTransactions > 0;\n\n    const alert = this.shadowRoot.querySelector('alert-element');\n    if (this.errorMessage) {\n      alert.dataset.alertType = 'danger';\n      alert.innerHTML = '';\n      const span = document.createElement('span');\n      span.textContent = this.errorMessage;\n      alert.appendChild(span);\n      alert.hidden = false;\n    } else {\n      alert.hidden = true;\n    }\n  }\n\n  /**\n   * Builds one row for the list of applied discounts, showing the code, the amount coming off the\n   * cart for it, and a button to remove it. The amount is optional because a code can be applied\n   * without the cart allocating anything to it, and the row hides its amount in that case.\n   *\n   * The caller passes the template in because a row is built per code and the template is the same\n   * one every time, so looking it up here would repeat that work for every row.\n   */\n  private createAppliedDiscount(template: HTMLTemplateElement, code: string, amount: number) {\n    const appliedDiscountFragment = <DocumentFragment>template.content.cloneNode(true);\n\n    const discountInfo = appliedDiscountFragment.querySelector<HTMLDivElement>('.discount-info');\n    discountInfo.dataset.code = code;\n\n    const codeEl = discountInfo.querySelector('.discount-code');\n    codeEl.textContent = code;\n\n    if (Number.isInteger(amount)) {\n      // if the amount is found, we display it\n      const amountEl = discountInfo.querySelector('price-element');\n      amountEl.dataset.subunits = amount.toString();\n      amountEl.dataset.currencyCode = this.dataset.currencyCode;\n      amountEl.dataset.currencySymbol = this.dataset.currencySymbol;\n    } else {\n      console.debug('Trying to display discount application, but no amount found', code);\n\n      // if the amount is not found, we still display the code, but we hide the amount\n      for (const infoElement of discountInfo.querySelectorAll<HTMLElement>('.amount-info')) {\n        infoElement.hidden = true;\n      }\n    }\n\n    const cta = appliedDiscountFragment.querySelector('button');\n    cta.dataset.code = code;\n    cta.setAttribute('aria-label', 'Remove discount code ' + code);\n    cta.addEventListener('click', onRemoveDiscountClicked);\n    if (this.activeTransactions > 0) {\n      cta.disabled = true;\n    } else {\n      cta.disabled = false;\n    }\n\n    return appliedDiscountFragment;\n  }\n\n  private onCartTransactionStarted(_event: WindowEventMap['cart-transaction-started']) {\n    this.activeTransactions += 1;\n    this.render();\n  }\n\n  private onCartTransactionEnded(_event: WindowEventMap['cart-transaction-completed']) {\n    // there is a completed event without a matching started event on page load - ignore it\n    if (this.activeTransactions > 0) {\n      this.activeTransactions -= 1;\n    }\n\n    this.render();\n  }\n\n  /**\n   * Whenever someone is typing in the input, we have to consider enabling or disabling the apply\n   * button.\n   */\n  private onDiscountInput(_event: Event) {\n    this.render();\n  }\n\n  private onDiscountFormSubmit(event: SubmitEvent) {\n    event.preventDefault();\n\n    const input = this.shadowRoot.querySelector<HTMLInputElement>('#input');\n\n    const newCodes = [input.value];\n\n    // Both of these carry logic here, not just analytics, unlike most other cart mutations.\n    // Dispatching the failure from this element is what lets this element's own listener catch it\n    // and render the message beside the input. The scenario lets the global listener that renders\n    // errors at the top of the cart drawer recognize this case and suppress its own message,\n    // deferring to the messaging shown beside the input.\n\n    const scenario = 'cart-discount-input';\n    const dispatcher = this;\n\n    // The wrapper in finally is important because it keeps render bound to this instance.\n\n    apply(dispatcher, newCodes, scenario)\n      .then(() => input.value = '')\n      .catch(console.warn)\n      .finally(() => this.render());\n  }\n\n  private onCartUpdateErred(event: GlobalEventHandlersEventMap['cart-update-erred']) {\n    // Because this listener is attached to the element itself rather than to global scope, it only\n    // receives failures that reach this element. Discount failures from other sources, such as\n    // application on page load or as part of adding to the cart, originate elsewhere and never\n    // arrive here; those are surfaced by a separate global listener that renders errors at the top\n    // of the cart drawer.\n\n    // Handle only failures dispatched at this element, the ones it caused, and not any that merely\n    // bubble up through it from a descendant. By convention this element dispatches its own\n    // discount failures from itself, so this holds, but asserting it makes that a local guarantee\n    // rather than something inherited from how errors happen to be dispatched elsewhere.\n\n    if (event.target !== this) {\n      return;\n    }\n\n    // Narrow to discount application failures so that any other kind of failure this element might\n    // dispatch is not mistaken for one.\n\n    if (event.detail.code !== 'APPLY_DISCOUNTS') {\n      return;\n    }\n\n    // Try to get the existing codes\n\n    let existingCodes: string[];\n    try {\n      if (isFullyDefinedStringArray(event.detail.inputs?.before_codes) &&\n        event.detail.inputs.before_codes.length > 0) {\n        existingCodes = event.detail.inputs.before_codes.map(before => before.toUpperCase());\n      } else {\n        const cart = read();\n        // this ignores whether a discount code is currently applicable as we are only using this to\n        // decide what error message to show\n        existingCodes = getDiscountCodes(cart);\n      }\n    } catch (error) {\n      console.warn(error);\n    }\n\n    // If there are already some codes, then one of the reasons a discount does not apply is because\n    // the existing codes are combinable with the new codes and the existing codes provide a better\n    // discount. We try to provide a better error message in this case.\n    if (existingCodes?.length > 0) {\n      this.errorMessage = 'Oops. It looks like this discount could not be applied. But don\\'t ' +\n        'worry, you\\'re already getting the best deal!';\n    } else {\n      this.errorMessage = 'There was a problem applying the discount.' +\n        ' Please try again in a few moments';\n    }\n\n    this.render();\n  }\n\n  private onCartUpdated(event: GlobalEventHandlersEventMap['cart-updated']) {\n    // update the cart we render from, both on initial page load and when the cart state changes\n\n    this.cart = event.detail.cart;\n\n    // On any update, we have a successful update. If there was a prior update error, there may be\n    // still an error message. Hide that error message.\n    this.errorMessage = null;\n    // trigger a render\n    this.render();\n  }\n}\n\nfunction onRemoveDiscountClicked(event: MouseEvent) {\n  // Use the current target rather than the target or the bound receiver. The current target is by\n  // definition the element this listener is attached to, the remove button, regardless of which\n  // inner element actually received the click and without depending on how this function is\n  // invoked. That makes it the reliable choice both for reading the code and as the failure origin.\n\n  const target = event.currentTarget;\n\n  if (target instanceof HTMLButtonElement && !target.disabled) {\n    target.disabled = true;\n\n    const code = target.dataset.code;\n    const scenario = 'cart-discount-input-remove-click';\n\n    // The dispatcher is the clicked button, the element most directly responsible for the removal.\n    // This is a standalone listener bound to the button, so the button is what is in scope; the\n    // surrounding input element would have to be reached by climbing out of its shadow tree. It\n    // also makes no observable difference to listeners outside that tree, since the failure event\n    // composes out of it and is seen as originating from the surrounding element either way.\n\n    const dispatcher = target;\n\n    remove(dispatcher, code, scenario).catch(console.warn);\n  }\n}\n\nfunction isFullyDefinedStringArray(value: any): value is string[] {\n  return Array.isArray(value) && value.every(element => typeof element === 'string');\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'discount-input': DiscountInput;\n  }\n}\n\nif (!customElements.get('discount-input')) {\n  customElements.define('discount-input', DiscountInput);\n}\n","import type { Cart } from '@integrabeauty/shopify-ajax-api';\nimport { digest } from '../digest.js';\nimport { report } from '../error.js';\nimport { read, write } from '../state.js';\nimport { start } from '../transaction.js';\nimport { getApplicableDiscountCodes } from './discount.js';\nimport { applyDiscountsHelper } from './helper.js';\n\n/**\n * Applies discount codes to the cart.\n *\n * Do not include existing codes in new codes. Only include new codes in new codes.\n *\n * This does not check whether client state is desynced because it does not matter.\n *\n * @param target the element most directly responsible for the apply, used to originate any failure\n * event with truthful provenance instead of at global scope\n * @param newCodes the new discount codes to apply, excluding existing codes\n * @param scenario optional description or code describing the context in which this takes place\n * @param postValidate whether a new code that ends up applied but not applicable counts as a\n * failure, which it always does for an empty cart\n */\nexport async function apply(target: EventTarget | null, newCodes: string[], scenario?: string,\n  postValidate = true) {\n  const transaction = start('applyDiscountCodes', newCodes, scenario);\n\n  let beforeCart: Cart;\n\n  // TODO: this is cheating by relying on a global declared in the cart lifecycle section, which is\n  // a different section. we need a different way to load overrides, such as in their own section.\n\n  try {\n    beforeCart = read();\n    const afterCart = await applyDiscountsHelper(beforeCart, newCodes,\n      cart_lifecycle.discount_overrides, postValidate);\n    write(target, afterCart, transaction, scenario);\n  } catch (error) {\n    // In this catch clause we must be extra paranoid not to introduce additional errors so we can\n    // make very few assumptions about state.\n\n    // Downstream analytics looks at codes that failed to apply. Downstream tools do simple string\n    // aggregation. If we use raw codes, which can be in mixed case, we end up with one bucket for\n    // each variation of character case. To make things easier we emit normalized values.\n    const normalNewCodes = Array.isArray(newCodes) ? newCodes.map(code => code?.toUpperCase()) : [];\n\n    report(target, {\n      code: 'APPLY_DISCOUNTS',\n      error,\n      inputs: {\n        before_cart: digest(beforeCart),\n        before_codes: getApplicableDiscountCodes(beforeCart),\n        new_codes: normalNewCodes\n      },\n      scenario,\n      transaction\n    });\n  } finally {\n    transaction.complete();\n  }\n}\n","import { cartUpdate, type Cart } from '@integrabeauty/shopify-ajax-api';\nimport { digest } from '../digest.js';\nimport { report } from '../error.js';\nimport { cartFetch, read, write } from '../state.js';\nimport { start } from '../transaction.js';\nimport { getDiscountCodes } from './discount.js';\n\n/**\n * Remove a specific discount code from the cart.\n *\n * This leverages a special flag configured in the lange store that enables the discount property to\n * be specified in the cart update request body. This special feature is not documented.\n *\n * Shopify does not provide an API for removing a specific code. We figured out that the discount\n * property accepts a comma separated values list of discounts to apply. Specifying the empty string\n * means remove all. In order to remove a specific discount, we specify a csv of all codes other\n * than the specific code to remove, which effectively removes that discount code. If omitting the\n * discount code results in an empty csv string, then effectively all codes are removed.\n *\n * @param target the element most directly responsible for the removal, used to originate any\n * failure event with truthful provenance instead of at global scope\n * @param code the discount code to remove\n * @param scenario optional description or code describing the context in which this takes place\n */\nexport async function remove(target: EventTarget | null, code: string, scenario?: string) {\n  const transaction = start('removeDiscount', code, scenario);\n\n  let previousCart: Cart;\n\n  try {\n    // In order to remove the discount codes, we need to determine the new set of codes to apply. We\n    // have to preserve any existing codes. The trick to removal is that we basically just filter\n    // out the code to be removed from the list of existing codes, then apply the resulting set of\n    // codes.\n\n    previousCart = read();\n\n    // When obtaining previous codes, we ignore whether the code is applicable. We want to retain\n    // inapplicable codes in the update.\n\n    const previousCodes = getDiscountCodes(previousCart);\n\n    const newCodes = previousCodes.filter(previousCode => previousCode !== code.toUpperCase());\n\n    // The code being removed may be the only applied code, in which case newCodes is an empty\n    // array. When newCodes is an empty array, the join returns an empty string. When discount is an\n    // empty string, cartUpdate is effectively removing all discount codes.\n\n    const discount = newCodes.join(',');\n    await cartUpdate({ discount });\n    const cart = await cartFetch();\n    write(target, cart, transaction, scenario);\n  } catch (error) {\n    report(target, {\n      code: 'REMOVE_DISCOUNT',\n      error,\n      inputs: {\n        code,\n        previous_cart: digest(previousCart)\n      },\n      scenario,\n      transaction\n    });\n  } finally {\n    transaction.complete();\n  }\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders a promo goal. Used in slot of promo-goal-progress.\n */\nclass PromoGoal extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return [\n      'data-currency-code',\n      'data-currency-symbol',\n      'data-is-locked',\n      'data-offer-text',\n      'data-threshold-cents'\n    ];\n  }\n\n  declare readonly dataset: {\n    /**\n     * The ISO code of the currency\n     *\n     * @example USD\n     */\n    currencyCode: string;\n\n    /**\n     * Currency symbol\n     *\n     * @example $\n     */\n    currencySymbol: string;\n\n    /**\n     * The state of the goal\n     */\n    isLocked: 'false' | 'true';\n\n    /**\n     * Text that describes the promotional goal\n     */\n    offerText: string;\n\n    /**\n     * The amount of money the customer needs to spend to unlock the promotional goal in cents\n     */\n    thresholdCents: string;\n  };\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    this.render();\n  }\n\n  public attributeChangedCallback(_name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && newValue !== oldValue) {\n      this.render();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private render() {\n    if (!this.dataset.thresholdCents ||\n      !this.dataset.currencySymbol ||\n      !this.dataset.currencyCode ||\n      !this.dataset.offerText) {\n      return;\n    }\n\n    const thresholdAmount = parseInt(this.dataset.thresholdCents || '0', 10);\n\n    const label = this.shadowRoot.querySelector('.label');\n\n    // if the promo goal does not have a spend threshold, then it is instantly applicable. in this\n    // case we just need to render 'get y' not 'spend x, get y'\n    label.innerHTML = thresholdAmount === 0 ?\n      `Get ${this.dataset.offerText}!` :\n      `Spend <price-element\n      data-currency-symbol=\"${this.dataset.currencySymbol}\"\n      data-currency-code=\"${this.dataset.currencyCode}\"\n      data-subunits=\"${this.dataset.thresholdCents}\">\n      </price-element>, get ${this.dataset.offerText}!`;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'promo-goal': PromoGoal;\n  }\n}\n\nif (!customElements.get('promo-goal')) {\n  customElements.define('promo-goal', PromoGoal);\n}\n","import type { Cart } from '@integrabeauty/shopify-ajax-api';\n\n/**\n * Computes how much of the cart counts toward the next promo goal threshold.\n *\n * We want the total after discounts, except that we want to exclude the promotional discount\n * itself. As a result, we cannot work with the original total price or the price after discounts.\n * So we have to create a custom total of discounts and subtract this from the original total price\n * to get a custom price after discounts.\n *\n * This is a front end mirror of the qualifying amount computed server side by the backend tiered\n * discount functions. If those functions change what counts toward a tier, this must change with\n * them or the progress bar will mislead.\n */\nexport function computeQualifyingSubtotal(cart: Cart) {\n  const originalTotalPrice = cart.original_total_price;\n\n  // The total amount of discounts, regardless of the type of discount\n  const totalDiscountPrice = calculateTotalDiscount(cart);\n\n  // This is the total amount discounted by promo goal discounts (e.g. BMSM)\n  const totalPromoGoalDiscountPrice = calculateTotalPromoGoalDiscount(cart);\n\n  const totalPriceOfExcludedItems = calculateTotalPriceOfPromoExcludedItems(cart);\n\n  const priceAfterDiscounts = originalTotalPrice - totalDiscountPrice;\n  const priceAfterDiscountsExceptPromoGoalDiscounts = priceAfterDiscounts +\n    totalPromoGoalDiscountPrice - totalPriceOfExcludedItems;\n\n  return priceAfterDiscountsExceptPromoGoalDiscounts;\n}\n\nfunction calculateTotalDiscount(cart: Cart) {\n  let total = 0;\n  for (const application of cart.cart_level_discount_applications) {\n    total += application.total_allocated_amount;\n  }\n\n  for (const item of cart.items) {\n    for (const allocation of item.line_level_discount_allocations) {\n      total += allocation.amount;\n    }\n  }\n\n  return total;\n}\n\nfunction calculateTotalPriceOfPromoExcludedItems(cart: Cart) {\n  let total = 0;\n\n  // There are also $0 items to exclude, but since they are 0, we just ignore them since they do not\n  // change the total.\n\n  for (const item of cart.items) {\n    if (item.gift_card) {\n      total += item.final_line_price;\n    }\n  }\n\n  return total;\n}\n\nfunction calculateTotalPromoGoalDiscount(cart: Cart) {\n  let total = 0;\n\n  for (const application of cart.cart_level_discount_applications) {\n    if (isPromoNamedDiscount(application.title)) {\n      total += application.total_allocated_amount;\n    }\n  }\n\n  for (const item of cart.items) {\n    for (const allocation of item.line_level_discount_allocations) {\n      if (isPromoNamedDiscount(allocation.discount_application.title)) {\n        total += allocation.discount_application.total_allocated_amount;\n      }\n    }\n  }\n\n  return total;\n}\n\nfunction isPromoNamedDiscount(value: string) {\n  if (typeof value !== 'string') {\n    return false;\n  }\n\n  const upper = value.toUpperCase();\n\n  if (upper.startsWith('BUY MORE SAVE MORE')) {\n    return true;\n  }\n\n  // This is a special $20 off that is allocated over line items. This is also a type of promo goal.\n  if (upper.includes('$20 DISCOUNT APPLIED')) {\n    return true;\n  }\n\n  if (/SPEND \\$[0-9]+ GET [0-9]+% OFF/.test(upper)) {\n    return true;\n  }\n\n  return false;\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport type { Cart } from '@integrabeauty/shopify-ajax-api';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport html from './index.html';\nimport styles from './index.scss';\nimport { computeQualifyingSubtotal } from './qualifying-subtotal.js';\n\n/**\n * Takes promo-goal web elements as inputs in its slot and renders a progress bar.\n */\nclass PromoGoalProgress extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * Active currency ISO code, used when rendering the amounts in the progress message.\n     *\n     * @example \"USD\"\n     */\n    currencyCode: string;\n\n    /**\n     * Currency symbol character, used when rendering the amounts in the progress message.\n     *\n     * @example \"$\"\n     */\n    currencySymbol: string;\n  };\n\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n  private declare cart: Cart;\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onCartUpdated(event: GlobalEventHandlersEventMap['cart-updated']) {\n    this.cart = event.detail.cart;\n    this.render();\n  }\n\n  private render() {\n    if (!this.cart) {\n      return;\n    }\n\n    const qualifyingSubtotal = computeQualifyingSubtotal(this.cart);\n\n    const paragraph = this.shadowRoot.querySelector('p');\n\n    const promoGoals = this.collectPromoGoalsData();\n\n    if (promoGoals.length === 0) {\n      paragraph.hidden = true;\n      return;\n    }\n\n    let eligibleThreshold = 0;\n    const eligiblePromoGoals = [];\n\n    // The goals array is sorted from lowest threshold to highest\n\n    for (const promoGoal of promoGoals) {\n      // When the cart subtotal (estimated total) is greater than or equal to the threshold, then\n      // the promo goal has already been unlocked and can be skipped.\n      if (qualifyingSubtotal >= promoGoal.thresholdCents) {\n        promoGoal.element.dataset.isLocked = 'false';\n        continue;\n      }\n\n      promoGoal.element.dataset.isLocked = 'true';\n\n      // If there are no eligible promo goals, add them now\n      if (eligiblePromoGoals.length === 0) {\n        eligiblePromoGoals.push(promoGoal);\n        // also capture any other promo goals with identical threshold\n        eligibleThreshold = promoGoal.thresholdCents;\n        continue;\n      }\n\n      if (eligibleThreshold === promoGoal.thresholdCents) {\n        eligiblePromoGoals.push(promoGoal);\n        continue;\n      }\n    }\n\n    if (eligiblePromoGoals.length === 0) {\n      paragraph.hidden = true;\n    }\n\n    const thresholdAmount = eligiblePromoGoals.length > 0 ?\n      eligiblePromoGoals[0].thresholdCents :\n      promoGoals[promoGoals.length - 1].thresholdCents;\n\n    const amountToGoal = thresholdAmount - qualifyingSubtotal;\n    const offerText = eligiblePromoGoals.map(i => i.offerText).join(' and ');\n\n    paragraph.innerHTML = `Add <price-element\n      data-currency-code=\"${this.dataset.currencyCode}\"\n      data-currency-symbol=\"${this.dataset.currencySymbol}\"\n      data-subunits=\"${amountToGoal}\">\n      </price-element> to unlock ${offerText}!`;\n\n    const percentSpentToGoal = Math.min(100, Math.round(\n      (qualifyingSubtotal / thresholdAmount) * 100));\n    const progressEl = this.shadowRoot.querySelector('#progress');\n    progressEl.setAttribute('style', `width: ${percentSpentToGoal}%;`);\n\n    type PriceElement = HTMLElementTagNameMap['price-element'];\n    const spentEl = this.shadowRoot.querySelector<PriceElement>('#spent');\n    spentEl.dataset.currencyCode = this.dataset.currencyCode;\n    spentEl.dataset.currencySymbol = this.dataset.currencySymbol;\n\n    if (qualifyingSubtotal >= promoGoals[promoGoals.length - 1].thresholdCents) {\n      spentEl.dataset.subunits = '';\n    } else {\n      spentEl.dataset.subunits = qualifyingSubtotal.toString();\n    }\n\n    const goalEl = this.shadowRoot.querySelector<PriceElement>('#goal');\n    goalEl.dataset.currencyCode = this.dataset.currencyCode;\n    goalEl.dataset.currencySymbol = this.dataset.currencySymbol;\n    goalEl.dataset.subunits = thresholdAmount.toString();\n\n    if (eligiblePromoGoals.length > 0) {\n      paragraph.hidden = false;\n    }\n\n    const progressBar = progressEl.parentElement;\n    progressBar.setAttribute('aria-valuenow', percentSpentToGoal.toString());\n    if (percentSpentToGoal < 100) {\n      progressBar.setAttribute('aria-label', [\n        this.dataset.currencySymbol,\n        (qualifyingSubtotal / 100).toFixed(2),\n        ' of ',\n        this.dataset.currencySymbol,\n        // threshold amounts will usually be rounded to a full dollar\n        (thresholdAmount / 100).toString(),\n        ' goal'\n      ].join(''));\n    } else {\n      progressBar.setAttribute('aria-label', [\n        this.dataset.currencySymbol,\n        (thresholdAmount / 100).toString(),\n        ' goal reached'\n      ].join(''));\n    }\n  }\n\n  private collectPromoGoalsData() {\n    const elements = this.querySelectorAll('promo-goal');\n\n    const goals: PromoGoal[] = [];\n    for (const element of elements) {\n      const goal = <PromoGoal>{};\n      goal.currencyCode = element.dataset.currencyCode;\n      goal.currencySymbol = element.dataset.currencySymbol;\n      goal.isLocked = element.dataset.isLocked === 'true';\n      goal.offerText = element.dataset.offerText;\n      goal.thresholdCents = parseInt(element.dataset.thresholdCents, 10);\n      goal.element = element;\n      goals.push(goal);\n    }\n\n    return goals.sort(compareThresholds);\n  }\n}\n\nfunction compareThresholds(a: PromoGoal, b: PromoGoal) {\n  if (a.thresholdCents === b.thresholdCents) {\n    return 0;\n  }\n\n  return a.thresholdCents < b.thresholdCents ? -1 : 1;\n}\n\ninterface PromoGoal {\n  currencyCode: string;\n  currencySymbol: string;\n  element: HTMLElement;\n  isLocked: boolean;\n  offerText: string;\n  thresholdCents: number;\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'promo-goal-progress': PromoGoalProgress;\n  }\n}\n\nif (!customElements.get('promo-goal-progress')) {\n  customElements.define('promo-goal-progress', PromoGoalProgress);\n}\n","import { measure, now } from './perf.js';\n\n/**\n * Waits for the runtime's load to reach a terminal state. Resolves true once a client is ready to\n * read, and false once the runtime has failed or the given number of milliseconds passes.\n *\n * The caller does not have to care whether the runtime loaded before or after it, which is the\n * whole point. Arriving late is covered by reading the load state, because the broadcast has\n * already been and gone by then, and arriving early is covered by listening for the broadcast.\n * Between them there is no ordering a caller can lose to.\n *\n * A failure resolves as soon as it is known rather than waiting out the timeout. That matters\n * because two of the failures are decided long before any caller's deadline. The theme editor is\n * known before a single bundle runs, and a missing anonymous id is known the moment the runtime\n * looks for one. Sitting out two or three seconds for an answer that already exists would delay\n * everything downstream of the value for nothing.\n *\n * Anything other than pending is terminal, which is what the check tests for, so a failure reason\n * added later needs no change here.\n *\n * On a preview theme the wait is extended, because the runtime deliberately holds the broadcast\n * open there to give the GrowthBook DevTools extension time to push forced variant overrides in. A\n * caller that gave up partway through that hold would silently drop the forced variant and make the\n * extension unreliable to debug with. How long the hold lasts comes off the runtime global, so the\n * two sides cannot drift apart into a hold that outlives the wait covering it.\n *\n * Neither the preview check nor the hold length can be read from anything the runtime's boot script\n * writes, because that script loads async and can execute after a caller has already asked. Both\n * the Shopify globals and the runtime's inline seed are set in the head instead, so they are always\n * available.\n *\n * The timeout stays as a floor for the one case no broadcast can cover, the boot script never\n * running at all. On that path the listener is left registered rather than torn down, since a\n * broadcast that does eventually arrive resolves a promise that has already settled, which changes\n * nothing.\n */\nfunction whenLoaded(milliseconds: number) {\n  return new Promise<boolean>(resolve => {\n    if (growthbook_runtime.load_state !== 'pending') {\n      resolve(growthbook_runtime.load_state === 'ready');\n      return;\n    }\n\n    const extraMillis = Shopify.previewMode ? growthbook_runtime.preview_hold_ms : 0;\n    const timer = setTimeout(resolve, milliseconds + extraMillis, false);\n\n    addEventListener('growthbook-load-settled', () => {\n      clearTimeout(timer);\n      resolve(growthbook_runtime.load_state === 'ready');\n    }, { once: true });\n  });\n}\n\n/**\n * Fault-tolerant wrapper around GrowthBook's own getFeatureValue SDK method.\n *\n * In GrowthBook the unit is a feature (a feature flag) identified by a feature key. The feature's\n * value for the current user is decided by evaluating its rules. One kind of rule is an experiment\n * rule (a \"feature flag experiment\"): it randomly buckets the user into a variation and returns\n * that variation's value. So the same feature key can resolve to a static flag value or to an A/B\n * test assignment, and this call returns whatever it resolves to. The caller cannot tell which, and\n * does not need to. When the feature is backed by an experiment rule, the returned value is the\n * user's assigned variation.\n *\n * Calling this is not a passive read, and that is the thing to know before adding a call. Bucketing\n * happens during the read, and the SDK reports the exposure to the runtime's tracking callback on\n * the way back out, which is what puts the visitor in the experiment's analytics. So the moment a\n * section calls this, that visitor counts as having seen whatever variation came back.\n *\n * That makes where a call sits matter as much as what it does with the answer. A read the visitor\n * could not possibly have seen still counts, and every such visitor lands in both groups and\n * behaves the same in both, which drags the measured difference between them toward zero. So gate\n * the call on the thing being on the page and reachable, rather than reading up front and deciding\n * later.\n *\n * On top of the raw SDK call this wrapper adds:\n *\n * - Fault tolerance. On a timeout, an error, or an undefined feature it returns the caller's\n *   fallback instead of throwing. It does assume the runtime global is present, which the always-on\n *   layer guarantees, so a page that somehow lacks it rejects rather than falling back.\n * - A shared, pre-initialized client. It reads the single client the cross-section coordination\n *   global exposes, so the SDK is initialized once per page rather than once per call.\n * - Indifference to load order. It waits on the runtime's ready signal rather than on the client\n *   promise existing yet, so a caller whose bundle runs before the runtime's is not penalized for\n *   it.\n * - A bounded wait. It always resolves within the given timeout, or within a global default when no\n *   per-call timeout is passed.\n * - Per-page memoization. The first resolved value for a feature key is cached and returned for\n *   every later call in the same page load. Once the payload has loaded this buys nothing on its\n *   own, because the SDK evaluates from attributes that do not change during a page, so repeated\n *   reads would agree with each other anyway. Where it earns its place is the timeout boundary. A\n *   caller that gave up waiting holds the fallback and the page has already acted on it, the client\n *   can become ready a moment later, and without it the next caller would get the real value and\n *   disagree with what the page is showing.\n *\n * @param featureKey The GrowthBook feature key.\n * @param fallback The value returned when the feature is undefined or when the lookup times out or\n * errors.\n * @param timeoutMs Optional per-call override for the lookup timeout, in milliseconds.\n *\n * @see https://docs.growthbook.io/lib/js#quick-usage GrowthBook JS SDK getFeatureValue\n * @see https://docs.growthbook.io/feature-flag-experiments How A/B tests are delivered via features\n * @see https://docs.growthbook.io/features/basics Feature flag fundamentals (keys, values, rules)\n */\nexport async function getFeatureValue<T extends string = string>(featureKey: string,\n  fallback: NoInfer<T>, timeoutMs?: number) {\n  const resolvedFeatures = growthbook_runtime.resolved_features;\n  if (resolvedFeatures[featureKey]) {\n    return <T>resolvedFeatures[featureKey];\n  }\n\n  let value: T | null = null;\n\n  // The span starts here rather than at function entry, so that it only ever covers a real wait.\n  // The memoized return above answers without waiting on anything, and timing it would put a span\n  // on the timeline for every read in the page. Leaving it outside means a span showing up at all\n  // is a caller that actually waited. Closing the span in a finally is what covers the timeout and\n  // the failure paths, which are the ones most worth a number.\n\n  const startMillis = now();\n  const effectiveTimeoutMs = timeoutMs ?? growthbook_runtime.default_timeout_ms;\n\n  try {\n    const loaded = await whenLoaded(effectiveTimeoutMs);\n\n    // The runtime either failed or ran out of time, and neither is reported here so as to avoid\n    // Sentry noise, so we return instead of throwing. The fallback is cached so that later calls\n    // for the same feature key in the same page load get the same answer, since a feature read in\n    // several places has to agree with itself.\n\n    if (!loaded) {\n      resolvedFeatures[featureKey] = fallback;\n      return fallback;\n    }\n\n    // The signal says the client is ready, so this await is already settled and only defends\n    // against the impossible case of the two disagreeing.\n\n    const growthbook = await growthbook_runtime.client_loader;\n\n    if (!growthbook) {\n      resolvedFeatures[featureKey] = fallback;\n      return fallback;\n    }\n\n    // Passing null here rather than the caller's fallback is deliberate. The SDK applies its own\n    // default only on a strict null check against the evaluated value, so handing it null makes\n    // this a straight pass-through of whatever the feature evaluated to, and every \"nothing usable\"\n    // case then lands on the single type check below.\n    //\n    // The evaluated value is null for an unknown feature key, for a feature with no default value\n    // configured, and for an unmet prerequisite. Passing the fallback here would cover exactly\n    // those and nothing else.\n    //\n    // What it would not cover is a value that is present but not a string. A feature configured\n    // with a number, a boolean, or a JSON value evaluates to that value, the SDK's null check does\n    // not fire, and the value comes back untouched. Undefined slips through for the same reason. So\n    // the type check below is load-bearing either way, and routing everything through it keeps the\n    // decision about what counts as usable in one place.\n\n    value = <T | null>growthbook.getFeatureValue<string | null>(featureKey, null);\n  } catch (error) {\n    console.warn('Error getting feature value from GrowthBook:', error);\n  } finally {\n    measure('growthbook-feature-value', startMillis, now(), {\n      feature: featureKey,\n      timeout: effectiveTimeoutMs\n    });\n  }\n\n  // The value comes from GrowthBook, so its type is an unverified boundary assertion. Coalesce\n  // anything that is not a string (null, or a missing or unexpected value) to the caller's default,\n  // so the resolved value is always a defined string.\n\n  if (typeof value !== 'string') {\n    value = fallback;\n  }\n\n  resolvedFeatures[featureKey] = value;\n\n  return value;\n}\n\n/**\n * Compares a value read through {@link getFeatureValue} against an expected value, ignoring case.\n * Every comparison against a feature value goes through this, even where a plain equality check\n * would do today, so that the casing decision lives in one place and new code copied from an\n * existing call site picks it up.\n *\n * The value is GrowthBook boundary data whose casing is not guaranteed, and a feature has been seen\n * returning an uppercase value. The expected side can be typed by hand too, such as an experiment\n * id typed into a section setting in the theme editor. Neither side's casing is enforced anywhere,\n * so we lowercase both. We leave the value itself untouched in the wrapper, so a caller reading it\n * directly still sees exactly what GrowthBook served.\n *\n * @param value The feature value\n * @param expected The value to compare against\n */\nexport function featureValueMatches(value: string, expected: string) {\n  return value.toLowerCase() === expected.toLowerCase();\n}\n","import { cartUpdate } from '@integrabeauty/shopify-ajax-api';\nimport { getEventQueue, isCartUpdatedEvent } from '../../lib/cart-event-queue.js';\nimport { ready } from '../../lib/dom-ready.js';\nimport { getFeatureValue, featureValueMatches } from '../../lib/growthbook-feature.js';\n\n/**\n * We are intentionally not using a transaction to lock down the UI here or rewrite the global page\n * cart state. We do not think it is needed here. Beware that this relies on other code that checks\n * for this attribute to not rely on the cart state from events, unless it so happens to be based on\n * updated state after a later fetch. So this is a point in time where local cart state in the cart\n * lifecycle goes stale.\n *\n * Shopify's update cart endpoint only mutates the specified attributes. We do not need to gather\n * all attributes first in order to ensure they are preserved. We are just writing the attributes we\n * own and leaving all of the other attributes unaffected. This function is written with the\n * assumption that there is a point to the update.\n *\n * This should not be called when the attribute already exists. The caller should be checking first.\n */\nfunction onGetFeatureValue(promoProgress: HTMLElementTagNameMap['promo-goal-progress-v2'],\n  group: string, cartTotal: number) {\n  // We have just found out which group this shopper is in, and this is the only point in this page\n  // view where we know it. The attribute write below does not come back around to us as a cart\n  // updated event, so nothing else is going to show the bar. If we leave it to the attribute, the\n  // shopper sees no bar at all until their next page load fetches the cart from Shopify and tells\n  // us the group all over again.\n\n  // Only group \"2\" sees the free shipping progress bar when the cart total is at least $75\n  if (featureValueMatches(group, '2') && cartTotal >= 7500) {\n    promoProgress.dataset.show = 'true';\n  }\n\n  // Only group \"1\" sees the free shipping progress bar unconditionally\n  if (featureValueMatches(group, '1')) {\n    promoProgress.dataset.show = 'true';\n  }\n\n  // We clear the attribute from the first shipping experiment at the same time. Shoppers who were\n  // in that experiment still carry it on their cart, and Shopify copies every cart attribute onto\n  // the order's note attributes at checkout, so it keeps turning up in reporting long after that\n  // experiment ended. Setting a value to null is how we remove a key.\n  return cartUpdate({\n    attributes: {\n      _aux_opt_in: null,\n      _aux_opt_in_v2: null,\n      _aux_opt_in_v3: group\n    }\n  });\n}\n\nfunction onCartUpdated(event: GlobalEventHandlersEventMap['cart-updated']) {\n  const promoProgress = document.querySelector<HTMLElementTagNameMap['promo-goal-progress-v2']>(\n    'cart-element promo-goal-progress-v2');\n\n  // Since the feature gate determining whether to render the promo goals progress bar is server\n  // side, we need to check if the promo goals progress bar exists to determine if the experiment is\n  // active. If the promo goals progress bar does not exist, we can exit early.\n  if (!promoProgress) {\n    return;\n  }\n\n  // Once a shopper's group is written on their cart, every later cart update comes back through\n  // here, so this is what shows the bar on all of their page loads after the one where we picked\n  // the group. We stop instead of falling through, because reading the feature again would report\n  // another exposure to GrowthBook for a shopper we already bucketed.\n\n  // If the shopper is in the \"2\" group they are shown the $89 Free Shipping progress bar when the\n  // cart total price is at least $75\n  if (event.detail.cart.attributes?._aux_opt_in_v3 === '2') {\n    // Since cart total price changes in both directions, we must also hide the progress bar when\n    // the total falls below the threshold.\n    if (event.detail.cart.total_price >= 7500) {\n      promoProgress.dataset.show = 'true';\n    } else {\n      promoProgress.dataset.show = 'false';\n    }\n\n    return;\n  }\n\n  // If the shopper is in the \"1\" group they are shown the $89 Free Shipping progress bar\n  // unconditionally\n  if (event.detail.cart.attributes?._aux_opt_in_v3 === '1') {\n    promoProgress.dataset.show = 'true';\n    return;\n  }\n\n  // Shoppers bucketed into \"old\" group get no progress bar, so there is nothing to render. We still\n  // have to stop here so that we do not fall through and read the feature a second time.\n  if (event.detail.cart.attributes?._aux_opt_in_v3 === '0') {\n    return;\n  }\n\n  getFeatureValue('shipping-suppression-experiment', '0')\n    .then(value => onGetFeatureValue(promoProgress, value, event.detail.cart.total_price))\n    .catch(console.warn);\n}\n\nfunction onReady() {\n  for (const event of getEventQueue()) {\n    if (isCartUpdatedEvent(event)) {\n      try {\n        onCartUpdated(event);\n      } catch (error) {\n        console.warn(error);\n      }\n    }\n  }\n\n  addEventListener('cart-updated', onCartUpdated);\n}\n\nready(onReady);\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * A thin progress bar for a wait whose real progress we cannot measure, where all we know is the\n * longest the wait can last. A small ball moves along a line and gets all the way to the end right\n * when that longest time is up. It moves quickly at first and slows down as it goes, so a short\n * wait still looks like it got somewhere and a long one never looks stuck. The bar fades in slowly,\n * so a wait that ends quickly barely shows it.\n *\n * The bar is timed entirely by CSS animations, so there is no state to keep and nothing for a\n * caller to start or stop. It starts over each time it goes from hidden to shown, for example when\n * the cart drawer switches to the progress view. Whoever places it has to set its duration through\n * the `--progress-bar-custom-duration` custom property, and without one the ball never moves.\n */\nclass ProgressBarCustom extends HTMLElement implements CustomElement {\n  public connectedCallback() {\n    this.initializeShadowRoot();\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'progress-bar-custom': ProgressBarCustom;\n  }\n}\n\nif (!customElements.get('progress-bar-custom')) {\n  customElements.define('progress-bar-custom', ProgressBarCustom);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * A view of the cart drawer saying that something is in progress, shown while a cart change is in\n * flight. Whoever asks the drawer for this view passes the message along with the request, so the\n * view itself knows nothing about which change it is waiting on. Add to cart is one user, and the\n * drawer itself is another while the cart initializes. The drawer handles showing and hiding it,\n * and switching back to the cart, so this element only lays out the view, shows the message, and\n * asks the drawer to close when its close button is clicked.\n */\nclass ProgressView extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-focus-requested', 'data-message'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Set to \"true\" by the cart drawer when it shows this view, asking the view to put focus on its\n     * close button. The view removes it again once it has, so the next request fires too.\n     */\n    focusRequested: string;\n\n    /**\n     * \"embedded\" on the cart page, where there is no drawer to close. Set in the liquid.\n     */\n    layout: string;\n\n    /**\n     * The text to show, such as \"Just a moment ...\". Set by the drawer control from the open\n     * request, or by the drawer while the cart initializes.\n     */\n    message: string;\n  };\n\n  private onCloseClickedBound = this.onCloseClicked.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n    this.renderMessage();\n\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton.addEventListener('click', this.onCloseClickedBound);\n  }\n\n  public attributeChangedCallback(name: string, _oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && name === 'data-focus-requested' && newValue === 'true') {\n      this.focusCloseButton();\n    }\n\n    if (this.isConnected && name === 'data-message') {\n      this.renderMessage();\n    }\n  }\n\n  public disconnectedCallback() {\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton?.removeEventListener('click', this.onCloseClickedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  /**\n   * See the upsell view for why focus is forced visible here.\n   */\n  private focusCloseButton() {\n    delete this.dataset.focusRequested;\n    this.shadowRoot.querySelector('close-button').focus({ focusVisible: true });\n  }\n\n  private renderMessage() {\n    const messageEl = this.shadowRoot.querySelector('#message');\n    messageEl.textContent = this.dataset.message || '';\n  }\n\n  private onCloseClicked(_event: MouseEvent) {\n    type Detail = WindowEventMap['cart-close-requested']['detail'];\n    const event = new CustomEvent<Detail>('cart-close-requested', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        animate: true,\n        source: 'close-button'\n      }\n    });\n    this.dispatchEvent(event);\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'progress-view': ProgressView;\n  }\n}\n\nif (!customElements.get('progress-view')) {\n  customElements.define('progress-view', ProgressView);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * The pre-checkout upsell, shown as a view of the cart drawer. The drawer switches to it when a\n * shopper clicks Checkout, and \"Back to Bag\" switches back. The drawer handles showing and hiding,\n * so this element only lays out the view, keeps its product list in sync with the cart, and asks\n * the drawer to close when its close button is clicked.\n */\nclass UpsellView extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-focus-requested'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Currency ISO code. Required.\n     *\n     * @example \"US\"\n     */\n    currencyCode: string;\n\n    /**\n     * Currency Symbol. Required.\n     *\n     * @example \"US\"\n     */\n    currencySymbol: string;\n\n    /**\n     * Set to \"true\" by the cart drawer when it shows this view, asking the view to put focus on its\n     * close button. The view removes it again once it has, so the next request fires too.\n     */\n    focusRequested: string;\n\n    /**\n     * Subtitle copy to display in the pre-checkout upsell\n     */\n    subtitle: string;\n\n    /**\n     * Title copy to display in the pre-checkout upsell\n     */\n    title: string;\n  };\n\n  private declare cartVariantIds: number[];\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n  private onCloseClickedBound = this.onCloseClicked.bind(this);\n\n  public connectedCallback() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton.addEventListener('click', this.onCloseClickedBound);\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  public attributeChangedCallback(name: string, _oldValue: string, newValue: string) {\n    if (this.isConnected && name === 'data-focus-requested' && newValue === 'true') {\n      this.focusCloseButton();\n    }\n  }\n\n  public disconnectedCallback() {\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton?.removeEventListener('click', this.onCloseClickedBound);\n\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  /**\n   * Focus moves here programmatically, so without forcing the visible-focus state the browser\n   * suppresses the focus ring after a mouse-driven open or switch, and the just-focused close\n   * button gives no visual cue of where focus landed. Older browsers that ignore the option degrade\n   * to an unadorned programmatic focus.\n   */\n  private focusCloseButton() {\n    delete this.dataset.focusRequested;\n    this.shadowRoot.querySelector('close-button').focus({ focusVisible: true });\n  }\n\n  private render() {\n    const titleEl = this.shadowRoot.querySelector('.header h2');\n    titleEl.textContent = this.dataset.title;\n\n    const subtitleEl = this.shadowRoot.querySelector('.header p');\n    subtitleEl.textContent = this.dataset.subtitle;\n\n    if (this.cartVariantIds?.length < 1) {\n      return;\n    }\n\n    const upsellItems = this.querySelectorAll<HTMLElement>('li[data-product-id]');\n    for (const upsellItem of upsellItems) {\n      const variantId = parseInt(upsellItem.dataset.variantId, 10);\n      // When the variant is already in the cart, hidden becomes true and the item is not shown.\n      // When it is not in the cart, hidden becomes false and the item is shown. We only upsell\n      // something the shopper does not already have.\n      upsellItem.hidden = this.cartVariantIds.includes(variantId);\n    }\n  }\n\n  private onCartUpdated(event: GlobalEventHandlersEventMap['cart-updated']) {\n    this.cartVariantIds = event.detail.cart.items.map(item => item.variant_id);\n\n    this.render();\n  }\n\n  private onCloseClicked(_event: MouseEvent) {\n    type Detail = WindowEventMap['cart-close-requested']['detail'];\n    const event = new CustomEvent<Detail>('cart-close-requested', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        animate: true,\n        source: 'close-button'\n      }\n    });\n    this.dispatchEvent(event);\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'upsell-view': UpsellView;\n  }\n}\n\nif (!customElements.get('upsell-view')) {\n  customElements.define('upsell-view', UpsellView);\n}\n","import type { Cart } from '@integrabeauty/shopify-ajax-api';\nimport { v4 as uuidv4 } from 'uuid';\nimport { read } from '../../lib/cart/state.js';\n\n/**\n * Reports the start of checkout when the visitor clicks the checkout button.\n *\n * Checkout can occur on other websites such as Shopify's Shop Pay checkout. We do not have a\n * trivial way of recording the start of checkout when a checkout page loads. Therefore we have to\n * try to fire the event before navigating to checkout.\n *\n * This watches for the click from out here rather than being called by the cart actions element, so\n * that the element's own click handling stays concerned only with navigating to checkout. It is a\n * best effort report, and observing in parallel means it cannot delay or prevent the navigation.\n * The button lives inside a shadow root, so the click is identified from the composed path rather\n * than from the target, which is retargeted to the host by the time it reaches us. The report is\n * then dispatched from the button itself, since that is the element responsible for it. Bubbling\n * out of the shadow root is what carries it to the window level consumers that read it.\n *\n * The event is dispatched via setTimeout. Non-native events are evaluated synchronously by\n * listeners. Dispatching via setTimeout ensures that no long-running listener introduces extra\n * delay before the redirect occurs. It is vital that the redirect occurs quickly. There is already\n * a concerning lag that happens when navigating to checkout that we do not want to increase.\n */\nfunction onClick(event: MouseEvent) {\n  const targets = event.composedPath();\n  const target = targets.find(target => target instanceof HTMLButtonElement &&\n    target.id === 'checkout-button');\n  if (target) {\n    setTimeout(emit, 0, target);\n  }\n}\n\nfunction emit(target: EventTarget) {\n  const cart = read();\n\n  const startedEvent = new CustomEvent<CheckoutStartedEvent['detail']>('checkout-started', {\n    bubbles: true,\n    cancelable: false,\n    composed: true,\n    detail: {\n      cart,\n      event_id: uuidv4()\n    }\n  });\n\n  target.dispatchEvent(startedEvent);\n}\n\n/**\n * Announces that the visitor is on their way to checkout.\n */\ntype CheckoutStartedEvent = CustomEvent<{\n  /**\n   * Snapshot of the cart taken at the moment of the click.\n   */\n  cart: Cart;\n\n  /**\n   * Identifies this occurrence so downstream pixels can deduplicate it.\n   */\n  event_id: string;\n}>;\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    'checkout-started': CheckoutStartedEvent;\n  }\n}\n\ndocument.addEventListener('click', onClick);\n","import { v4 as uuidv4 } from 'uuid';\nimport { getEventQueue, isCartUpdateErredEvent } from '../../lib/cart-event-queue.js';\n\n// Drives showing and hiding the cart drawer. It owns the open and close requests the rest of the\n// page dispatches, the light dismiss gesture the modal manager broadcasts, the decision to open on\n// a failed cart init, and the state attributes on the drawer element that everything else reads.\n// This lives beside the section index rather than inside it because the index is only there to pull\n// the section's pieces together, and it lives outside the drawer element because the requests come\n// from all over the page rather than from the element itself.\n\n/**\n * Opens the cart drawer. Emits a cart-drawer-opened event and a cart-viewed event.\n *\n * The cart-drawer-opened event is primarily only intended for downstream analytics, not managing UI\n * state. Avoid implementing any ui management logic that uses this event.\n *\n * Callers ask to open without knowing whether the drawer is already open, the same way they do when\n * closing, so this mirrors close and owns that check itself. The source is only for diagnostics.\n *\n * The view is the one to show, such as \"progress\", and missing means the cart. A request naming a\n * view while the drawer is already open switches to that view. The message is handed to the view as\n * its `data-message`, for views that show one.\n */\nfunction open(animate: boolean, source: string, view?: string, message?: string) {\n  const cartDrawer = document.querySelector<HTMLElement>(\n    'cart-drawer:not([data-layout=\"embedded\"])'\n  );\n\n  // Not finding the drawer is normal and not a problem. The cart page renders the drawer element in\n  // its embedded layout, which is always shown, so there is no drawer to open there.\n  if (!cartDrawer) {\n    return;\n  }\n\n  if (message) {\n    const viewEl = cartDrawer.querySelector<HTMLElement>(`[slot=\"${view || 'cart'}-view\"]`);\n    if (viewEl) {\n      viewEl.dataset.message = message;\n    }\n  }\n\n  // The drawer marks itself open with this flag while it is shown, so an open request that arrives\n  // while it is already open is redundant and the rest of this would re-open an open drawer. Only a\n  // request naming a view still has something to do, which is switching to it.\n  if (cartDrawer.dataset.show === 'true') {\n    if (view) {\n      cartDrawer.dataset.view = view;\n    } else {\n      console.log('Drawer already open, ignoring open request from', source);\n    }\n\n    return;\n  }\n\n  // The drawer opens on the cart unless the request names another view, whichever view it was\n  // showing when it last closed. Resetting here rather than on close keeps the old view in place\n  // while the panel slides out. While the cart is still initializing there is no cart to show yet,\n  // so it opens on the progress view instead.\n  if (view) {\n    cartDrawer.dataset.view = view;\n  } else if (cartDrawer.dataset.cartInitializing === 'true') {\n    cartDrawer.dataset.view = 'progress';\n  } else {\n    cartDrawer.dataset.view = 'cart';\n  }\n\n  // Messages left from the last time the drawer was open are stale now. Clearing them here rather\n  // than on close keeps them in place while the panel slides out, the same as the view.\n  //\n  // This also clears a message the shopper never saw. If they close the drawer while an add is\n  // still running, the add finishes while the drawer is closed and its confirmation gets cleared\n  // the next time they open it. We are fine with that, because a shopper who closed the drawer\n  // before the add finished probably does not care about seeing the confirmation later.\n  const cartElement = cartDrawer.querySelector('cart-element');\n  if (cartElement) {\n    cartElement.dataset.clearMessages = 'true';\n  }\n\n  cartDrawer.dataset.animate = animate ? 'true' : 'false';\n  cartDrawer.dataset.show = 'true';\n\n  console.log('Cart opened', source);\n\n  const showEvent = new Event('modal-open-requested', { bubbles: true, composed: true });\n  cartDrawer.dispatchEvent(showEvent);\n\n  addEventListener('modal-dismiss-requested', onModalDismissRequested);\n\n  type CartDrawerOpenedEvent = GlobalEventHandlersEventMap['cart-drawer-opened'];\n  const openedEvent = new CustomEvent<CartDrawerOpenedEvent['detail']>('cart-drawer-opened', {\n    bubbles: true,\n    composed: true,\n    detail: {\n      event_id: uuidv4()\n    }\n  });\n  cartDrawer.dispatchEvent(openedEvent);\n\n  type CartViewedEvent = GlobalEventHandlersEventMap['cart-viewed'];\n  const viewedEvent = new CustomEvent<CartViewedEvent['detail']>('cart-viewed', {\n    bubbles: true,\n    composed: true,\n    detail: {\n      surface: 'drawer'\n    }\n  });\n  cartDrawer.dispatchEvent(viewedEvent);\n}\n\n/**\n * Closes the cart drawer.\n *\n * This bails out unless the drawer is actually open, which makes closing idempotent at the source.\n * Callers ask to close without knowing whether the drawer is open. The installment message closes\n * the cart preemptively before opening its own modal even though on a product page the cart is\n * almost never open, and the drawer's own close control can be activated twice in quick succession\n * before the first close finishes. Without this guard each of those still announces a close to the\n * modal manager, which then sees a close for a modal it does not have recorded as active and logs a\n * mismatch warning. Mirroring the early return that the open path already uses keeps a redundant\n * close from reaching the manager at all, and it also avoids broadcasting a closed event for a\n * close that never happened. Production diagnostics confirmed this unguarded close was the source\n * of those warnings and that the page was already fully torn down in every case, so returning here\n * strands nothing.\n *\n * The source is only for diagnostics, the same as on open.\n */\nfunction close(animate: boolean, source: string) {\n  // Drop the dismiss listener unconditionally, ahead of the early returns below. It is registered\n  // on the window while the drawer is open, so it outlives the drawer element: if the section is\n  // rebuilt and replaced while open, the replacement comes back without the open flag, and the\n  // guards below would then skip cleanup and leak the listener. Removal is idempotent and has no\n  // observable effect when nothing is registered, unlike the event broadcasts further down, which\n  // must stay behind the guard so a redundant close stays silent.\n  removeEventListener('modal-dismiss-requested', onModalDismissRequested);\n\n  const cartDrawer = document.querySelector<HTMLElement>(\n    'cart-drawer:not([data-layout=\"embedded\"])'\n  );\n\n  // Not finding the drawer is normal and not a problem. The cart page renders the drawer element in\n  // its embedded layout, which is always shown, so there is no drawer to close there.\n  if (!cartDrawer) {\n    return;\n  }\n\n  // The drawer marks itself open with this flag while it is shown, so anything other than open\n  // means there is nothing to close and the rest of this would be a redundant close.\n  if (cartDrawer.dataset.show !== 'true') {\n    console.log('Not closing cart because the drawer is not open, close request from', source);\n    return;\n  }\n\n  cartDrawer.dataset.animate = animate ? 'true' : 'false';\n  cartDrawer.dataset.show = 'false';\n\n  console.log('Cart closed', source);\n\n  const closeEvent = new Event('modal-close-requested', { bubbles: true, composed: true });\n  cartDrawer.dispatchEvent(closeEvent);\n\n  const closedEvent = new Event('cart-drawer-closed', { bubbles: true, composed: true });\n  cartDrawer.dispatchEvent(closedEvent);\n}\n\nfunction onCartOpenRequested(event: WindowEventMap['cart-open-requested']) {\n  const animate = event.detail?.animate ?? true;\n  const source = event.detail?.source;\n  open(animate, source, event.detail?.view, event.detail?.message);\n}\n\nfunction onCartCloseRequested(event: WindowEventMap['cart-close-requested']) {\n  const animate = event.detail?.animate ?? true;\n  const source = event.detail?.source;\n  close(animate, source);\n}\n\n/**\n * Closes the cart drawer when the user performs a light-dismiss gesture (a click on the backdrop or\n * an escape key press) while it is the open modal. The modal manager broadcasts the request; we\n * close with animation since this is a user-initiated dismissal.\n */\nfunction onModalDismissRequested(_event: WindowEventMap['modal-dismiss-requested']) {\n  console.log('Closing cart on modal-dismiss-requested');\n  close(true, 'modal-dismiss');\n}\n\n/**\n * Opens the drawer when cart init fails, so the shopper sees that it failed. The message itself is\n * rendered separately by cart-messages in response to the same error, so all we do here is make it\n * visible. We have no other way to show the shopper that message. Only an init failure opens the\n * drawer. The other cart errors happen while the drawer is already in use, so they are left to\n * their own handling.\n *\n * We keep listening after the first failure rather than unregistering, because cart init can happen\n * more than once during the life of the page.\n */\nfunction onCartUpdateErred(event: GlobalEventHandlersEventMap['cart-update-erred']) {\n  if (event.detail.code === 'INIT' || event.detail.code === 'INIT_TIMEOUT') {\n    // The error message renders in the cart view. Naming it keeps the drawer off the progress view\n    // in case the drawer hears about the failure after this does.\n    open(true, 'cart-lifecycle-error', 'cart');\n  }\n}\n\nfunction main() {\n  // Init can fail before this bundle runs, because this bundle is deferred and the lifecycle\n  // snippet starts init while the page is still parsing. A listener we register now would never\n  // hear about that failure, and the drawer would stay shut over an error message already rendered\n  // inside it. So we replay the buffered cart events for a failure we missed, then listen for the\n  // ones still to come.\n\n  const cartUpdateErrorEvents = getEventQueue().filter(isCartUpdateErredEvent);\n\n  if (cartUpdateErrorEvents.length > 0) {\n    console.log('Replaying %d cart error events in drawer-control', cartUpdateErrorEvents.length);\n  }\n\n  for (const event of cartUpdateErrorEvents) {\n    onCartUpdateErred(event);\n  }\n\n  addEventListener('cart-update-erred', onCartUpdateErred);\n  addEventListener('cart-close-requested', onCartCloseRequested);\n  addEventListener('cart-open-requested', onCartOpenRequested);\n}\n\nmain();\n","import { cartUpdate } from '@integrabeauty/shopify-ajax-api';\nimport { getEventQueue, isCartUpdatedEvent } from '../../lib/cart-event-queue.js';\nimport { ready } from '../../lib/dom-ready.js';\nimport { getFeatureValue, featureValueMatches } from '../../lib/growthbook-feature.js';\n\n// Runs the gift with purchase experiment for the cart drawer. The progress bar renders hidden, and\n// we show it to shoppers GrowthBook puts in the test group. We save the group on the cart so later\n// page loads can show or skip the bar without asking GrowthBook again.\n\nfunction onGetFeatureValue(promoProgress: HTMLElementTagNameMap['gift-with-purchase-progress'],\n  group: string) {\n  // We have just found out which group this shopper is in, and this is the only point in this page\n  // view where we know it. The attribute write below does not come back around to us as a cart\n  // updated event, so nothing else is going to show the bar. If we leave it to the attribute, the\n  // shopper sees no bar at all until their next page load fetches the cart from Shopify and tells\n  // us the group all over again.\n\n  // Only group \"1\" sees the progress bar unconditionally\n  if (featureValueMatches(group, '1')) {\n    promoProgress.dataset.show = 'true';\n  }\n\n  return cartUpdate({\n    attributes: {\n      _gwp_limited: group\n    }\n  });\n}\n\nfunction onCartUpdated(event: GlobalEventHandlersEventMap['cart-updated']) {\n  const promoProgress = document.querySelector('gift-with-purchase-progress');\n\n  // Since the feature gate determining whether to render the gift with purchase progress bar is\n  // server side, we need to check if the gift with purchase progress bar exists to determine if the\n  // experiment is active. If the gift with purchase progress bar does not exist, we can exit early.\n  if (!promoProgress) {\n    return;\n  }\n\n  // Once a shopper's group is written on their cart, every later cart update comes back through\n  // here, so this is what shows the bar on all of their page loads after the one where we picked\n  // the group. We stop instead of falling through, because reading the feature again would report\n  // another exposure to GrowthBook for a shopper we already bucketed.\n\n  // If the shopper is in the \"1\" group they are shown the gift with purchase progress bar\n  if (event.detail.cart.attributes?._gwp_limited === '1') {\n    promoProgress.dataset.show = 'true';\n    return;\n  }\n\n  // Shoppers bucketed into \"0\" group get no progress bar, so there is nothing to render. We still\n  // have to stop here so that we do not fall through and read the feature a second time.\n  if (event.detail.cart.attributes?._gwp_limited === '0') {\n    return;\n  }\n\n  getFeatureValue('gift-with-purchase-limited', '0')\n    .then(value => onGetFeatureValue(promoProgress, value))\n    .catch(console.warn);\n}\n\nfunction onReady() {\n  for (const event of getEventQueue()) {\n    if (isCartUpdatedEvent(event)) {\n      try {\n        onCartUpdated(event);\n      } catch (error) {\n        console.warn(error);\n      }\n    }\n  }\n\n  addEventListener('cart-updated', onCartUpdated);\n}\n\nready(onReady);\n"],"names":["getEventQueue","cart_lifecycle","queue","isCartUpdatedEvent","value","Event","type","isCartUpdateErredEvent","ready","callback","document","readyState","timer","setTimeout","clearTimeout","addEventListener","removeEventListener","CartActions","HTMLElement","checkoutInitSucceeded","onCheckoutButtonClickedBound","this","onCheckoutButtonClicked","bind","onCartTransactionStartedBound","onCartTransactionStarted","onCartTransactionCompletedBound","onCartTransactionCompleted","onCartUpdatedBound","onCartUpdated","onReadyBound","onReady","onPageShowBound","onPageShow","onContinueShoppingButtonClickedBound","onContinueShoppingButtonClicked","connectedCallback","initializeShadowRoot","releaseReady","disconnectedCallback","checkoutButton","shadowRoot","querySelector","continueShopping","attachShadow","mode","innerHTML","isConnected","continueButton","dataset","hideCloseButton","remove","event","error","console","warn","button","currentTarget","HTMLButtonElement","log","disabled","sessionStorage","setItem","Date","now","toString","cartDrawer","closest","upsellView","querySelectorAll","length","cart_drawer","upsell_modal_displayed","view","cartElement","clearMessages","url","URL","Shopify","routes","root","skipShopPay","searchParams","set","location","href","_event","endLoading","detail","is_initial","persisted","CustomEvent","bubbles","composed","animate","source","dispatchEvent","_performance","performance","debug","measure","name","start","end","_performance3","customElements","get","define","CartDrawer","observedAttributes","onCartInitRequestedBound","onCartInitRequested","onCartUpdateErredBound","onCartUpdateErred","showCartAfterProgressBound","showCartAfterProgress","layout","renderEmbeddedLayout","some","startInitProgress","renderView","attributeChangedCallback","oldValue","newValue","modalState","render","onViewChanged","endInitProgress","scrollTo","cartInitializing","progressView","message","remaining","progressShownAt","Math","max","showCartDrawer","modal","inert","focusView","hideCartDrawer","viewSlotName","slots","slot","hidden","panel","removeAttribute","tabIndex","focusRequested","focus","preventScroll","safeDecode","decodeURIComponent","read","cookie","split","map","parts","_parts$slice","index","indexOf","encodedValue","slice","trim","undefined","filter","readAll","shift","CartGwp","tiers","updateTierStates","cart","maybeAutoAdd","collectTierData","lockedThresholds","t","total_price","thresholdCents","nextThreshold","min","tier","state","determineTierState","currentAmountCents","readGwpAutoAddCookie","rewardType","variants","v","available","selector","CSS","escape","promoCode","click","tierElements","tierEl","_tierEl$dataset$rewar","_tierEl$dataset$promo","_tierEl$dataset$thres","isShipping","push","parseInt","goalText","claimMessage","unlockedMessage","parseVariants","sort","a","b","renderTier","template","importNode","content","classList","add","isUnlocked","iconId","iconTemplate","appendChild","labelPriceEl","labelNameEl","currencyCode","currencySymbol","subunits","textContent","getTierLabel","amountsEl","currentSubunits","currentEl","thresholdEl","messagePrefixEl","messagePriceEl","messageTextEl","progressEl","percent","round","setAttribute","style","width","cardsEl","availableVariants","variant","renderVariantCard","card","link","img","imageSrc","src","alt","title","compareEl","compareAtPrice","priceEl","customAttribute","createElement","key","variantEl","id","variantId","quantity","addToCartButton","discountCode","_this$tiers","tiersContainer","productElements","productEl","_productEl$querySelec","_productEl$dataset$ti","_variantEl$dataset$ti","_variantEl$dataset$co","_ref","_variantEl$dataset$im","_variantEl$dataset$id","selectedId","selectedVariantId","productTitle","variantTitle","mainImage","code","toLowerCase","items","item","line_level_discount_allocations","alloc","discount_application","isGwpClaimedInCart","CartElement","onCloseClickedBound","onCloseClicked","onContinueShoppingButtonClickBound","onContinueShoppingButtonClick","onShowDiscountInputClickedBound","onShowDiscountInputClicked","cartViewedDispatched","_oldValue","focusCloseButton","cartMessages","clear","closeButton","showDiscountInput","bestSellersUrl","focusVisible","templateName","origin","onCartUpdatedInitial","role","promo","installmentsMessage","countryCode","isMBG","estimatedTotal","renderDeferredPaymentMessage","estTotal","hideBackdrop","renderLineItems","scenario","pulseLastAdded","variants_added","dispatchCartViewed","heading","item_count","join","cartContents","emptyCart","footerEl","lineItems","promoGoalEl","installmentPaymentMessageElement","_cart$attributes","attributes","_aux_opt_in_v2","promoGoalV2El","show","surface","deferredAmount","_item$selling_plan_al","_item$selling_plan_al2","selling_plan_allocation","selling_plan","recurring_deliveries","price","line_level_total_discount","dueTodayEl","parents","uniqueParentKeys","Set","_item$parent_relation","parent_relationship","parent_key","giftWithPurchaseProduct","find","properties","_gift_with_purchase","fragment","createDocumentFragment","_item$parent_relation2","element","createItemElement","has","isParent","appendChildItems","final_price","isGiftWithPurchase","giftCount","giftChangeable","regularPrice","_gift_with_purchase_price","linePrice","row","lineItem","variant_id","pulse","parentElement","parentKey","_item$parent_relation3","childElement","_item$parent_relation4","_item$parent_relation5","_item$parent_relation6","_item$parent_relation7","lineItemEl","image","imageAlt","isAvailable","lineItemId","productType","product_type","productUrl","original_line_price","linePriceAfterDiscount","final_line_price","parentId","_is_upsell","isUpsell","maxQuantity","handle","productHandle","product_id","productId","sellingPlanId","JSON","parse","upgradeButton","clickTrack","sellingPlanFrequency","replace","select","options","Array","isArray","option","optionEl","selected","messages","allocation","amount","toFixed","test","_contains_alcohol","_compatibility","_final_sale","_backorder_message","_is_tbyb","generateMessages","ul","cloneNode","itemEl","update","showBackdrop","backdrop","_item$properties","getRequestId","response","headers","isResponse","ok","status","text","json","decorateError","resolveUrl","path","protocol","host","async","cartChange","input","init","body","stringify","cache","method","fetch","Error","_error$response_body3","_error$response_body4","consumeError","response_body","description","cartFetch","responseBodyText","responseBodyObject","isCart","cartUpdate","_error$response_body5","_error$response_body6","isUnavailableError","isObject","startsWith","includes","digest","cart_input_type","_cart$cart_level_disc","_cart$items","token","attribute_keys","Object","keys","discount_codes","cart_level_discount_titles","cart_level_discount_applications","application","_item$line_level_disc","discounts","_allocation$discount_","digest_failed","hasSimilarItems","cart1","cart2","item1","found","item2","decorate_error_decorateError","assignmentError","reason","tag","prototype","call","report","dispatcher","target","Element","window","dispatchCartUpdateErred","ignoredCodes","_detail$transaction","_detail$transaction2","inputs","transaction","reportError","raise","escalateCartError","assert","expression","AssertionError","constructor","super","rnds8","Uint8Array","byteToHex","i","unsafeStringify","arr","offset","dist_v4","buf","crypto","randomUUID","_options$random","rnds","random","rng","getRandomValues","RangeError","_v4","copy","structuredClone","cloned","trustworthy","isWellFormed","upper","toUpperCase","containsJavascriptWord","getApplicableDiscountCodes","applicable","getDiscountCodes","state_write","initial","previous","state_read","oldVariants","groupByVariant","newVariants","prevCodes","currCodes","discount_codes_added","complement","discount_codes_removed","event_id","findVariantsAdded","variants_removed","findVariantsRemoved","state_cartFetch","limit","attempts","delays","lastError","delay","resolveAfterTimeout","cartFetchOnceWithTimeout","timeout","CartFetchTimeoutError","timeoutPromise","Promise","_resolve","reject","race","milliseconds","resolve","output","property","merged_properties","image_url","product_title","sku","variant_title","c0map","c1map","increments","decrements","getSharedState","cart_transaction","activeCount","count","timeline","shared","Number","isInteger","_len","arguments","args","_key","epoch","completed","startMillis","didTimeOut","complete","snapshot","reportConcurrency","append","duration","entry","splice","change","remoteCart","error_code","determineErrorCode","previous_cart","fetched_cart","remove_remove","lineKeys","errorCode","every","isString","size","lineIndicesToRemove","reverse","line","line_keys","update_determineErrorCode","CartLineItem","onSellingPlanChangedBound","onSellingPlanChanged","onUpgradeButtonClickBound","onUpgradeButtonClick","onRemoveLineItemClickBound","onRemoveLineItemClick","onChangeGiftClickBound","onChangeGiftClick","onQuantityChangedBound","onQuantityChanged","removeButton","changeGiftButton","sellingPlanSelect","_name","HTMLSelectElement","updates","catch","trusted","isTrusted","actionElements","actionElement","primaryImageElement","featuredImageAnchor","lineItemTitle","linePriceAfterDiscountEl","linePriceEl","ariaLabelPrefix","renderQuantity","quantityElement","optionsHtml","CartMessages","initErrorAlert","onClearRequested","alerts","alert","_event$detail$inputs","showInitErrorMessage","showErrorMessage","before_codes","_event$detail$scenari","errors","showItemsAddedMessage","itemsString","alertType","showDiscountCodeAppliedMessages","showDiscountRemovedMessages","showSuccessMessage","upsellItem","showUpsellItemMessage","backorderedItem","showBackorderItemMessage","addedLineItemsString","backorderMessage","backorderAlert","messageHtml","onReloadBagClick","_button$closest","requestEvent","CartTotals","discountRow","discountAmount","subtotalEl","subtotalAmount","discountTotalEl","discountPercent","subtotal","_value_set_compare_price","floor","computeSubtotal","applied","display","orderDiscountsValue","discount","total_allocated_amount","orderDiscountsEl","CartUpsellItem","buildTitle","renderTitle","renderImage","renderPrices","renderAddButton","renderReviews","anchor","resolvePrices","_this$dataset$product","upsellTagSearch","upsellTag","productTags","upsellPriceArray","upsellPriceString","upsellPrice","parseFloat","prices","compareAtPriceEl","hide","addButton","country","reviews","average","reviewsAverage","reviewsCount","CartUpsellItems","requestAnimationFrame","variantIds","elements","visibleCount","formatMoney","cents","format","placeholderRegex","formatString","formatWithDelimiters","number","precision","thousands","decimal","isNaN","match","FreeShippingBanner","subscriptionsInCart","span","progressContainer","progressBar","percentage","getMessageText","threshold","subscriptionText","difference","GiftQualifiedMessage","onClickBound","onClick","_this$shadowRoot$quer","applyDiscountsHelper","newCodes","overrides","postValidate","discount_code","validateCodesBeforeUpdate","oldUppers","newCodesCount","currentCodes","getCurrentCodes","newUppers","retainedOldUppers","override","old_code","new_code","updatedCart","updatedNormalCodes","newCode","upperNewCode","validateCodesAfterUpdate","discountCodes","enrichedItems","enrich","stepStart","_error$response_body","_error$response_body2","responseConsumeAndParseBodyError","cartAdd","beforeCart","afterCart","discount_overrides","applyDiscountsOnAdd","addItemsInternal","add_determineErrorCode","isBundleAddonMixError","isMaxQuantityError","isInvalidSellingPlanError","_item","_item2","_url_path","pathname","OfferView","onCartDrawerClosedBound","onCartDrawerClosed","onConfirmClickedBound","onConfirmClicked","onGiftClickedBound","onGiftClicked","onOfferViewRequestedBound","onOfferViewRequested","onSkipClickedBound","onSkipClicked","gift","_this$shadowRoot$quer2","_this$shadowRoot$quer3","claimedGift","_event$detail","_this$claimedGift$var","_this$claimedGift","saving","gifts","drawer","returnView","addGift","_this$claimedGift2","showView","saveGift","finally","rememberSkip","_gwp_skipped","_this$claimedGift3","added","_this$claimedGift4","claimedEvent","unlock","confirm","skip","GiftWithPurchaseProduct","imageEl","imageUrl","titleEl","subtitleEl","subtitle","selectedCheckEl","checked","GiftWithPurchaseProgress","unlockPending","skipClearPending","cartQuantity","quantityThresholdInitialized","shippingInitialized","thresholdAchieved","claimedGiftWithPurchase","updateAchievedState","fromThisTab","skipped","isShopperChange","achieved","thresholdType","thresholdQuantity","giftThreshold","total","claimed","renderQualifiedMessages","renderThresholdMessage","renderShippingMessage","renderShippingProgress","renderQuantityThresholdProgress","renderCentsThresholdProgress","hasGifts","container","stepSize","goals","progressAchieved","stepOffset","goal","left","quantityStep","step","progressWidth","giftGoal","showFreeShipping","shippingGoal","freeShippingThresholdCents","shippingThreshold","shippingPercentage","shipping","shippingMessage","shippingElement","giftMessage","PromoGoalProgressV2","ingestPromoGoals","promoGoals","offerText","goalNamesContainer","goalIndex","nameDiv","spanDiv","thresholdsContainer","thresholdElement","DiscountInput","activeTransactions","errorMessage","onCartTransactionEndedBound","onCartTransactionEnded","onDiscountFormSubmitBound","onDiscountFormSubmit","onDiscountInputBound","onDiscountInput","form","inputElement","codes","discountAmounts","Map","currentTotal","getDiscountAmounts","unmatched","from","appliedDiscountsEl","appliedDiscountTemplate","appliedDiscountFragment","createAppliedDiscount","discountInfo","amountEl","infoElement","cta","onRemoveDiscountClicked","preventDefault","normalNewCodes","before_cart","new_codes","apply","then","_existingCodes","existingCodes","before","previousCart","previousCode","discount_remove_remove","PromoGoal","thresholdAmount","computeQualifyingSubtotal","originalTotalPrice","original_total_price","totalDiscountPrice","calculateTotalDiscount","totalPromoGoalDiscountPrice","isPromoNamedDiscount","calculateTotalPromoGoalDiscount","totalPriceOfExcludedItems","gift_card","calculateTotalPriceOfPromoExcludedItems","PromoGoalProgress","qualifyingSubtotal","paragraph","collectPromoGoalsData","eligibleThreshold","eligiblePromoGoals","promoGoal","isLocked","amountToGoal","percentSpentToGoal","spentEl","goalEl","compareThresholds","getFeatureValue","featureKey","fallback","timeoutMs","resolvedFeatures","growthbook_runtime","resolved_features","effectiveTimeoutMs","default_timeout_ms","load_state","extraMillis","previewMode","preview_hold_ms","once","growthbook","client_loader","feature","featureValueMatches","expected","_event$detail$cart$at","_event$detail$cart$at2","_event$detail$cart$at3","promoProgress","_aux_opt_in_v3","group","cartTotal","_aux_opt_in","onGetFeatureValue","ProgressBarCustom","ProgressView","renderMessage","UpsellView","_this$cartVariantIds","cartVariantIds","upsellItems","emit","startedEvent","cancelable","drawer_control_open","viewEl","showEvent","onModalDismissRequested","openedEvent","viewedEvent","drawer_control_close","closeEvent","closedEvent","onCartOpenRequested","_event$detail$animate","_event$detail2","_event$detail3","_event$detail4","onCartCloseRequested","_event$detail$animate2","_event$detail5","_event$detail6","gift_with_purchase_experiment_onCartUpdated","_gwp_limited","gift_with_purchase_experiment_onGetFeatureValue","composedPath","cartUpdateErrorEvents","main"],"sourceRoot":""}