# Storefront API

This section documents the API available to theme developers and app developers to interact with Discount Ninja on a Shopify storefront.

## Discount Ninja's storefront components

Discount Ninja includes frontend components that interact with the storefront and allow the app to:

* Discover available promotions based on the user's context
* Calculate discounts for products based on applicable promotions
* Apply those discounts to the products displayed on the storefront on the product pages (PDP), collection pages (PLP) and the cart

The key storefront component is the Price Rule Engine, documented [here](/discount-ninja-developer-hub/storefront-api/promotion-engine).

## Extending and integrating

Check out our [JavaScript API](/discount-ninja-developer-hub/storefront-api/promotion-engine/javascript-api) if you want to extend the capabilities of Discount Ninja's Price Rule Engine or integrate it with your theme or your app.

The functions and events provided by the JavaScript API can be used to integrate with:

* custom drawer carts
* subscription apps
* loyalty apps
* ...

## Widgets

Additionally, the app includes components that help to increase Average Order Value and increase Conversion Rate by effectively displaying promotions using widgets.

Learn more about widgets [here](/discount-ninja-developer-hub/storefront-api/widgets).


# Promotion Engine

Information about the core JavaScript script used to load Discount Ninja promotions on a Shopify storefront and apply the promotions to the available products and the cart.

## **Headless**

{% hint style="danger" %}
Please note that Discount Ninja is not compatible with storefronts that use a headless implementation of Shopify as it relies on App embeds and App blocks.
{% endhint %}

## Guiding principles

### Performance

Performance is key in many areas and this is certainly true in eCommerce. Poor performance leads to frustration for visitors of your online store and results in loss of conversion potential.

What we do to ensure the best possible performance:

* **Vanilla JavaScript Implementation**\
  Our script and widgets are built entirely with Vanilla JavaScript, ensuring that they do not depend on external libraries or frameworks. This approach eliminates unnecessary bloat, reduces compatibility issues, and ensures seamless integration into any environment. By avoiding dependencies, we eliminate the risk of version conflicts and provide a lightweight, robust solution for developers.
* **Optimized for Minimal Size**\
  The script is carefully engineered to deliver the smallest possible payload. Through optimization techniques such as minification, and compression, we ensure that only the essential code is included. This minimizes download times, improves rendering performance, and reduces the overall bandwidth required, making the user experience faster and smoother, especially in resource-constrained environments.
* **Smart Caching Strategies**\
  To maximize efficiency, our engine leverages intelligent caching mechanisms. It stores frequently accessed data in the browser, significantly reducing the need for repeated network requests. Additionally, caching minimizes computation time by reusing pre-calculated data where possible, ensuring consistently fast performance even under high demand.
* **Continuous Performance Testing and Profiling**\
  We prioritize performance at every stage of development through rigorous testing and profiling. Using industry-standard tools and real-world scenarios, we regularly analyze the script’s behavior to identify potential bottlenecks and optimize for speed and efficiency. This iterative approach ensures that our solution consistently delivers top-tier performance across diverse environments.

### **Browser support**

* **Comprehensive Browser Compatibility:** The script is thoroughly tested across modern browsers, using progressive enhancements to ensure a consistent experience for all users.
* All features used by the app are supported by modern browsers, including Chrome, Firefox, and Safari.

### **Extensibility**

We include a rich client-side API that allows developers to interact with our script and extend the functionality of the promotion engine.


# Enable

## Open the theme editor <a href="#h_4f91f739ed" id="h_4f91f739ed"></a>

1. Open the *Sales channels* section in the Shopify admin, then find *Online Store* and click *Themes*:

   <figure><img src="/files/64snexyjgLow7GhyOlFk" alt=""><figcaption><p>Sales channels menu section</p></figcaption></figure>
2. Find the theme you want to customize and click the *Customize* button for that theme:

   <figure><img src="/files/ZWNjZh4TsiQvHkhYPUhA" alt=""><figcaption><p>Customize theme</p></figcaption></figure>
3. The theme editor opens.
4. In the sidebar on the left, click *App embeds*. \
   If you can't find Discount Ninja in the list, type Discount Ninja in the search bar (where it says "Search app embeds"').

## Enable the App embed <a href="#h_7b073dc2be" id="h_7b073dc2be"></a>

1. Switch the toggle on
2. Click the "Save" button

   [![](https://downloads.intercomcdn.com/i/o/836619213/33d486c5f04fbd348b511ab4/image.png)](https://downloads.intercomcdn.com/i/o/836619213/33d486c5f04fbd348b511ab4/image.png)


# JavaScript API

Documentation of the JavaScript API that can be used to extend Discount Ninja's Promotion Engine.

{% hint style="info" %}
The documentation below describes the API as of version 9.x of the script.&#x20;
{% endhint %}

## Documentation

* [Functions](/discount-ninja-developer-hub/storefront-api/promotion-engine/javascript-api/functions): documents all properties and functions available in the public API
* [Events](/discount-ninja-developer-hub/storefront-api/promotion-engine/javascript-api/events): documents the events published and subscribed to by the Price Rule Engine
* [Objects](/discount-ninja-developer-hub/storefront-api/promotion-engine/javascript-api/objects): documents the objects returned by the functions and properties


# Functions

Functions and properties available on the discountNinja.api namespace.

{% hint style="info" %}
The documentation below describes the API as of version 9.x of the script.&#x20;

Public properties or functions that are not in the `discountNinja.api` namespace should be considered obsolete and will be removed in the next major version release.

Using undocumented functions or objects will result in issues as custom code that relies on those functions or objects may break in the future when Discount Ninja's script is automatically updated.
{% endhint %}

## Cache

### Clear

<table data-header-hidden><thead><tr><th width="186"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>Yes</td></tr><tr><td><strong>Function</strong></td><td><code>await discountNinja.api.cache.clear()</code></td></tr><tr><td></td><td>Clears all session and local storage data used by the script. Also clears the content of the indexedDb used by the script.</td></tr><tr><td><strong>Type</strong></td><td>Public</td></tr><tr><td></td><td>This function is intended for use in troubleshooting sessions.</td></tr></tbody></table>

#### Syntax

```javascript
//Clear cache
if (discountNinja) await discountNinja.api.cache.clear();
```

## Cart

### Add

<table data-header-hidden><thead><tr><th width="186"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>Yes</td></tr><tr><td><strong>Function</strong></td><td><code>await discountNinja.api.cart.add(variant: number, quantity: number, properties?: Record&#x3C;string, string>, sellingPlanId?: number)</code></td></tr><tr><td></td><td>Adds a variant to the cart. Note: this is an asynchronous function; the result must be awaited.</td></tr><tr><td><strong>Type</strong></td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties.</td></tr><tr><td><strong>Parameters</strong></td><td><code>variant (number)</code>: the variant id</td></tr><tr><td></td><td><code>quantity (number)</code>: the number of items to add</td></tr><tr><td></td><td><code>properties? (Record&#x3C;string, string>)</code>: an optional object with key/value pairs of line item properties </td></tr><tr><td></td><td><code>sellingPlanId? (number)</code>: an optional selling plan (to allow for recurring purchases based on a subscription)</td></tr></tbody></table>

#### Syntax

```javascript
async function addVariantToCart() { 
    if (!discountNinja) return;
    
    const variant = 123456; //Ensure the variant id exists on the shop
    const quantity = 1;
    const properties = { "my_property": "my value", "my_property_2": "another value" };
    const sellingPlanId = 123456; //Ensure this is a valid selling plan for the variant
    
    //Add the variant
    await discountNinja.api.cart.add(variant, quantity);
    //Add the variant with a line item property
    await discountNinja.api.cart.add(variant, quantity, properties);
    //Add the variant with a selling plan
    await discountNinja.api.cart.add(variant, quantity, undefined, sellingPlanId);
}

//Run the above test
await addVariantToCart();
```

### Content

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>No</td></tr><tr><td><strong>Property</strong></td><td><code>discountNinja.api.cart.content</code></td></tr><tr><td></td><td>Can be used to get access to an object that represents the content of the cart, including any discounts applied by Shopify's backend.</td></tr><tr><td><strong>Type</strong></td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties.</td></tr><tr><td><strong>Return value</strong></td><td><code>object | null</code> Returns the <a href="https://shopify.dev/docs/api/liquid/objects/cart">Shopify cart object</a>. Returns null if no cart is available.</td></tr></tbody></table>

#### Syntax

```javascript
function logShopifyCart() {
    const cart = discountNinja discountNinja.api.cart.content ? null;
    const itemCount = cart ? cart.item_count : 0;
    console.log(`There are currently ${itemCount} items in the cart`, cart);
}

logShopifyCart()
```

### Prefill

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>Yes</td></tr><tr><td><strong>Property</strong></td><td><code>await discountNinja.api.cart.prefill(variants?: string[], redirectUrl?: string)</code></td></tr><tr><td></td><td>Can be used to prefill a cart with specific variants.<br>Typically used to create a link to a prefilled, discounted cart. <br>Cf. <a href="https://support.discountninja.io/en/articles/5194395-how-to-build-a-prefilled-discounted-cart">https://support.discountninja.io/en/articles/5194395-how-to-build-a-prefilled-discounted-cart</a></td></tr><tr><td><strong>Type</strong></td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties.</td></tr><tr><td>Parameters</td><td><code>variants?: string[]</code>: a comma-separated list of the variant id of the product variants you want to add to the cart; for each variant you'll need to specify the quantity. </td></tr><tr><td></td><td>This parameter is optional. If it is omitted, the app will look for a query parameter named <code>variants</code> instead. If neither is available the prefill method will not execute.</td></tr><tr><td></td><td><code>redirectUrl?: string</code>: define which page should be loaded once the cart is prefilled</td></tr><tr><td></td><td>This parameter is optional. If it is omitted, the app will look for a query paramer named <code>redirectUrl</code> instead. If neither is available the default value is "<code>/cart</code>".</td></tr></tbody></table>

#### Syntax: example 1

This script can be used on a cart template to automatically prefill the cart based on the query parameters as explained here: [ttps://support.discountninja.io/en/articles/5194395-how-to-build-a-prefilled-discounted-cart](https://support.discountninja.io/en/articles/5194395-how-to-build-a-prefilled-discounted-cart)

```javascript
<script type="text/javascript">
    document.addEventListener("la:dn:promotions:loaded", async function() { 
	if (discountNinja) await discountNinja.api.cart.prefill();
    });
</script>
```

#### Syntax: example 2

Alternatively, create a variants array manually and then&#x20;

```javascript
async function prefillCartWithSpecificVariants() {	
    // Replace 123456 and 2345678 with valid variant ids
    // Add a quantity of 1 of the variant with id 1234567x1
    // and a quantity of 2 of the variant with id 2345678x2
    // then, redirect to the home page
    const variants = [];
    variants.push('1234567x1');
    variants.push('2345678x2');
    if (discountNinja) await discountNinja.api.cart.prefill(variants, '/');
}

await prefillCartWithSpecificVariants();
```

## Checkout

### Is discounted

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>Yes</td></tr><tr><td><strong>Function</strong></td><td><code>await discountNinja.api.checkout.isDiscounted()</code></td></tr><tr><td></td><td>Can be used to check if any of the promotions apply to the cart and result in a product, order or shipping discount. </td></tr><tr><td>Type</td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties.</td></tr><tr><td><strong>Return value</strong></td><td><p><code>Promise&#x3C;boolean></code> Returns a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">promise</a> which resolves to a <code>boolean</code>. </p><p></p><p>The return value indicates if the app will instruct Shopify to apply discounts at checkout. Additionally, when this function returns <code>true</code>, the script will attempt to take over the checkout process when the cart form is submitted using a checkout button.</p></td></tr></tbody></table>

#### Syntax

```javascript
document.addEventListener("la:dn:cart:updated", async function(event) {
    const discountedCart = event.detail.data[0]; 
    const isDiscounted = await discountNinja.api.checkout.isDiscounted();
    if (isDiscounted) {                
        console.log('The cart is discounted', discountedCart);
        // Add logic when the cart is discounted by Discount Ninja offers
    }
    else {
        console.log('The cart is not discounted', discountedCart);
        // Add logic when the cart is not discounted by Discount Ninja offers
    }
});
```

### Add pipeline rule

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>No</td></tr><tr><td><strong>Function</strong></td><td><code>discountNinja.api.checkout.addPipelineRule()</code></td></tr><tr><td></td><td>Can be used to add custom business logic that is executed when the customer click the checkout button. </td></tr><tr><td></td><td>Discount Ninja needs to take over the checkout process when a user clicks the checkout button to ensure the checkout is correctly discounted. </td></tr><tr><td></td><td>As a result, the app overrides any logic you may have associated with clicking the checkout button. To execute this logic you can add it to the pipeline of rules that is executed by the app.</td></tr><tr><td></td><td>Each pipeline rule is a parameterless function that returns a boolean indicating if execution should continue (<code>true</code>) or not (<code>false</code>).</td></tr><tr><td>Type</td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties.</td></tr><tr><td><strong>Return value</strong></td><td><code>boolean</code> The return value indicates if the rule was added properly.</td></tr></tbody></table>

#### Syntax

```javascript
function checkoutPipelineRule1() {
    //Add your logic here
    //Return true to indicate that the customer can continue to the checkout
    //Return false to halt the pipeline
    console.log('Executing checkout pipeline rule 1');
    return true; 
}

function checkoutPipelineRule2() {
    //Add your logic here
    //Return true to indicate that the customer can continue to the checkout
    //Return false to halt the pipeline
    console.log('Executing checkout pipeline rule 2');
    return true; 
}

function registerCheckoutPipelineRules() {
    const successfullyAddedRule1 = 
        discountNinja.api.checkout.addPipelineRule(checkoutPipelineRule1);
    const successfullyAddedRule2 = 
        discountNinja.api.checkout.addPipelineRule(checkoutPipelineRule2);
    if (successfullyAddedRule1 && successfullyAddedRule2) {
        console.log('Pipeline rules successfully registered');
    }
    else {
        console.error('Pipeline rules could not be registered');
    }
}

//Check if the API is ready
if (typeof discountNinja === 'undefined' || 
    typeof discountNinja.api === 'undefined') {
    //The API is not ready, wait for it
    document.addEventListener("la:dn:api:ready", function(event) { 
        registerCheckoutPipelineRules();
    });
}
else {
    //The API was already available, no need to wait
    registerCheckoutPipelineRules();
}
```

### With

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>Yes</td></tr><tr><td><strong>Property</strong></td><td><code>await discountNinja.api.checkout.with(variants: string[], discountCodes?: string[])</code></td></tr><tr><td></td><td>Can be used to checkout using specific variants and one or more discount codes.</td></tr><tr><td><strong>Type</strong></td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties.</td></tr><tr><td>Parameters</td><td><code>variants?: string[]</code>: a comma-separated list of the variant id of the product variants you want to add to the cart; for each variant you'll need to specify the quantity. The variant and the quantity are separated by an x. E.g. 123456x2</td></tr><tr><td></td><td><code>discountCodes?: string[]</code>: a list of discount codes to apply to the cart</td></tr></tbody></table>

#### Example

This example shows you can use this api method to build a discounted checkout link, that can be used on any page that includes the Discount Ninja App embed.

```javascript
<a href="#" onclick='discountNinja.api.checkout.with(["19812741972097x2","19812742037633x3"],["ABCD"]); return false;'>
  Checkout with selected items
</a>
```

## Discount code

### Add

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>Yes</td></tr><tr><td><strong>Function</strong></td><td><code>await discountNinja.api.discountCode.add(discountCode: string)</code></td></tr><tr><td></td><td>Adds a discount code or promotion code to the Discount Ninja promotion code field programmatically.</td></tr><tr><td><strong>Type</strong></td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties.</td></tr><tr><td><strong>Parameters</strong></td><td><code>discountCode (string)</code>: the discount code to add</td></tr></tbody></table>

#### Syntax

```javascript
async function addDiscountCode(discountCode) {
    if (discountNinja) await discountNinja.api.discountCode.add(discountCode);
    console.log(`Added the promotion with promotion code '${discountCode}' to the cart`);
}

await addDiscountCode('WELCOME10');
```

### Remove

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>Yes</td></tr><tr><td><strong>Function</strong></td><td><code>await discountNinja.api.discountCode.remove(discountCode: string)</code></td></tr><tr><td></td><td>Removes a discount code or promotion code from the Discount Ninja promotion code field programmatically.</td></tr><tr><td><strong>Type</strong></td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties.</td></tr><tr><td><strong>Parameters</strong></td><td><code>discountCode (string)</code>: the discount code to remove</td></tr></tbody></table>

#### Syntax

```javascript
async function removeDiscountCode(discountCode ) {
    if (discountNinja) await discountNinja.api.discountCode.remove(discountCode);
    console.log(`Removed the promotion with promotion code '${discountCode}' from the cart`);
}

await removeDiscountCode('WELCOME10');
```

## Discounted cart

### Content

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Property</strong></td><td><code>discountNinja.api.discountedCart.content</code></td></tr><tr><td></td><td>Can be used to get access to an object that represents the content of the cart, including any discounts applied by Shopify's backend.</td></tr><tr><td>Type</td><td>Internal</td></tr><tr><td></td><td>This is an internal function or property.<br>Use it only for debugging purposes.<br>Do <em>not</em> rely on this function or property in your integration code.</td></tr><tr><td><strong>Return value</strong></td><td><code>object | null</code> Returns an object representing the current cart, with the applicable Discount Ninja offers applied. Note that amounts returned are in cents, not in dollars.</td></tr></tbody></table>

#### Syntax

```javascript
function logDiscountedCart() {
    const discountedCart = discountNinja ? discountNinja.api.discountedCart.content : null;
    console.log(`The discounted cart`, discountedCart);
}

logDiscountedCart()
```

## Entitlements

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Property</strong></td><td><code>discountNinja.api.entitlements</code></td></tr><tr><td></td><td>Lists the entitlements that apply based on the available offers. The array contains one item for each available offer. It details the prerequisites that were found in the cart, the discount amount, the target products and the entitled line items that were found in the cart.</td></tr><tr><td>Type</td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties..</td></tr><tr><td><strong>Return value</strong></td><td><code>object[] | null</code> Returns an array of <a href="/pages/jSUo98LqzKbt8ynNaxPN#entitlement-info">Entitement info</a> objects.</td></tr></tbody></table>

#### Syntax

```javascript
function logEntitlements() {
    if (discountNinja) console.log(`Entitlements`, discountNinja.api.entitlements);
}

logEntitlements()
```

## Events

### Subscribe

{% hint style="warning" %}
This function is not used to subscribe to events. Instead it is simply a helper function that prints sample code to the log console to explain how to subscribe to an event.
{% endhint %}

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>No</td></tr><tr><td><strong>Function</strong></td><td><code>discountNinja.api.events.subscribe(eventName: string)</code></td></tr><tr><td></td><td>Provides instructions on how to subscribe to an event with a given name. <strong>Note</strong>: this function does not, itself, subscribe to the event.</td></tr><tr><td></td><td>See the list of available <a href="#events">events</a>.</td></tr><tr><td><strong>Type</strong></td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties.</td></tr></tbody></table>

#### Syntax

<pre class="language-javascript"><code class="lang-javascript">function printInstructionsToSubscribeToEvent(event) {
    if (discountNinja) discountNinja.api.events.subscribe(event);
}

printInstructionsToSubscribeToEvent('cart:updated')
// Prints the following message on the console:
<strong>// To subscribe to this event use the following code: 
</strong><strong>// document.addEventListener("la:dn:cart:updated", function(event) { const eventData = event.detail.data[0]; console.log('Event la:dn:cart:updated was published.', eventData); });
</strong></code></pre>

### Publish

<table data-header-hidden><thead><tr><th width="186"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>No</td></tr><tr><td><strong>Function</strong></td><td><code>discountNinja.api.events.publish(eventName: string)</code></td></tr><tr><td></td><td>Publishes an event to the PriceRuleEngine. </td></tr><tr><td></td><td>See the list of available <a href="#events">events</a>.</td></tr><tr><td><strong>Type</strong></td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties.</td></tr></tbody></table>

#### Syntax

```javascript
function test() {
    if (discountNinja) discountNinja.api.events.publish('cart:updated');
}
```

## Help

<table data-header-hidden><thead><tr><th width="186"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>No</td></tr><tr><td><strong>Function</strong></td><td><code>discountNinja.api.help()</code></td></tr><tr><td></td><td>Prints a link to this page on the console.</td></tr><tr><td><strong>Type</strong></td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties.</td></tr></tbody></table>

#### Syntax

```javascript
discountNinja.api.help()
```

## Line item

### Get updated key

<table data-header-hidden><thead><tr><th width="186"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>No</td></tr><tr><td><strong>Function</strong></td><td><code>discountNinja.api.lineItem.getUpdatedKey(key: string)</code></td></tr><tr><td></td><td>Returns the updated key associated with a cart line item. A key can be updated by Shopify's back-end when properties are added, a selling plan changes or a discount is added. Cf. <a href="https://support.discountninja.io/en/articles/9109023-fix-network-requests-to-cart-js">https://support.discountninja.io/en/articles/9109023-fix-network-requests-to-cart-js</a></td></tr><tr><td><strong>Type</strong></td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties.</td></tr><tr><td><strong>Parameters</strong></td><td><code>key (string)</code>: the original key of the cart line item</td></tr><tr><td><strong>Return value</strong></td><td><code>string</code> The updated key.</td></tr></tbody></table>

#### Syntax

```javascript
function test() {
    for (let i = 0; i < cart.items.length; i++) {
        const item = cart.items[i];
        let key = item.key;
        if (discountNinja) {
            key = discountNinja.api.lineItem.getUpdatedKey(key);
        }
        
        // ... use the updated key, for example, to send requests to cart.js
    }
});
```

## Offers

### All&#x20;

<table data-header-hidden><thead><tr><th width="186"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>No</td></tr><tr><td><strong>Function</strong></td><td><code>discountNinja.api.offers.all</code></td></tr><tr><td></td><td>Lists all the active offers loaded by the app. </td></tr><tr><td><strong>Type</strong></td><td>Internal</td></tr><tr><td></td><td>This function returns an internal object.<br>Do <em>not</em> rely on the properties of this object in your integration code as they may be removed or renamed in future versions.</td></tr><tr><td><strong>Return value</strong></td><td><code>object[]</code> An array of <code>offer</code> objects, representing the offers that are applicable in the current context.</td></tr></tbody></table>

#### Syntax

```javascript
document.addEventListener("la:dn:promotions:loaded", function(event) { 
    const offers = discountNinja ? discountNinja.api.offers.all : null;
    console.log('Offers loaded', offers);
});
```

### Get

{% hint style="info" %}
This function is used to look up an offer that is available in the browser's cache. To load a promotion that is not available to the browser, use the Promotion [Trigger](#trigger) function or the Discount Code [Add](#add-1) function instead.
{% endhint %}

<table data-header-hidden><thead><tr><th width="186"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>No</td></tr><tr><td><strong>Function</strong></td><td><code>discountNinja.api.offers.get(token: string)</code></td></tr><tr><td></td><td>Finds a specific offer in the list of all the active offers loaded by the app. </td></tr><tr><td><strong>Type</strong></td><td>Internal</td></tr><tr><td></td><td>This function returns an internal object.<br>Do <em>not</em> rely on the properties of this object in your integration code as they may be removed or renamed in future versions.</td></tr><tr><td><strong>Parameters</strong></td><td><code>token (string)</code>: the token of the offer to find</td></tr><tr><td><strong>Return value</strong></td><td><code>object | null</code> An<code>offer</code> object if the offer is found, otherwise <code>null</code>.</td></tr></tbody></table>

#### Syntax

```javascript
document.addEventListener("la:dn:promotions:loaded", function(event) { 
    const offerToken = 'ABCD';
    const bfcmOffer =  discountNinja ? discountNinja.api.offers.get(offerToken) : null;
    if (bfcmOffer === null) {
        console.log(`Offer with token ${offerToken} not found`);
    }
    else {
        console.log(`Offer with token ${offerToken} found`, bfcmOffer);
    }
});
```

## Promotions

### All&#x20;

<table data-header-hidden><thead><tr><th width="186"></th><th></th></tr></thead><tbody><tr><td><strong>Function</strong></td><td><code>discountNinja.api.promotions.all</code></td></tr><tr><td></td><td>Lists all the active promotions loaded by the app. </td></tr><tr><td><strong>Type</strong></td><td>Internal</td></tr><tr><td></td><td>This function returns an internal object.<br>Do <em>not</em> rely on the properties of this object in your integration code as they may be removed or renamed in future versions.</td></tr><tr><td><strong>Return value</strong></td><td><code>object[]</code> An array of <code>promotion</code> objects, representing the promotions that are applicable in the current context. The promotion object provides information about the trigger used and the list of offers associated with the promotion.</td></tr></tbody></table>

#### Syntax

```javascript
document.addEventListener("la:dn:promotions:loaded", function(event) { 
    const promotions = discountNinja ? discountNinja.api.promotions.all : null;
    console.log('Offers loaded', promotions);
});
```

### Get

{% hint style="info" %}
This function is used to look up a promotion that is available in the browser's cache. To load a promotion that is not available to the browser, use the Promotion [Trigger](#trigger) function or the Discount Code [Add](#add-1) function instead.
{% endhint %}

<table data-header-hidden><thead><tr><th width="186"></th><th></th></tr></thead><tbody><tr><td><strong>Function</strong></td><td><code>discountNinja.api.promotions.get(token: string)</code></td></tr><tr><td></td><td>Finds a specific promotion in the list of all the active promotions loaded by the app. </td></tr><tr><td><strong>Type</strong></td><td>Internal</td></tr><tr><td></td><td>This function returns an internal object.<br>Do <em>not</em> rely on the properties of this object in your integration code as they may be removed or renamed in future versions.</td></tr><tr><td><strong>Parameters</strong></td><td><code>token (string)</code>: the token of the promotion to find</td></tr><tr><td><strong>Return value</strong></td><td><code>object | null</code> A <code>promotion</code> object if the promotion is found, otherwise <code>null</code>.</td></tr></tbody></table>

#### Syntax

```javascript
document.addEventListener("la:dn:promotions:loaded", function(event) { 
    const promotionToken = 'ABCD';
    const bfcmPromotion = discountNinja ? discountNinja.api.promotions.get(promotionToken) : null;
    if (bfcmPromotion === null) {
        console.log(`Promotion with token ${promotionToken} not found`);
    }
    else {
        console.log(`Promotion with token ${promotionToken} found`, bfcmPromotion);
    }
});
```

### Trigger

<table data-header-hidden><thead><tr><th width="186"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>No</td></tr><tr><td><strong>Function</strong></td><td><code>discountNinja.api.promotions.trigger(tokens: string)</code></td></tr><tr><td></td><td>Triggers a private promotion programmatically.</td></tr><tr><td></td><td>Use this as a programmatic alternative to using a discount link. To trigger a promotion based on a promotion code, use the Discount Code <a href="#add-1">Add</a> function instead.</td></tr><tr><td><strong>Type</strong></td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties.</td></tr><tr><td><strong>Parameters</strong></td><td><code>tokens (string | string[])</code>: the token(s) of the promotion(s) to trigger</td></tr></tbody></table>

#### Syntax

```javascript
function triggerCustomPromotion() {
    const bcfmPromotionToken = 'ABCD';
    if (discountNinja) discountNinja.api.promotions.trigger(bcfmPromotionToken);
}
```

```html
<a href='javascript:triggerCustomPromotion()'>Unlock the BFCM promo!</a>
<button onclick='triggerCustomPromotion()'>Unlock the Cyber Monday offer!</button>
```

## Strikethrough Pricing

### Apply

<table data-header-hidden><thead><tr><th width="186"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>Yes</td></tr><tr><td><strong>Function</strong></td><td><code>await discountNinja.api.strikethroughPricing.apply()</code></td></tr><tr><td></td><td>Triggers a refresh of all strikethrough pricing programmatically. This is typically not required since the app automatically detects situations where the strikethrough pricing must be updated.</td></tr><tr><td></td><td>For an event based approach, use <a href="/pages/Ov5bugDWYPqgvS6Rcy1p#drawer-cart-opened">this event</a> instead.</td></tr><tr><td><strong>Type</strong></td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties.</td></tr></tbody></table>

#### Syntax

```javascript
async function myPriceUpdateLogic() {
    //Trigger Discount Ninja's strikethrough pricing rendering
    if (discountNinja) await discountNinja.api.strikethroughPricing.apply();
};
```

### Before render

<table data-header-hidden><thead><tr><th width="186"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>No</td></tr><tr><td><strong>Function</strong></td><td><code>discountNinja.api.strikethroughPricing.setBeforeRender(async (input: Object) => string)</code></td></tr><tr><td></td><td>Discount Ninja ships with its own internal rendering function wired up as the “before render” callback. This function allows the user to override this callback with a custom callback.</td></tr><tr><td><strong>Parameters</strong></td><td><p><code>async (input: Object) => string)</code> A function that receives an input object and returns a string.<br></p><p><code>priceData: Object</code> An input object that can be used to decide if the rendered text should be updated.<br></p><p><code>string</code> The HTML that should be rendered. To render the HTML proposed by the internal rendering function, simply return input.defaultHtml</p><p></p><p>This is the shape of the <code>input</code>object:</p><pre class="language-typescript"><code class="lang-typescript">{
    offerToken: string;
    defaultHtml: string; // This is the default HTML, computed by the script
<strong>    productInfo: {
</strong>        productHandle: string;
        variantId: number;
        sellingPlanId: number | null;
        available: boolean;
        price: {
          original: number;
          compareAt: number;
          discounted: number;
        };
    };
<strong>};
</strong></code></pre></td></tr><tr><td><strong>Return value</strong></td><td></td></tr><tr><td><strong>Type</strong></td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties.</td></tr></tbody></table>

#### Syntax

```javascript
function registerBeforeRenderLogic() {
    async function beforeRenderLogic(input) {          
      ... custom logic ...
      return input.defaultHtml;        
    }
    discountNinja.api.strikethroughPricing.setBeforeRender(beforeRenderLogic); 
}

//Check if the API is ready
if (typeof discountNinja === 'undefined' || 
    typeof discountNinja.api === 'undefined') {
    //The API is not ready, wait for it
    document.addEventListener("la:dn:api:ready", function(event) { 
        registerBeforeRenderLogic();
    });
}
else {
    //The API was already available, no need to wait
    registerBeforeRenderLogic();
}
```

## Widgets

### Render

<table data-header-hidden><thead><tr><th width="186"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>Yes</td></tr><tr><td><strong>Function</strong></td><td><code>await discountNinja.api.widgets.render()</code></td></tr><tr><td></td><td>Triggers a refresh of all widgets (including strikethrough pricing) programmatically. This is typically not required since the app automatically detects situations where the widgets must be updated.</td></tr><tr><td><strong>Type</strong></td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties.</td></tr></tbody></table>

#### Syntax

```javascript
async function myWidgetUpdateLogic() {
    //Trigger Discount Ninja's widget rendering update
    if (discountNinja) await discountNinja.api.widgets.render();
};
```

## Widgets > Promotion Code Field

### Toggle

<table data-header-hidden><thead><tr><th width="186"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>Yes</td></tr><tr><td><strong>Function</strong></td><td><code>await discountNinja.api.widgets.promotionCodeField.toggle()</code></td></tr><tr><td></td><td>Expands or collapses the promotion code field widget.</td></tr><tr><td><strong>Type</strong></td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties.</td></tr></tbody></table>

#### Syntax

```javascript
async function myFunction() {
    if (discountNinja) discountNinja.api.widgets.promotionCodeField.toggle();
};
```

### Footer Instructions

<table data-header-hidden><thead><tr><th width="186"></th><th></th></tr></thead><tbody><tr><td><strong>Async</strong></td><td>No</td></tr><tr><td><strong>Function</strong></td><td><p><code>discountNinja.api.widgets.promotionCodeField.showFooterInstructions()</code></p><p><code>discountNinja.api.widgets.promotionCodeField.hideFooterInstructions()</code></p><p><code>discountNinja.api.widgets.promotionCodeField.toggleFooterInstructions()</code></p></td></tr><tr><td></td><td>Expands or collapses the instructions section in the footer of the promotion code field.</td></tr><tr><td><strong>Type</strong></td><td>Public</td></tr><tr><td></td><td>This function is intended for use in integration code by third parties.</td></tr></tbody></table>

#### Syntax

```javascript
async function myFunction() {
    if (discountNinja) discountNinja.api.widgets.promotionCodeField.toggleFooterInstructions();
};
```


# Events

Events are the primary way to interact with the Promotion Engine. The app publishes events when data becomes available and subscribes to a number of events that can be published by third parties.

{% hint style="info" %}
The documentation below describes the API as of version 9.x of the script.&#x20;

Public properties or functions that are not in the `discountNinja.api` namespace should be considered obsolete and will be removed in the next major version release.

Using undocumented functions or objects will result in issues as custom code that relies on those functions or objects may break in the future when Discount Ninja's script is automatically updated.
{% endhint %}

## API ready

#### Details

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Event</strong></td><td><code>la:dn:api:ready</code></td></tr><tr><td><strong>Direction</strong></td><td>This event is published by the app. You can optionally subscribe to it.</td></tr><tr><td><strong>Behavior</strong></td><td>Signals that the script has loaded and interaction with the API is possible. </td></tr></tbody></table>

#### Syntax

```javascript
function performActionWhenApiIsReady() {
    //Add your logic here
    console.log('API ready');
}

//Check if the API is ready
if (typeof discountNinja === 'undefined' || 
    typeof discountNinja.api === 'undefined') {
    //The API is not ready, wait for it
    document.addEventListener("la:dn:api:ready", function(event) { 
        performActionWhenApiIsReady();
    });
}
else {
    //The API was already available, no need to wait
    performActionWhenApiIsReady();
}
```

## Promotions loaded

#### Details

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Event</strong></td><td><code>la:dn:promotions:loaded</code></td></tr><tr><td><strong>Direction</strong></td><td>This event is published by the app. You can optionally subscribe to it.</td></tr><tr><td><strong>Behavior</strong></td><td>Published after promotions have been loaded, processed and filtered by the app. It allows the consumer to inspect which promotions are available for the user during the browsing session. </td></tr><tr><td><strong>Event properties</strong></td><td>The event data contains a promotions object.</td></tr></tbody></table>

#### Syntax

```javascript
document.addEventListener("la:dn:promotions:loaded", function(event) { 
    const promotions = event.detail.data[0]; 
    console.log('Promotions loaded', promotions);
});
```

## Product discount calculated

#### Details

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Event</strong></td><td><code>la:dn:product:discount:calculated</code></td></tr><tr><td><strong>Direction</strong></td><td>This event is published by the app. You can optionally subscribe to it.</td></tr><tr><td><strong>Behavior</strong></td><td>This event is published when the (discounted) price of any marked product on the page has been calculated.</td></tr><tr><td></td><td>The app will attempt to find all products on a page based on markers in the HTML. It then applies the available promotions to those products and publishes one event per product variant with the results.</td></tr><tr><td></td><td>For the current product, this event is published when the page is loaded and is published again when the selected variant or selected selling plan changes.<br>For other products, this event is published only once per product and per page load. </td></tr><tr><td><strong>Event properties</strong></td><td>The event data contains a <a href="/pages/jSUo98LqzKbt8ynNaxPN#productvariantpriceinfo">product variant price info</a> object.</td></tr></tbody></table>

#### Syntax

<pre class="language-javascript"><code class="lang-javascript"><strong>document.addEventListener("la:dn:product:discount:calculated", function(event) {
</strong>    const productVariantPriceInfo = event.detail.data[0]; 
    console.log('Product variant price calculated', productVariantPriceInfo);
});
</code></pre>

## Cart updated

#### Details

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Event</strong></td><td><code>la:dn:cart:updated</code></td></tr><tr><td><strong>Direction</strong></td><td>This event is published by the app. You can optionally subscribe to it.</td></tr><tr><td><strong>Behavior</strong></td><td>Published when a change to the cart has been detected and promotions have been evaluated. </td></tr><tr><td></td><td>This event is published when the page is loaded and is published again when the content of the cart changes or the set of applied promotions changes.</td></tr><tr><td><strong>Event properties</strong></td><td>The event data contains a <a href="/pages/jSUo98LqzKbt8ynNaxPN#discounted-cart">discounted cart</a> object.</td></tr></tbody></table>

#### Syntax

```javascript
document.addEventListener("la:dn:cart:updated", function(event) {
    const discountedCart = event.detail.data[0]; 
    console.log('Discounted cart available', discountedCart); 
});
```

## Cart discounted

#### Details

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Event</strong></td><td><code>la:dn:cart:discounted</code></td></tr><tr><td><strong>Direction</strong></td><td>This event is published by the app. You can optionally subscribe to it.</td></tr><tr><td><strong>Behavior</strong></td><td>Published when a discount has been applied to the cart. </td></tr><tr><td></td><td>This event is published when the page is loaded and is published again when the content of the cart changes or the set of applied promotions changes.</td></tr><tr><td><strong>Event properties</strong></td><td>The event data contains a <a href="/pages/jSUo98LqzKbt8ynNaxPN#discounted-cart">discounted cart</a> object.</td></tr></tbody></table>

#### Syntax

```javascript
document.addEventListener("la:dn:cart:discounted", function(event) {
    const discountedCart = event.detail.data[0]; 
    console.log('Discount applied to the cart', discountedCart); 
});
```

## Cart mutations processing

#### Details

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Event</strong></td><td><code>la:dn:cart-mutations:processing</code></td></tr><tr><td><strong>Direction</strong></td><td>This event is published by the app. You can optionally subscribe to it.</td></tr><tr><td><strong>Behavior</strong></td><td>Published when the store is making updates to the cart.</td></tr><tr><td></td><td>This event is published when Discount Ninja detects that network requests have been made to clear, update, change or add to the cart. Specifically, the app detects fetch requests to the /cart/add.js, /cart/update.js, /cart/change.js and /cart/clear.js (see <a href="https://shopify.dev/docs/api/ajax/reference/cart">https://shopify.dev/docs/api/ajax/reference/cart</a>).</td></tr></tbody></table>

#### Syntax

```javascript
// Add this HTML element to your (drawer) cart Liquid template
// It can be placed, for example, near the checkout button
<div id="cart-mutations-processing-label" style="display:none">
    Processing cart changes...
</div>

// Add this code to your theme.liquid in the body 
<script>
    document.addEventListener("la:dn:cart-mutations:processing", function() {
        console.log('Cart mutations are processing...'); 
        
        // Optionally, show an HTML element 
        // The HTML element can be used to communicate this process to the visitor
        const cartMutationsProcessingLabel = document.getElementById("cart-mutations-processing-label");
        if (cartMutationsProcessingLabel) cartMutationsProcessingLabel.style.display = 'block'
    });
</script>
```

## Cart mutations processed

#### Details

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Event</strong></td><td><code>la:dn:cart-mutations:processed</code></td></tr><tr><td><strong>Direction</strong></td><td>This event is published by the app. You can optionally subscribe to it.</td></tr><tr><td><strong>Behavior</strong></td><td>Published when the store has finished making updates to the cart.</td></tr><tr><td></td><td>This event is published when Discount Ninja detects that all network requests to the /cart/add.js, /cart/update.js, /cart/change.js and /cart/clear.js (see <a href="https://shopify.dev/docs/api/ajax/reference/cart">https://shopify.dev/docs/api/ajax/reference/cart</a>) have been completed.</td></tr></tbody></table>

#### Syntax

```javascript
// Add this HTML element to your (drawer) cart Liquid template
// It can be placed, for example, near the checkout button
<div id="cart-mutations-processing-label" style="display:none">
    Processing cart changes...
</div>

// Add this code to your theme.liquid in the body 
<script>
    document.addEventListener("la:dn:cart-mutations:processed", function() {
        console.log('Cart mutations are processed...'); 
        
        // Optionally, hide an HTML element 
        // The HTML element can be used to communicate this process to the visitor
        const cartMutationsProcessingLabel = document.getElementById("cart-mutations-processing-label");
        if (cartMutationsProcessingLabel) cartMutationsProcessingLabel.style.display = 'none';
    });
</script>
```

## Trigger checkout

#### Details

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Event</strong></td><td><code>la:dn:checkout:initiate</code></td></tr><tr><td><strong>Direction</strong></td><td>This event is subscribed to by the app.<br>To publish it, use the following syntax: </td></tr><tr><td></td><td><pre class="language-javascript"><code class="lang-javascript">document.dispatchEvent(new CustomEvent('la:dn:checkout:initiate'));
</code></pre></td></tr><tr><td><strong>Behavior</strong></td><td><p>This approach is useful if you need to trigger checkout programmatically without bypassing Discount Ninja.</p><p></p><p>Note: if you need a custom checkout pipeline (for example, you may want to execute custom validation logic before allowing the checkout to proceed) when clicking the checkout button, use <a href="/pages/ZytyXSg4PqJz4FbiWPzS#add-pipeline-rule">this API function</a> instead. </p></td></tr></tbody></table>

#### Syntax

```html
/// The markup below shows a custom button for illustration purposes
/// (i.e. not a button of type="submit" with name="checkout")
/// Note: we advise against using a custom checkout button
<button name="customcheckout" onclick="handleCustomCheckout()">
  Check out
</button>
```

```javascript
/// This function should be called by a custom checkout button 
/// such as the one described above
function handleCustomCheckout() {
  if (canAdvanceToCheckout() === true) {
     // Checks if Discount Ninja is available 
     if (discountNinja) {
        // Check if a Discount Ninja offer is applied to the cart        
        discountNinja.api.checkout.isDiscounted().then(function(requiresAdvancedCheckout) {
          if (requiresAdvancedCheckout === true) {
            // DN needs to handle the checkout
            advanceToCheckoutUsingDiscountNinja():
          }
          else {
            handleNonDiscountNinjaCheckout();
          }
        }); 
     }
     else {
        handleNonDiscountNinjaCheckout();
     }
  }
  else {
     //Custom logic to explain why the user cannot advance
  }
}

/// This optional function returns true if the user 
/// can advance to the checkout
function canAdvanceToCheckout() {
  return true;
}

/// This function instructs Discount Ninja
/// to advance to the checkout and apply offers
function advanceToCheckoutUsingDiscountNinja() {
  document.dispatchEvent(new CustomEvent('la:dn:checkout:initiate'));
}

/// This function should redirect the user to the checkout
/// when a checkout with Discount Ninja is not required
function handleNonDiscountNinjaCheckout() {
  //Custom logic to handle checkout
}
```

## Redirect to checkout

Discount Ninja expects users to transfer from the cart to the checkout using a checkout button or link. See this article for more information: <https://support.discountninja.io/en/articles/3505661-discount-ninja-checkout-button>

As a result, any code that redirects the user to the checkout using `window.location` is not supported. The following example shows what such an implementation would look like:

```html
/// The markup below shows a custom button for illustration purposes
/// (i.e. not a button of type="submit" with name="checkout")
/// Note: we advise against using a custom checkout button
<button name="customcheckout" onclick="handleCustomCheckout()">
  Check out
</button>
```

```javascript
/// This function is called by a custom checkout button 
function handleCustomCheckout() {
  window.location = "/checkout";
}
```

Redirecting a user to the checkout by setting the `window.location` causes problems with Discount Ninja. Additionally, it causes other integration issues for the merchant since redirecting without submitting the form has numerous flaws:

* The order note field is not submitted if it was changed during this session (and not updated using cart.js)
* The locale may not be passed
* The discount code present in the cart form is not passed
* ...

To resolve this, either use a standard checkout button (a button of type="submit" with name="checkout") or change your checkout logic as follows:

```javascript
/// This function is called by a custom checkout button 
function handleCustomCheckout() {
  if (discountNinja) {
    document.dispatchEvent(new CustomEvent('la:dn:checkout:initiate'));
  }
  else {
    // Your fallback logic
    // Note: we do not recommend redirecting to the checkout this way
    //       (see documentation), preferably resubmit the cart form
    window.location = window.Shopify.routes.root + "checkout";
  }
}
```

## Convert money fields

#### Details

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Event</strong></td><td><code>la:dn:money:convert</code></td></tr><tr><td><strong>Direction</strong></td><td>This event is subscribed to by the app.<br>To publish it, use the syntax explained in the example below. </td></tr><tr><td><strong>Behavior</strong></td><td>Most currency conversion mechanisms are supported out-of-the-box by Discount Ninja. This means there is no need to implement custom logic in most cases.</td></tr><tr><td></td><td>In case the currency conversion mechanism of your theme (or supporting app) is not automatically triggered, you can publish this event after the theme has updated prices to let Discount Ninja know that it should re-render prices.</td></tr></tbody></table>

#### Syntax

```javascript
function test() {
    // Custom logic that overrides prices
    document.dispatchEvent(new CustomEvent('la:dn:money:convert'));
}
```

## Collection products updated

#### Details

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Event</strong></td><td><code>la:dn:collection:updated</code></td></tr><tr><td><strong>Direction</strong></td><td>This event is subscribed to by the app.<br>To publish it, use the syntax explained in the example below. </td></tr><tr><td><strong>Behavior</strong></td><td>When collection products are added, removed or updated the app must recalculate the discounted prices and render the price.</td></tr><tr><td></td><td>Most mechanisms used to update collections are supported out-of-the-box by Discount Ninja. This means there is no need to implement custom logic in most cases.</td></tr><tr><td></td><td>In case the app is not automatically detecting a situation where collection products are updated, you can publish this event after the theme has updated the products to let Discount Ninja know that it should re-render prices.</td></tr></tbody></table>

#### Syntax

```javascript
function test() {
    // Custom logic that updates collection products
    document.dispatchEvent(new CustomEvent('la:dn:collection:updated'));
}
```

## Product changed

#### Details

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Event</strong></td><td><code>la:dn:product:changed</code></td></tr><tr><td><strong>Direction</strong></td><td>This event is subscribed to by the app.<br>To publish it, use the syntax explained in the example below. </td></tr><tr><td><strong>Behavior</strong></td><td>When a product page is rendered, the app calculates the discounted price for the selected variant of that product. If the same product page is re-used to render a different product (without navigating or reloading the page), this event can be used to inform Discount Ninja of that.</td></tr><tr><td></td><td>This event is only required in unusual cases where the same product page is used to render the product details of different products (usually variations of the same product that are not modeled as product variants).</td></tr><tr><td><strong>Parameters</strong></td><td><code>detail.data.product.id (number)</code>: the id of the product<br><code>detail.data.product.handle (string)</code>: the handle of the product</td></tr></tbody></table>

#### Syntax

```javascript
function test() {
    // Custom logic that informs Discount Ninja that a different
    // product is rendered on the product page
    const productId = 123456;
    const productHandle = "my-product";
    document.dispatchEvent(new CustomEvent("la:dn:product:changed", { 
        detail: { 
            data: { 
                product: { 
                    id: productId,
                    handle: productHandle
                } 
            } 
        } 
    }));
}
```

## Variant changed

#### Details

<table data-header-hidden><thead><tr><th width="179"></th><th></th></tr></thead><tbody><tr><td><strong>Event</strong></td><td><code>la:dn:variant:changed</code></td></tr><tr><td><strong>Direction</strong></td><td>This event is subscribed to by the app.<br>To publish it, use the syntax explained in the example below. </td></tr><tr><td><strong>Behavior</strong></td><td>When a different variant is selected, the app must recalculate the discounted price and render the price.</td></tr><tr><td></td><td>Most mechanisms used to change variants are supported out-of-the-box by Discount Ninja. This means there is no need to implement custom logic in most cases.</td></tr><tr><td></td><td>In case the app is not automatically detecting a situation where the product variant is changed, you can publish this event to let Discount Ninja know that it should re-render prices.</td></tr><tr><td><strong>Parameters</strong></td><td><code>detail.data.variant.id (number)</code>: the id of the selected variant</td></tr></tbody></table>

#### Syntax

```javascript
function test() {
    // Custom logic that changes the selected variant
    const variantId = 123456;
    document.dispatchEvent(new CustomEvent("la:dn:variant:changed", { 
        detail: { 
            data: { 
                variant: { 
                    id: variantId 
                } 
            } 
        } 
    }));
}
```

## Drawer cart opened

#### Details

<table data-header-hidden><thead><tr><th width="157"></th><th></th></tr></thead><tbody><tr><td><strong>Event</strong></td><td><code>la:dn:drawercart:opened</code></td></tr><tr><td><strong>Direction</strong></td><td>This event is subscribed to by the app.<br>To publish it, use the following syntax: </td></tr><tr><td></td><td><pre class="language-javascript"><code class="lang-javascript">document.dispatchEvent(new CustomEvent('la:dn:drawercart:opened'));
</code></pre></td></tr><tr><td><strong>Behavior</strong></td><td>When a drawer cart is opened the app may need to render discounted prices.</td></tr><tr><td></td><td>In case the app is not automatically detecting a situation where the drawer cart is opened, you can publish this event to let Discount Ninja know that it should re-render the line item prices and subtotal in the drawer cart.</td></tr></tbody></table>

#### Syntax

```javascript
function test() {
    //Custom logic that opens the drawer cart
    //And renders the content (including prices)
    document.dispatchEvent(new CustomEvent('la:dn:drawercart:opened'));
}
```

## Entitlements calculated

#### Details

<table data-header-hidden><thead><tr><th width="157"></th><th></th></tr></thead><tbody><tr><td><strong>Event</strong></td><td><code>la:dn:entitlements:calculated</code></td></tr><tr><td><strong>Direction</strong></td><td>This event is published by the app. You can optionally subscribe to it.</td></tr><tr><td><strong>Behavior</strong></td><td>Published when the (discounted) cart and the associated entitlements have been calculated. </td></tr><tr><td></td><td>This event is published when the page is loaded and is published again when the content of the cart changes or the set of applied promotions changes.</td></tr><tr><td><strong>Event properties</strong></td><td>The event data contains an array with <a href="/pages/jSUo98LqzKbt8ynNaxPN#entitlement-info">Entitlement info</a> objects. Each object documents if the user is entitled to a gift for a gift offer.</td></tr></tbody></table>

#### Syntax

```javascript
document.addEventListener("la:dn:entitlements:calculated", function(event) {
    const entitlements = event.detail.data[0]; 
    console.log('Entitlements calculated', entitlements); 
});
```

## Promotion code added

#### Details

<table data-header-hidden><thead><tr><th width="157"></th><th></th></tr></thead><tbody><tr><td><strong>Event</strong></td><td><code>la:dn:promotioncode:added</code></td></tr><tr><td><strong>Direction</strong></td><td>This event is published by the app. You can optionally subscribe to it.</td></tr><tr><td><strong>Behavior</strong></td><td>Published when the a promotion code is redeemed using the promotion code field. </td></tr><tr><td><strong>Event properties</strong></td><td>The event data contains:<br>- <code>promotionCode</code>: the promotion code that is applied<br>- <code>success</code>: a boolean indicating that the code was added successfully<br>- <code>manual</code>: a boolean indicating if the code was added manually or by the system</td></tr></tbody></table>

#### Syntax

```javascript
document.addEventListener("la:dn:promotioncode:added", function(event) {
    const eventData = event.detail.data[0]; 
    console.log('Promotion code added', eventData); 
});
```

## Promotion code removed

#### Details

<table data-header-hidden><thead><tr><th width="157"></th><th></th></tr></thead><tbody><tr><td><strong>Event</strong></td><td><code>la:dn:promotioncode:removed</code></td></tr><tr><td><strong>Direction</strong></td><td>This event is published by the app. You can optionally subscribe to it.</td></tr><tr><td><strong>Behavior</strong></td><td>Published when the a promotion code is removed using the promotion code field. </td></tr><tr><td><strong>Event properties</strong></td><td>The event data contains:<br>- <code>promotionCode</code>: the promotion code that is removed<br>- <code>success</code>: a boolean indicating that the code was removed successfully</td></tr></tbody></table>

#### Syntax

```javascript
document.addEventListener("la:dn:promotioncode:removed", function(event) {
    const eventData = event.detail.data[0]; 
    console.log('Promotion code removed', eventData); 
})
```


# Objects

{% hint style="info" %}
The documentation below describes the API as of version 9.x of the script.&#x20;

Public properties or functions that are not in the `discountNinja.api` namespace should be considered obsolete and will be removed in the next major version release.

Using undocumented functions or objects will result in issues as custom code that relies on those functions or objects may break in the future when Discount Ninja's script is automatically updated.
{% endhint %}

{% hint style="info" %}
Note that the properties of the objects made available by the API should be considered **read-only.** To modify the state of the object's properties, the user can:

* call one of the available [functions](/discount-ninja-developer-hub/storefront-api/promotion-engine/javascript-api/functions)
* publish one of the available [events](/discount-ninja-developer-hub/storefront-api/promotion-engine/javascript-api/events)
  {% endhint %}

## Product variant price info

<table data-header-hidden><thead><tr><th width="241">Property</th><th width="139">Type</th><th></th></tr></thead><tbody><tr><td><code>amount</code></td><td><code>number</code></td><td>The discount amount, expressed in cents.</td></tr><tr><td><code>discountCode</code></td><td><code>string</code></td><td>The offer token, Shopify discount code or promotion code associated with the applied discount.</td></tr><tr><td><code>discountedPrice</code></td><td><code>number</code></td><td>The discounted price, expressed in cents.</td></tr><tr><td><code>isCurrentProduct</code></td><td><code>boolean</code></td><td><code>true</code> if the user is on a product page that matches the handle for this object</td></tr><tr><td><code>isDiscountedByOffer</code></td><td><code>boolean</code></td><td><code>true</code> if the variant is discounted by a Discount Ninja offer (not, for example, a compare-at price).</td></tr><tr><td><code>offerToken</code></td><td><code>string</code></td><td>The token of the offer associated with the applied discount.</td></tr><tr><td><code>originalPrice</code></td><td><code>number</code></td><td>The original price (i.e. the price before discounts are applied), expressed in cents.</td></tr><tr><td><code>percentage</code></td><td><code>number</code></td><td>The discount percentage.</td></tr><tr><td><code>productHandle</code></td><td><code>string</code></td><td>The handle of the product associated with the object.</td></tr><tr><td><code>sellingPlan</code></td><td><code>number</code> | <code>null</code></td><td>The id of the selected selling plan for subscription products. <code>Null</code> for one-time purchases.</td></tr><tr><td><code>promotionToken</code></td><td><code>string</code></td><td>The token of the promotion associated with the applied discount.</td></tr><tr><td><code>variantId</code></td><td><code>number</code> | <code>null</code></td><td>The id of the selected variant. <code>Null</code> if the variant is not known, for example for products on collection pages.</td></tr></tbody></table>

## Entitlement info

<table data-header-hidden><thead><tr><th width="243">Property</th><th width="192">Type</th><th></th></tr></thead><tbody><tr><td><code>offerToken</code></td><td><code>string</code></td><td>The token of the offer.</td></tr><tr><td><code>applied</code></td><td><code>boolean</code></td><td>Indicates if the offer has been applied to the cart.</td></tr><tr><td><code>source</code></td><td><code>"app" | "native" | null</code></td><td>Set to "<code>app</code>" if the offer was configured in Discount Ninja. If set to "<code>native</code>", this offer is a wrapper for a discount configured in Shopify.</td></tr><tr><td><code>prerequisite</code></td><td><code>object</code></td><td></td></tr><tr><td>   <code>prerequisiteItems</code></td><td><code>array</code></td><td>An array of objects that lists the line items that are considered prerequisites for this offer to apply.<br><br>Each object has two properties:<br>- <code>key</code> (<code>string</code>): the key of the line item<br>- <code>quantity</code> (<code>number</code>): the number of items on the line item that are prerequisites</td></tr><tr><td><code>entitlement</code></td><td><code>object</code></td><td></td></tr><tr><td>   <code>amount</code></td><td><code>number | null</code></td><td>The amount that is discounted. <code>Null</code> for a shipping offer.</td></tr><tr><td>   <code>level</code></td><td><code>"product" | "order" | "shipping"</code></td><td>Indicates at what level the offer discounts the cart.</td></tr><tr><td>   <code>type</code></td><td><code>"gift" | "percentage" | "fixed_amount" | "fixed_price"</code></td><td>The type of entitlement that is applied by the offer.</td></tr><tr><td>   <code>target</code></td><td><code>object</code></td><td></td></tr><tr><td>      <code>variants</code></td><td><code>string | null</code></td><td>A comma-separated list of variant ids the customer is entitled to.</td></tr><tr><td>      <code>collections</code></td><td><code>string | null</code></td><td>A comma-separated list of collection handles the customer is entitled to.</td></tr><tr><td>      <code>products</code></td><td><code>string | null</code></td><td>A comma-separated list of product handles the customer is entitled to.</td></tr><tr><td>      <code>allProducts</code></td><td><code>boolean</code></td><td>Set to <code>true</code> if the entitlement applies to all products.</td></tr><tr><td>      <code>shipping</code></td><td><code>boolean</code></td><td>Set to <code>true</code> if the entitlement applies to the shipping line.</td></tr><tr><td>   <code>entitledItems</code></td><td><code>object</code></td><td></td></tr><tr><td>      <code>quantity</code></td><td><code>number</code></td><td>The number of items the customer is entitled to.</td></tr><tr><td>      <code>claimed</code></td><td><code>number</code></td><td>The number of entitled items the customer has added to the cart.</td></tr><tr><td>      <code>lineItems</code></td><td><code>array</code></td><td>An array of objects that lists the line items that are considered entitled items.<br><br>Each object has two properties:<br>- <code>key</code> (<code>string</code>): the key of the line item<br>- <code>quantity</code> (<code>number</code>): the number of items on the line item that are entitlements</td></tr><tr><td>      <code>sets</code></td><td><code>array</code></td><td>An array of objects that lists the sets ( a group of prerequisites and entitlements).</td></tr></tbody></table>

## Discounted Cart

<table data-header-hidden><thead><tr><th width="283">Property</th><th width="192">Type</th><th></th></tr></thead><tbody><tr><td><code>appliedPromotions</code></td><td><code>array</code></td><td>The list of the product, discount and shipping promotions applied to the cart.</td></tr><tr><td>   <code>offerToken</code></td><td><code>string</code></td><td>The token of the Discount Ninja offer.</td></tr><tr><td>   <code>promotionToken</code></td><td><code>string</code></td><td>The token of the Discount Ninja promotion.</td></tr><tr><td>   <code>discountCode</code></td><td><code>string | null</code></td><td>The discount code if the discount is applied using a Shopify discount code. <code>Null</code> if the discount is applied using a Discount Ninja offer.</td></tr><tr><td>   <code>priceRuleId</code></td><td><code>string | null</code></td><td>The identifier of the Shopify price rule if the discount is applied using a Shopify discount code. <code>Null</code> if the discount is applied using a Discount Ninja offer.</td></tr><tr><td>   <code>amount</code></td><td><code>number</code></td><td>The discount amount, in cents.</td></tr><tr><td>   <code>tier</code></td><td><code>number</code></td><td>The number of the tier of the offer that is applied. The first tier is <code>1</code>.</td></tr><tr><td>   <code>level</code></td><td><code>"product" | "order" | "shipping"</code> </td><td>Indicates at what level the offer discounts the cart.</td></tr><tr><td><code>currency</code></td><td><code>string</code></td><td>The <a href="https://www.iso.org/iso-4217-currency-codes.html">ISO code</a> of the currency of the cart.</td></tr><tr><td><code>discountCode</code></td><td><code>string | null</code></td><td>The native Shopify discount code that is applied to the cart. <code>Null</code> if no code is applied.</td></tr><tr><td><code>items</code></td><td><code>array</code></td><td>The list of line items in the cart.</td></tr><tr><td>   <code>offerToken</code></td><td><code>string</code></td><td>The token of the Discount Ninja offer.</td></tr><tr><td>   <code>promotionToken</code></td><td><code>string</code></td><td>The token of the Discount Ninja promotion.</td></tr><tr><td>   <code>discountCode</code></td><td><code>string | null</code></td><td>The discount code if the product discount is applied using a Shopify discount code. <code>Null</code> if the product discount is applied using a Discount Ninja offer.</td></tr><tr><td>   <code>priceRuleId</code></td><td><code>string | null</code></td><td>The identifier of the Shopify price rule if the product discount is applied using a Shopify discount code. <code>Null</code> if the product discount is applied using a Discount Ninja offer.</td></tr><tr><td>   <code>discount</code></td><td><code>object</code></td><td></td></tr><tr><td>      <code>product</code></td><td><code>object</code></td><td></td></tr><tr><td>         <code>amount</code></td><td><code>number</code></td><td>The discount, in cents, per product.</td></tr><tr><td>         <code>percentage</code></td><td><code>number</code></td><td>The discount per product as a percentage.</td></tr><tr><td>      <code>line</code></td><td><code>object</code></td><td></td></tr><tr><td>         <code>amount</code></td><td><code>number</code></td><td>The discount for the cart line, in cents.</td></tr><tr><td>         <code>percentage</code></td><td><code>number</code></td><td>The discount for the cart line as a percentage.</td></tr><tr><td>   <code>isGift</code></td><td><code>boolean</code></td><td><code>True</code> if the item represents a gift with purchase.</td></tr><tr><td>   <code>isSubscriptionProduct</code></td><td><code>boolean</code></td><td><code>True</code> if the item is purchased with a selling plan.</td></tr><tr><td>   <code>price</code></td><td><code>object</code></td><td></td></tr><tr><td>      <code>product</code></td><td><code>object</code></td><td></td></tr><tr><td>         <code>discounted</code></td><td><code>number</code></td><td>The discounted price per product, in cents.</td></tr><tr><td>         <code>original</code></td><td><code>number</code></td><td>The original price per product, in cents.</td></tr><tr><td>         <code>compareAt</code></td><td><code>number</code></td><td>The compare-at price per product, in cents.</td></tr><tr><td>      <code>line</code></td><td><code>object</code></td><td></td></tr><tr><td>         <code>discounted</code></td><td><code>number</code></td><td>The discounted price for the cart line, in cents.</td></tr><tr><td>         <code>original</code></td><td><code>number</code></td><td>The compare-at price for the cart line, in cents.</td></tr><tr><td>         <code>compareAt</code></td><td><code>number</code></td><td>The compare-at price for the cart line, in cents.</td></tr><tr><td><code>product</code></td><td><code>object</code></td><td></td></tr><tr><td>   <code>available</code></td><td><code>boolean</code></td><td><code>True</code> if the variant is available. <code>False</code> if the variant is out of stock.</td></tr><tr><td>   <code>collectionIds</code></td><td><code>string | null</code></td><td>A comma-separated list of collection identifiers associated with the product. <code>Null</code> if the product does not belong to any collections. </td></tr><tr><td>   <code>collections</code></td><td><code>string | null</code></td><td>A comma-separated list of collection handles associated with the product. <code>Null</code> if the product does not belong to any collections. </td></tr><tr><td>   <code>productHandle</code></td><td><code>string</code></td><td>The handle of the product.</td></tr><tr><td>   <code>productId</code></td><td><code>number</code></td><td>The identifier of the product.</td></tr><tr><td>   <code>sellingPlanId</code></td><td><code>number | null</code></td><td>The identifier of the selling plan. <code>Null</code> for one-time purchases.</td></tr><tr><td>   <code>tags</code></td><td><code>string | null</code></td><td>A comma-separated list of tags associated with the product. <code>Null</code> if the product does not have tags. </td></tr><tr><td>   <code>title</code></td><td><code>string</code></td><td>The title of the variant.</td></tr><tr><td>   <code>variantId</code></td><td><code>number</code></td><td>The identifier of the variant.</td></tr><tr><td><code>orderDiscount</code></td><td><code>number</code></td><td>The total order discount in cents.</td></tr><tr><td><code>orderDiscounts</code></td><td><code>array</code></td><td>The list of order discounts applied to the cart.</td></tr><tr><td>   <code>offerToken</code></td><td><code>string</code></td><td>The token of the Discount Ninja offer.</td></tr><tr><td>   <code>promotionToken</code></td><td><code>string</code></td><td>The token of the Discount Ninja promotion.</td></tr><tr><td>   <code>discountCode</code></td><td><code>string | null</code></td><td>The discount code if the order discount is applied using a Shopify discount code. <code>Null</code> if the order discount is applied using a Discount Ninja offer.</td></tr><tr><td>   <code>priceRuleId</code></td><td><code>string | null</code></td><td>The identifier of the Shopify price rule if the order discount is applied using a Shopify discount code. <code>Null</code> if the order discount is applied using a Discount Ninja offer.</td></tr><tr><td>   <code>amount</code></td><td><code>number</code></td><td>The discount amount, in cents.</td></tr><tr><td>   <code>percentage</code></td><td><code>number</code></td><td>The discount percentage.</td></tr><tr><td>   <code>tier</code></td><td><code>number</code></td><td>The number of the tier of the offer that is applied. The first tier is <code>1</code>.</td></tr><tr><td>   <code>lineItems</code></td><td><code>array</code></td><td>The list of line items affected by the order discount.</td></tr><tr><td>   <code>valueType</code></td><td><code>"percentage" | "fixed_amount" | "fixed_price"</code></td><td>The type of discount.</td></tr><tr><td><code>shipping</code></td><td><code>object | undefined</code></td><td>An object containing information about the shipping discount that is applied to the cart. <code>Undefined</code> if no shipping discount is applied.</td></tr><tr><td>   <code>offerToken</code></td><td><code>string</code></td><td>The token of the Discount Ninja offer.</td></tr><tr><td>   <code>promotionToken</code></td><td><code>string</code></td><td>The token of the Discount Ninja promotion.</td></tr><tr><td>   <code>discountCode</code></td><td><code>string | null</code></td><td>The discount code if the shipping discount is applied using a Shopify discount code. <code>Null</code> if the shipping discount is applied using a Discount Ninja offer.</td></tr><tr><td>   <code>priceRuleId</code></td><td><code>number | null</code></td><td>The identifier of the Shopify price rule if the shipping discount is applied using a Shopify discount code. <code>Null</code> if the shipping discount is applied using a Discount Ninja offer.</td></tr><tr><td><code>subtotal</code></td><td><code>object</code></td><td>Overview of the subtotal of the cart, after applying product discounts.</td></tr><tr><td>   <code>discount</code></td><td><code>number</code></td><td>The discount in cents. This is the sum of all product discounts.</td></tr><tr><td>   <code>discountedPrice</code></td><td><code>number</code></td><td>The subtotal in cents after product discounts.</td></tr><tr><td>   <code>originalPrice</code></td><td><code>number</code></td><td>The subtotal in cents before product discounts.</td></tr><tr><td><code>total</code></td><td><code>object</code></td><td>Overview of the total of the cart, after applying product and order discounts.</td></tr><tr><td>      <code>discount</code></td><td><code>number</code></td><td>The discount in cents. This is the sum of all product and order discounts.</td></tr><tr><td>      <code>discountedPrice</code></td><td><code>number</code></td><td>The total in cents after product and order discounts.</td></tr><tr><td>      <code>originalPrice</code></td><td><code>number</code></td><td>The total in cents before product and order discounts.</td></tr><tr><td><code>token</code></td><td><code>number</code></td><td>The token of the cart, provided by Shopify.</td></tr></tbody></table>


# Error Messages

A description of the error messages that the JavaScript API may print to the browser console

```
    // start
    ST01: "Discount Ninja - Aborting because the script is running in the theme editor.",
    ST02: "Discount Ninja - Aborting because the script is already loaded.",
    ST03: "Discount Ninja - Intercept of AddEventListener failed.",
    ST04: "Limoni Apps DiscountNinja has detected it is running in the Shopify Admin theme editor. Aborting script.",
    ST05: "Discount Ninja - Could not prepare for design mode",

    // cart-text
    CT01: "CartText.create__MANGLED__",
    CT02: "CartText.render__MANGLED__",
    CT03: "CartText.reset__MANGLED__",

    // collection-badge
    CB01: "Badge.getHtml__MANGLED__",
    CB02: "Badge.add__MANGLED__",
    CB03: "Badge.create__MANGLED__",
    CB04: "Badge.render__MANGLED__",

    // discounted-price
    BDC01: "DiscountedPrice.create__MANGLED__",
    BDC02: "DiscountedPrice.render__MANGLED__",
    BDC03: "DiscountedPrice.isFirstTime__MANGLED__",
    BDC04: "DiscountedPrice.buildFormattedPrice__MANGLED__",
    BDC05: "DiscountedPrice.createMutationObserver__MANGLED__",
    BDC06: "DiscountedPrice.getBlockByProductHandle__MANGLED__",

    // drawer-cart
    DRC01: "DrawerCart.subscribeToEvents__MANGLED__",
    DRC02: "onSuccess",
    DRC03: "DrawerCart.refresh__MANGLED__",

    // notification
    N01: "Notification.getStyle__MANGLED__",
    N02: "Notification.getText__MANGLED__",
    N03: "Notification.getHtml__MANGLED__",
    N04: "Notification.hide__MANGLED__",
    N05: "Notification.toggleMinMaximize__MANGLED__",
    N06: "Notification.minimize__MANGLED__",
    N07: "Notification.appSettings__MANGLED__",
    N08: "Notification.defaultOffer__MANGLED__",
    N09: "Notification.maximize__MANGLED__",
    N10: "Notification.remove__MANGLED__",
    N11: "Notification.getOfferCounterElement__MANGLED__",
    N12: "Notification.updateOfferCounter__MANGLED__",
    N13: "Notification.getFromCache__MANGLED__",
    N14: "Notification.saveToCache__MANGLED__",
    N15: "Notification.add__MANGLED__",
    N16: "Notification.clearAnimation__MANGLED__",
    N17: "Notification.applyAnimation__MANGLED__",
    N18: "Notification.actAfterDelay__MANGLED__",
    N19: "Notification.AnimateNewPromotionNotifications__MANGLED__",
    N20: "Notification.AnimateNewPromotionNotifications__MANGLED__",
    N21: "Notification.highlightNewOffers.highlightNewOffers__MANGLED__",
    N22: "Notification.highlightNewOffers.animateCounter__MANGLED__",
    N23: "Notification.highlightNewOffers__MANGLED__",
    N24: "Notification.markNotificationsAsAnimated__MANGLED__",
    N25: "Notification.animateOnReminder__MANGLED__",
    N26: "Notification.createInternalAfterEnsuringDependencies__MANGLED__",
    N27: "Notification.createInternal__MANGLED__",
    N28: "Notification.create__MANGLED__",
    N29: "Expected token",
    N30: "Notification.render__MANGLED__",
    N31: "Notification.renderExistingNotification__MANGLED__",
    N32: "Notification.createNotificationsArray__MANGLED__",
    N33: "Notification.isNotificationRequired__MANGLED__",
    N34: "Notification.makeNotificationBody__MANGLED__",
    N35: "Notification.getNotificationsToBeAnimated__MANGLED__",

    // notification-web-component
    NWC01: "NotificationWebComponent.render__MANGLED__",
    NWC02: "NotificationWebComponent.addNotificationAttributes__MANGLED__",
    NWC03: "NotificationWebComponent.getMessages__MANGLED__",

    // pop-up
    PU01: "PopUp.hide__MANGLED__",
    PU02: "PopUp.injectPopUpHtml__MANGLED__",
    PU03: "PopUp.cancel__MANGLED__",
    PU04: "PopUp.accept__MANGLED__",
    PU05: "PopUp.show__MANGLED__",
    PU06: "Showing pop-up",

    // price
    BBP01: "PriceEngine.render__MANGLED__",
    BBP02: "PriceEngine.renderLabels__MANGLED__",
    BBP03: "PriceEngine.renderPrice__MANGLED__",
    BBP04: "PriceEngine.renderLabel__MANGLED__",
    BBP05: "PriceEngine.renderPriceFooter__MANGLED__",
    BBP06: "PriceEngine.getProductByLocationAndHandle__MANGLED__",
    BBP07: "PriceEngine.getDiscountedProductByLocationAndHandle__MANGLED__",
    BBP08: "PriceEngine.getPriceFormatByLocation__MANGLED__",

    // product-banner
    PB01: "ProductBanner.create__MANGLED__",
    PB02: "ProductBanner.render__MANGLED__",
    PB03: "ProductBanner.hide__MANGLED__",
    PB04: "ProductBanner.show__MANGLED__",
    PB05: "ProductBanner.inject__MANGLED__",
    PB06: "ProductBanner.getCommentElements__MANGLED__",

    // promotion-code-field
    PCF01: "PromotionCodeField.initialize__MANGLED__",
    PCF02: "PromotionCodeField.create__MANGLED__",
    PCF03: "PromotionCodeField.createTags__MANGLED__",
    PCF04: "PromotionCodeField.render__MANGLED__",
    PCF05: "PromotionCodeField.codes__MANGLED__",
    PCF06: "PromotionCodeField.lookup__MANGLED__",
    PCF07: "PromotionCodeField.delete__MANGLED__",

    // side-effects
    SE01: "SideEffects.apply__MANGLED__",

    // sticky-bar
    SB01: "StickyBar.appSettings__MANGLED__",
    SB02: "StickyBar.activeStickyBar__MANGLED__",
    SB03: "StickyBar.defaultOffer__MANGLED__",
    SB04: "StickyBar.shouldRequestNotification__MANGLED__",
    SB05: "StickyBar.topMarginElementsInner__MANGLED__",
    SB06: "StickyBar.topMarginElements__MANGLED__",
    SB07: "StickyBar.addTopMarginToElements__MANGLED__",
    SB08: "StickyBar.slideBodyDown__MANGLED__",
    SB09: "StickyBar.renameCssClass__MANGLED__",
    SB10: "StickyBar.reactivateStickyBarClasses__MANGLED__",
    SB11: "StickyBar.onAction__MANGLED__",
    SB12: "StickyBar.hideInner__MANGLED__",
    SB13: "StickyBar.hide__MANGLED__",
    SB14: "StickyBar.remove__MANGLED__",
    SB15: "StickyBar.getFromCache__MANGLED__",
    SB16: "StickyBar.saveToCache__MANGLED__",
    SB17: "StickyBar.add__MANGLED__",
    SB18: "StickyBar.cycleMessage.cycleMessageInner__MANGLED__",
    SB19: "StickyBar.cycleMessage__MANGLED__",
    SB20: "StickyBar.clearAnimation__MANGLED__",
    SB21: "StickyBar.applyAnimation__MANGLED__",
    SB22: "StickyBar.getBodyMessage__MANGLED__",
    SB23: "StickyBar.getTimerFooterMessage__MANGLED__",
    SB24: "StickyBar.show__MANGLED__",
    SB25: "StickyBar.show__MANGLED__.actionButton1Click__MANGLED__",
    SB26: "StickyBar.show__MANGLED__.actionButton2Click__MANGLED__",
    SB27: "StickyBar.showMessage__MANGLED__",
    SB28: "StickyBar.getHtml__MANGLED__",
    SB29: "StickyBar.show__MANGLED__.dismiss__MANGLED__",
    SB30: "StickyBar.animateOnReminder__MANGLED__",
    SB31: "StickyBar.show__MANGLED__",
    SB32: "Sticky bar could not be positioned under header. Header not found. Mark it with the CSS class: limoniapps-discountninja-headersection",
    SB33: "StickyBar.activeStickyBar__MANGLED__",
    SB34: "Sticky bar not found",
    SB35: "Expected token",
    SB36: "StickyBar.create__MANGLED__",
    SB37: "StickyBar.render__MANGLED__",
    SB38: "StickyBar.createStickyBarArray__MANGLED__",
    SB39: "StickyBar.checkShowPusher__MANGLED__",
    SB40: "StickyBar.isStickyBarRequired__MANGLED__",
    SB41: "StickyBar.createMessages__MANGLED__",
    SB42: "StickyBar.getBody__MANGLED__",
    SB43: "StickyBar.animateOnGoal__MANGLED__",

    // timer
    T01: "Timer.processCustomTimeLabels__MANGLED__",
    T02: "Timer.processCountdownClock__MANGLED__",
    T03: "Timer.start__MANGLED__",
    T04: "Timer.getTimeLabel__MANGLED__",
    T05: "Timer.getFormattingForStyle__MANGLED__",
    T06: "Timer.timeFormattingInternal__MANGLED__",
    T07: "Timer.expire__MANGLED__",
    T08: "Timer.updateTimer__MANGLED__",
    T09: "Expiring timer",

    // events
    // local-cart-updated-event
    LCUE01: "LocalCartUpdatedEvent__MANGLED__",

    // server-cart-items-changed-event
    SCICE01: "ServerCartItemsChangedEvent__MANGLED__",

    // server-collection-products-page-loaded-event
    SCPPLE01: "ServerCollectionProductsPageLoadedEvent__MANGLED__",

    // variant-changed-event
    VCE01: "VariantChangedEvent",

    // utilities
    // cache
    C01: "Cache.indexedDbInit__MANGLED__",
    C02: "Cache.indexedDbCreateTransaction__MANGLED__",
    C03: "Cache.indexedDbClear__MANGLED__",
    C04: "Cache.indexedDbGet__MANGLED__",
    C05: "Cache.indexedDbSet__MANGLED__",
    C06: "Cache.localRemove__MANGLED__",
    C07: "Cache.localGet__MANGLED__",
    C08: "Cache.localSet__MANGLED__",
    C09: "Cache.sessionRemove__MANGLED__",
    C10: "Cache.sessionGet__MANGLED__",
    C11: "Cache.sessionSet__MANGLED__",
    C12: "Cache.fallbackRemove__MANGLED__",
    C13: "Cache.fallbackGet__MANGLED__",
    C14: "Cache.fallbackSet__MANGLED__",
    C15: "Cache.fallbackClear__MANGLED__",
    C16: "Cache.localClear__MANGLED__",
    C17: "Cache.sessionClear__MANGLED__",
    C18: "Cache.clear__MANGLED__",
    C19: "IndexedDb not supported",
    C20: "Cache",
    C21: "Could not get data from IndexedDb",
    C22: "Could not set data in IndexedDb",

    // cookie
    COO01: "Cookie.markDiscountCodeAsOwned",
    COO02: "Cookie.isOwnedDiscountCode__MANGLED__",

    // currency
    CUR01: "Currency.activeCurrency__MANGLED__",
    CUR02: "Currency.escapeDollarSignForRegEx__MANGLED__",
    CUR03: "Currency.getSymbolFirst__MANGLED__",
    CUR04: "Currency.getSpace__MANGLED__",
    CUR05: "Currency.getThousandSeparator__MANGLED__",
    CUR06: "Currency.getFractionalSeparator__MANGLED__",
    CUR07: "Currency.getFormat__MANGLED__",
    CUR08: "Currency.getCurrencyFormat__MANGLED__",
    CUR09: "Currency.getCurrencyNumberFormat__MANGLED__",
    CUR10: "Currency.getConversionRate__MANGLED__",
    CUR11: "Currency.getConversionRate__MANGLED__",
    CUR12: "Currency.convertToBaseCurrency__MANGLED__",
    CUR13: "Currency.convertTo__MANGLED__",
    CUR14: "Currency.getCurrencyFormatForCurrency__MANGLED__",
    CUR15: "Currency.getCurrencyDefaultFormat__MANGLED__",

    // date-time
    DT01: "DateTime.formatDate__MANGLED__",

    // localization
    L01: "Localization.getLabel",
    L02: "Localization.formatLabel__MANGLED__",

    // events
    E01: "Events.clearListeners__MANGLED__",
    E02: "Events.publish__MANGLED__",
    E03: "Events.subscribe__MANGLED__",
    E04: "Events.addEventListener__MANGLED__",

    // html
    H01: "Html.append__MANGLED__",
    H02: "Html.getElementById__MANGLED__",
    H03: "Html.getNodesUnderElements__MANGLED__",
    H04: "Html.getNodesUnderElement__MANGLED__",
    H05: "Html.writeHtml__MANGLED__",
    H06: "Html.getNodes__MANGLED__",
    H07: "Html.getNode__MANGLED__",
    H08: "Html.filterNodes__MANGLED__",
    H09: "Html.filterNodesWhichDontHaveParent__MANGLED__",
    H10: "Html.filterVisible__MANGLED__",
    H11: "Html.writeCss__MANGLED__",
    H12: "Html.isVisible__MANGLED__",
    H13: "Html.isHidden__MANGLED__",
    H14: "Html.getParents__MANGLED__",
    H15: "Html.excludeNodes__MANGLED__",
    H16: "Html.setNodeData__MANGLED__",
    H17: "Html.filterVisible__MANGLED__",
    H18: "Html.getFirstNodeUnderElement__MANGLED__",
    H19: "Html.getFirstNode__MANGLED__",
    H20: "Html.getTemplate__MANGLED__",
    H21: "Html.actionForNodes__MANGLED__",
    H22: "Html.replaceHtml__MANGLED__",
    H23: "Html.addClass__MANGLED__",
    H24: "Html.hasClass__MANGLED__",
    H25: "Html.makeEmpty__MANGLED__",
    H26: "Html.addClass__MANGLED__",
    H27: "Html.removeClass__MANGLED__",
    H28: "Html.removeClass__MANGLED__",
    H29: "Html.remove__MANGLED__",
    H30: "Html.getClosest__MANGLED__",
    H31: "Html.show__MANGLED__",
    H32: "Html.hide__MANGLED__",
    H33: "Html.toggle__MANGLED__",
    H34: "Html.getValue__MANGLED__",
    H35: "Html.wrapNodes__MANGLED__",
    H36: "Html.wrapInner__MANGLED__",
    H37: "Html.replaceWith__MANGLED__",
    H38: "Html.getAttr__MANGLED__",
    H39: "Html.iterateAttrs__MANGLED__",
    H40: "Html.setAttr__MANGLED__",
    H41: "Html.removeAttr__MANGLED__",
    H42: "Html.setAttribute__MANGLED__",
    H43: "Html.addEventListener__MANGLED__",
    H44: "Html.stopEvent__MANGLED__",
    H45: "Html.iterateElements__MANGLED__",
    H46: "Html.getNextElement__MANGLED__",
    H47: "Html.createFragment__MANGLED__",
    H48: "Html.addInlineCssToDocument__MANGLED__",
    H49: "Html.isInViewport__MANGLED__",
    H50: "Html.supportsDOMParser__MANGLED__",
    H51: "Html.stringToHTML__MANGLED__",

    // leaky-bucket
    LB01: "LeakyBucket",
    LB02: "Bucket is not initialized",

    // multi-currency
    MC01: "updateCurrencyGeolizr",
    MC02: "An error occurred outside of Discount Ninja, when the function window.convertCurrencies() is called",
    MC03: "updateCurrencyStandardConvertAll",
    MC04: "updateCurrencyDoubly",
    MC05: "updateCurrency__MANGLED__ Switch currency",
    MC06: "updateCurrency__MANGLED__",
    MC07: "updateCurrencyGeolizr - Updating currency",
    MC08: "Currency unavailable",
    MC09: "Updating currency - convertCurrencies",
    MC10: "Updating currency (.limoniapps-discountninja-money only) - convertAll",
    MC11: "Updating currency - Doubly - Back to base currency",
    MC12: "Updating currency - Doubly - To current currency",

    // query-parameter
    QP01: "QueryParameter.getQueryParameters__MANGLED__",
    QP02: "QueryParameter.get__MANGLED__",
    QP03: "window.history.replaceState__MANGLED__ not supported",
    QP04: "QueryParameter.remove__MANGLED__",
    QP05: "QueryParameter.removeFromUrl__MANGLED__",
    QP06: "QueryParameter.appendToUrl__MANGLED__",

    // text-scaling
    TSC01: "TextScaling.replacePxWithRem__MANGLED__",

    // text-settings
    TSS01: "TextSettings.getText__MANGLED__",

    // utilities
    U01: "Utilities.jsonClone",
    U02: "Utilities.objectIsEmpty",
    U03: "Utilities.hash",
    U04: "Utilities.newGuid",
    U05: "Utilities.repeatWithIntervals",
    U06: "Utilities.numberOfFractionalDigits",
    U07: "Utilities.fixedMultipliedAmount",
    U08: "Utilities.roundToNumberOfDigits",
    U09: "Utilities.floorToNumberOfDigits",
    U10: "Utilities.decodeUriComponent",
    U11: "Utilities.encodeUriComponent",
    U12: "Utilities.encodeUriComponentForQueryString",
    U13: "Utilities.getNumberFromHtmlFragment",
    U14: "Utilities.isValidEmailAddress",
    U15: "Utilities.getMutationObserver",
    U17: "Utilities.getNoDecimalsCurrencyFormat",
    U18: "Utilities.getCurrencyFormatAmountComponent",
    U19: "Utilities.stringIsIn",
    U20: "Utilities.isMobilePhone",
    U21: "Utilities.isNumeric",
    U22: "Utilities.getHandleFromSelector",
    U23: "Utilities.getCountryName",
    U24: "Utilities.cancelEventDefaultBehavior",
    U25: "Utilities.getRandomNumber",
    U26: "Utilities.getEmoji",
    U27: "Utilities.extractSeparator",
    U28: "Utilities.getReferrer",
    U29: "Utilities.formatMoney",
    U30: "Utilities.getStandardFontSizeInternal",
    U31: "Utilities.getFontScaleInternal",
    U32: "Utilities.getStandardFontSize",
    U33: "Utilities.getFontScale",
    U34: "Utilities.getHostnameWithoutSubdomain",
    U35: "Utilities.endsWith",

    // variant-change
    VC01: "VariantChange.variantChangeDetected__MANGLED__",
    VC02: "VariantChange.observeVariantChangeInForms__MANGLED__",
    VC03: "VariantChange.registerEventHandlerForVariantChange__MANGLED__",

    // xml-http-request
    XHR01: "XMLHttpRequest.createFetchInterceptor__MANGLED__",
    XHR02: "CreateQueryStringFromParameters failed",
    XHR03: "XMLHttpRequest.requestOnSuccess__MANGLED__ failed",
    XHR04: "XMLHttpRequest.send__MANGLED__",
    XHR05: "XmlHttpRequest.sendBeacon__MANGLED__",
    XHR06: "XMLHttpRequest.xmlHttpInterceptIsConnected__MANGLED__",
    XHR07: "XMLHttpRequest.fetchInterceptIsConnected__MANGLED__",
    XHR08: "XMLHttpRequest.getRequestIdHeader__MANGLED__",
    XHR09: "XMLHttpRequest.handleFetch__MANGLED__",
    XHR10: "XMLHttpRequest.getRequestIdHeader__MANGLED__",
    XHR11: "XMLHttpRequest.handleAjaxSuccess__MANGLED__",
    XHR12: "XMLHttpRequest.shouldHandleRequest__MANGLED__",
    XHR13: "XMLHttpRequest.handleSend__MANGLED__",
    XHR14: "XMLHttpRequest.handleNetworkRequest__MANGLED__",
    XHR15: "XMLHttpRequest.createSendInterceptor__MANGLED__",
    XHR16: "XMLHttpRequest.createFetchInterceptor__MANGLED__",
    XHR17: "Invalid url in HandleNetworkRequest",
    XHR18: "XMLHttpRequest interceptor disconnected",
    XHR19: "Fetch interceptor disconnected",
    XHR20: "XMLHttpRequest.requestOnSuccess received empty xhr",
    XHR21: "Core",
    XHR22: "XMLHttpRequest not available",
    XHR23: "Fetch not available",
    XHR24: "XMLHttpRequest.send",

    // modules
    // account-login
    AL01: "AccountLogin.register__MANGLED__",

    // action-button
    AC01: "ActionButton.hideIfNecessary__MANGLED__",
    AC02: "ActionButton.handleAction__MANGLED__",
    AC03: "ActionButton.replacePlaceHoldersForButton__MANGLED__",

    // browsing-context
    BC01: "BrowsingContext.parseBrowsingContext__MANGLED__",
    BC02: "BrowsingContext.getBrowsingContextFromCache__MANGLED__",
    BC03: "BrowsingContext.getCountryFromBrowsingContext__MANGLED__",

    // buy-now
    BN01: "BuyNow.checkOut__MANGLED__",
    BN02: "BuyNow.getQuantityFromForm__MANGLED__",
    BN03: "BuyNow.checkOutSelectedVariant__MANGLED__",
    BN04: "checkOutSelectedVariant__MANGLED__ expects to be executed on a product page",

    // cart
    CAR01: "Cart.useCartJSApi__MANGLED__",
    CAR02: "DiscountCode.appendLocaleToUrl__MANGLED__",
    CAR03: "Cart.addLocale__MANGLED__",
    CAR04: "Cart.subscribeDrawerCartOpened__MANGLED__.CustomEvents",
    CAR05: "Cart.subscribeDrawerCartOpened__MANGLED__",
    CAR06: "Cart.onDrawerCartOpenedDebounced__MANGLED__",
    CAR07: "Cart.storeUpdatedCart__MANGLED__",
    CAR08: "Cart.processLocalQuantityChange__MANGLED__.1",
    CAR09: "Cart.processLocalQuantityChange__MANGLED__.2",
    CAR10: "Cart.getCartFromCache__MANGLED__",
    CAR11: "Cart.copyProperties__MANGLED__",
    CAR12: "Cart.updateCartJSCart__MANGLED__",
    CAR13: "Cart.updateCart__MANGLED__",
    CAR14: "Cart.clearCart__MANGLED__",
    CAR15: "Cart.Shopify.addToCartMultipleLines__MANGLED__",
    CAR16: "Cart.Shopify.addToCart__MANGLED__",
    CAR17: "Cart.Shopify.getCartObjectFromNetworkResponse__MANGLED__",
    CAR18: "Cart.Shopify.updateCartFromNetworkResponse__MANGLED__",
    CAR19: "Cart.Shopify.onUpdateCart__MANGLED__",
    CAR20: "Cart.Shopify.setAttribute__MANGLED__",
    CAR21: "Cart.Shopify.registerEventHandlerForSubmit__MANGLED__",
    CAR22: "CartJs.clear__MANGLED__ failed",
    CAR23: "Cart",
    CAR24: "CartJs.addItems__MANGLED__ failed",
    CAR25: "addToCartMultipleLines",
    CAR26: "Call to bulk cart/add.js failed this is likely an issue with stock levels. Workaround failed.",
    CAR27: "addToCart",
    CAR28: "Could not clear gift markers in cart.",

    // cart-adjustments
    CA01: "CartAdjustments.requiresUpdateCart__MANGLED__ failed",
    CA02: "CartAdjustments.finishIncompleteUpdateCart__MANGLED__ failed",
    CA03: "CartAdjustments.updateCart__MANGLED__ failed",
    CA04: "CartAdjustments.reloadPage__MANGLED__",
    CA05: "CartAdjustments.executeDrawerCartRefreshMethod__MANGLED__.eval",
    CA06: "CartAdjustments.executeDrawerCartRefreshMethod__MANGLED__.PublishEvent",
    CA07: "CartAdjustments.executeDrawerCartRefreshMethod__MANGLED__",
    CA08: "CartAdjustments.mergeItems found undefined items",
    CA09: "Finishing incomplete update of cart",
    CA10: "Canceling update of cart (add) due to navigation",
    CA11: "Canceling update of cart (clear) due to navigation",
    CA12: "Reloading page to display cart adjustments. This behavior can be changed in the app.",
    CA13: "Cart",
    CA14: "Could not refill cart after clearing the cart.",

    // checkout
    CH01: "showCheckoutPopUp__MANGLED__",
    CH02: "registerEventHandlerForCheckout__MANGLED__.fallBack__MANGLED__",
    CH03: "registerEventHandlerForCheckout__MANGLED__.fallBack__MANGLED__ - Error when programmatically clicking checkout form submit button",
    CH04: "registerEventHandlerForCheckout__MANGLED__.redirectToStandardCheckoutFallback__MANGLED__",
    CH05: "showLoadingCursor__MANGLED__",
    CH06: "getCheckoutDiscountCodes__MANGLED__",
    CH07: "onpageshow",
    CH08: "registerOnPageshowPersisted",
    CH09: "DiscountCode.undoInstructCheckoutToRemoveDiscountCodeInner__MANGLED__",
    CH10: "Checkout.undoInstructCheckoutToRemoveDiscountCode__MANGLED__",
    CH11: "DiscountCode.removeDiscountCodeFromFormActionInner__MANGLED__",
    CH12: "removeDiscountCodeFromFormAction",
    CH13: "Checkout.Shopify.discountCodeInAction__MANGLED__",
    CH14: "Checkout.Shopify.StripDiscountCode__MANGLED__",
    CH15: "Checkout.Shopify.registerEventHandlerForCheckout__MANGLED__.onSuccess",
    CH16: "buildGetCheckoutUrlParameters__MANGLED__",
    CH17: "Checkout.Shopify.getCheckoutUrlFromServer__MANGLED__.onSuccess",
    CH18: "applyCheckoutDataDiscountCode__MANGLED__",
    CH19: "setLoadingCheckoutPopUpMessage__MANGLED__",
    CH20: "getCheckoutData__MANGLED__",
    CH21: "registerEventHandlerForCheckout__MANGLED__.disableCheckoutButton",
    CH22: "Checkout.Shopify.registerEventHandlerForCheckout__MANGLED__.onCheckoutViaButtonInner",
    CH23: "Checkout.Shopify.mandatoryCheckoutPrerequisitesNotComplete__MANGLED__ - Could not verify status",
    CH24: "Checkout.Shopify.termsAndConditionsCheckboxesNotChecked__MANGLED__ - Could not verify status",
    CH25: "Checkout.Shopify.thirdPartyNotReady__MANGLED__ - Failed",
    CH26: "Checkout.Shopify.zapietNotReady__MANGLED__ - Failed",
    CH27: "registerEventHandlerForCheckout__MANGLED__.handleClickBuyNowButton",
    CH28: "registerEventHandlerForCheckout__MANGLED__.onCheckoutViaButtonAfterUpdateCart",
    CH29: "registerEventHandlerForCheckout__MANGLED__.onCheckoutViaButtonAfterTimeout",
    CH30: "registerEventHandlerForCheckout__MANGLED__.onCheckoutViaButton",
    CH31: "registerEventHandlerForCheckout__MANGLED__.handleClickCheckoutButton",
    CH32: "Checkout.removeEventsFromElement__MANGLED__",
    CH33: "Checkout.removeDiscountNinjaEventListener__MANGLED__",
    CH34: "Checkout.removeClickEventsForBuyNowButtons__MANGLED__",
    CH35: "registerHandlersForSubmitButtons__MANGLED__",
    CH36: "disableCheckoutFormSubmit__MANGLED__",
    CH37: "registerEventHandlerForCheckoutInner__MANGLED__",
    CH38: "requiresAdvancedCheckout__MANGLED__",
    CH39: "buildPayLoad.buildPayLoadLogEvent__MANGLED__",
    CH40: "buildPayLoadLogEvent__MANGLED__",
    CH41: "buildPayLoad.buildLineItemProperties__MANGLED__",
    CH42: "orderInCart__MANGLED__",
    CH43: "buildPayLoad.buildOrderDetailsJson__MANGLED__",
    CH44: "Checkout.buildPayLoad__MANGLED__",
    CH45: "prepareAdvancedCheckoutInner__MANGLED__",
    CH46: "prepareAdvancedCheckoutDebounced__MANGLED__",
    CH47: "Not processing checkout because required checkbox(es) is/are not checked",
    CH48: "Not processing checkout since Zapiet indicates the checkout is not ready. If Zapiet is no longer in use, remove the widget from the theme.liquid file.",
    CH49: "handleClickBuyNowButton expects to be executed on a product page",
    CH50: 'Could not find checkout button, check if a submit button with the "name" attribute set to "checkout" exists',
    CH51: "Checkout.Shopify.getCheckoutUrlFromServer__MANGLED__.onFailure",
    CH52: "Call to checkout api failed, attempting to redirect to standard url",
    CH53: "Checkout.Shopify.registerEventHandlerForCheckout__MANGLED__.onCheckoutViaButtonInner",
    CH54: "Timed out, redirecting to standard checkout",
    CH55: "Checkout",
    CH56: "Submit should be stopped, Discount Ninja checkout not processed yet. Set flag AllowCancelCheckoutByForm to true",

    // core
    CO01: "Core.handleDiscountNinjaQueryParameters__MANGLED__",
    CO02: "Core.disableApp__MANGLED__",
    CO03: "Core.enableApp__MANGLED__",
    CO04: "Core.interceptNetworkTraffic__MANGLED__",
    CO05: "Core.unRegisterDiscountButtonClickEvents__MANGLED__",
    CO06: "Core.waitForDom__MANGLED__",
    CO07: "Core.startInternal__MANGLED__",
    CO08: "Core.Exception in triggerPromotion__MANGLED__",
    CO09: "PromotionFlow.ExecuteCallback.eval",
    CO10: "PromotionFlow.ExecuteCallback",
    CO11: "Core.showConsoleWelcomeMessage__MANGLED__",
    CO12: "Core.start__MANGLED__",
    CO13: "Core.registerDiscountButtonClickEvents__MANGLED__",
    CO14: "Core.start__MANGLED__.waitForDom__MANGLED__",
    CO15: "Timed out",
    CO16: "Core.loadSettings__MANGLED__",
    CO17: "No settings available",
    CO18: "Failed to check for consent",
    CO19: "Discount Ninja promotions will NOT load, due to lack of consent from the customer.",
    CO20: "Core.attachFontSize__MANGLED__",

    // discount-code
    DC01: "updateFormInputDiscountCode__MANGLED__",
    DC02: "DiscountCode.undoInjectDiscountCodeInUrl__MANGLED__",
    DC03: "DiscountCode.appendDiscountCodeToUrl__MANGLED__",
    DC04: "DiscountCode.injectDiscountCodesInHtmlElements__MANGLED__",
    DC05: "DiscountCode.instructCheckoutToRemoveDiscountCodeInner__MANGLED__",
    DC06: "DiscountCode.instructCheckoutToRemoveDiscountCode__MANGLED__",
    DC07: "DiscountCode.removeCheckoutDiscountCode__MANGLED__",
    DC08: "DiscountCode.undoInjectDiscountCodesInHtml__MANGLED__",
    DC09: "DiscountCode.injectDiscountCodesInHtml__MANGLED__",
    DC10: "DiscountCode.getApplicationCounter__MANGLED__",
    DC11: "DiscountCode.getFromCache__MANGLED__",
    DC12: "DiscountCode.useLocalStorage__MANGLED__",
    DC13: "DiscountCode.saveToCache__MANGLED__",
    DC14: "DiscountCode.removeFromCache__MANGLED__",
    DC15: "DiscountCode.getExternalDiscountCodesFromCache__MANGLED__",
    DC16: "DiscountCode.lockDiscountCodeFieldIfRequired__MANGLED__",
    DC17: "DiscountCode.updateTags__MANGLED__",
    DC18: "DiscountCode.process__MANGLED__",
    DC19: "DiscountCode.isAcceptableCode__MANGLED__",
    DC20: "Offer.LoadPromotionInfo.buildParameters__MANGLED__",
    DC21: "DiscountCode.addDiscountCode__MANGLED__",
    DC22: "DiscountCode.add__MANGLED__",
    DC23: "DiscountCode.remove",
    DC24: "DiscountCode.removeNonAutomaticTokenFromCache__MANGLED__",
    DC25: "DiscountCode.removeTag__MANGLED__",
    DC26: "DiscountCode.removeTag__MANGLED__",
    DC27: "DiscountCode.toggleLink__MANGLED__",
    DC28: "DiscountCode.showFooterInstructions__MANGLED__",
    DC29: "DiscountCode.hideFooterInstructions__MANGLED__",
    DC30: "DiscountCode.getFirstActiveDiscountCodeForCheckout__MANGLED__",
    DC31: "DiscountCode.getDiscountCode__MANGLED__",
    DC32: "DiscountCode.enableDiscountCodeField__MANGLED__",
    DC33: "DiscountCode.removeSavedDiscountCode__MANGLED__",
    DC34: "DiscountCode.getSavedDiscountCode__MANGLED__",
    DC35: "DiscountCode.getDiscountCodeFromQueryParameter__MANGLED__",
    DC36: "DiscountCode.getPromotionForDiscountCode__MANGLED__",
    DC37: "DiscountCode.add__MANGLED__ - No discountcode or priceruleid available",

    // discount-code-trigger
    DCT01: "DiscountCodeTrigger.useLocalStorage__MANGLED__",
    DCT02: "DiscountCodeTrigger.get",
    DCT03: "DiscountCodeTrigger.save__MANGLED__",
    DCT04: "DiscountCodeTrigger.findByDiscountCode__MANGLED__",
    DCT05: "DiscountCodeTrigger.add__MANGLED__",
    DCT06: "DiscountCodeTrigger.remove__MANGLED__",

    // discount-tier
    DTI01: "DiscountTier.discountTierIsApplicable__MANGLED__",
    DTI02: "DiscountTier.getCurrentDiscountTierNumber__MANGLED__",
    DTI03: "DiscountTier.getMaxDiscountTierNumber__MANGLED__",
    DTI04: "DiscountTier.getCurrentDiscountTier__MANGLED__",
    DTI05: "DiscountTier.getGoalDiscountTier__MANGLED__",
    DTI06: "DiscountTier.getGoalDiscountTier__MANGLED__",
    DTI07: "DiscountTier.getPercentageAmountFromDiscountTier__MANGLED__",
    DTI08: "DiscountTier.goalAchieved__MANGLED__",

    // discounted-cart
    DCA01: "DiscountedCart.get__MANGLED__",
    DCA02: "DiscountedCart.getDiscountedAmountForOffer__MANGLED__",
    DCA03: "DiscountedCart.saveDiscountedCartToCache__MANGLED__",
    DCA04: "DiscountedCart.getFreeGiftOfferToken__MANGLED__",
    DCA05: "DiscountedCart.buildProperties__MANGLED__",
    DCA06: "DiscountedCart.hasGiftProperty__MANGLED__",
    DCA07: "DiscountedCart.addDiscountedDiscountedCartItem__MANGLED__",
    DCA08: "DiscountedCart.saveTiersUsed__MANGLED__",
    DCA09: "DiscountedCart.hasPromotionCodesOrShopifyDiscountCodes__MANGLED__",
    DCA10: "DiscountedCart.getPrerequisiteUnits__MANGLED__",
    DCA11: "DiscountedCart.canCombineWithOrderLevelOffers__MANGLED__",
    DCA12: "DiscountedCart.hasProperty__MANGLED__",
    DCA13: "DiscountedCart.isSubscriptionProduct__MANGLED__",
    DCA14: "DiscountedCart.markSubscriptionProducts__MANGLED__",
    DCA15: "DiscountedCart.buildDiscountedCart__MANGLED__",
    DCA16: "DiscountedCart.applyAttributedQuantityToOffers__MANGLED__",
    DCA17: "DiscountedCart.getDiscountedCartFromCache__MANGLED__",
    DCA18: "DiscountedCart.getDiscountedCartItemKeys__MANGLED__",
    DCA19: "DiscountedCart.discountedDiscountedCartItemsByDelegate__MANGLED__",
    DCA20: "DiscountedCart.getDiscountedLinePriceFromItems__MANGLED__",
    DCA21: "DiscountedCart.itemsInDiscountedCartFromCollections__MANGLED__",
    DCA22: "DiscountedCart.itemsInDiscountedCartFromProducts__MANGLED__",
    DCA23: "DiscountedCart.itemsInDiscountedCartFromVariants__MANGLED__",
    DCA24: "DiscountedCart.itemsInDiscountedCart__MANGLED__",
    DCA25: "DiscountedCart.valueInDiscountedCartFromCollections__MANGLED__",
    DCA26: "DiscountedCart.valueInDiscountedCartFromProducts__MANGLED__",
    DCA27: "DiscountedCart.valueInDiscountedCartFromVariants__MANGLED__",
    DCA28: "DiscountedCart.valueInDiscountedCart__MANGLED__",
    DCA29: "DiscountedCart.applicableCartLevelDiscounts__MANGLED__ - Missing LineItems info",
    DCA30: "DiscountedCart.applicableCartLevelDiscounts__MANGLED__",
    DCA31: "DiscountedCart.totalCartLevelDiscountInCents__MANGLED__",
    DCA32: "DiscountedCart.getFirstDiscountCode__MANGLED__",
    DCA33: "DiscountedCart.hasFreeShipping__MANGLED__",
    DCA34: "DiscountedCart.countAppliedPromotions__MANGLED__",
    DCA35: "DiscountedCart.hasAppliedPromotions__MANGLED__",
    DCA36: "DiscountedCart.getAppliedPromotions__MANGLED__",
    DCA37: "DiscountedCart.checkSum__MANGLED__",
    DCA38: "The amount of permutations of order level offers exceeds the maximum limit",
    DCA39: "Building discounted cart - Rehydrating from cache - FAILED",

    // discounted-product
    DP01: "currentVariantIsDiscounted__MANGLED__",
    DP02: "DiscountedProduct.getInfoForTier__MANGLED__",
    DP03: "DiscountedProduct.get__MANGLED__",
    DP04: "DiscountedProduct.getFromProductPage__MANGLED__",

    // dynamic-pricing
    DYP01: "DynamicPricing.getInstallmentHtml",
    DYP02: "DynamicPricing.replaceInstallmentsPrices__MANGLED__.ShopPay",
    DYP03: "DynamicPricing.replaceInstallmentsPrices__MANGLED__.ShopPay.Popup",
    DYP04: "DynamicPricing.replaceInstallmentsPrices__MANGLED__.ShopPay.Popup",
    DYP05: "DynamicPricing.replaceInstallmentsPrices__MANGLED__",
    DYP06: "DynamicPricing.replaceInstallmentsPrices__MANGLED__",
    DYP07: "DynamicPricing.updatePriceInternal__MANGLED__",
    DYP08: "Publish product:discountapplied",
    DYP09: "DynamicPricing.updatePriceForProductWrapper__MANGLED__",
    DYP10: "DynamicPricing.updatePriceForProduct__MANGLED__",
    DYP11: "DynamicPricing.waitForProductCards__MANGLED__",
    DYP12: "DynamicPricing.updatePriceInternal__MANGLED__",
    DYP13: "DynamicPricing.setDynamicPricingVisibility__MANGLED__",
    DYP14: "DynamicPricing.show__MANGLED__",
    DYP15: "DynamicPricing.findElementInCart__MANGLED__",
    DYP16: "DynamicPricing.replacePricesInDrawerCart__MANGLED__",
    DYP17: "DynamicPricing.replacePricesInClassicCart__MANGLED__",
    DYP18: "DynamicPricing.buildProductInfoFromAmount__MANGLED__",
    DYP19: "DynamicPricing.replacePricesInCartForRoot__MANGLED__",
    DYP20: "DynamicPricing.replacePricesForCartItem__MANGLED__",
    DYP21: "DynamicPricing.getOriginalPriceHtmlFromCartItem__MANGLED__",
    DYP22: "DynamicPricing.resetCartItemLevel__MANGLED__",
    DYP23: "replacePriceInCart",
    DYP24: "DynamicPricing.replacePricesForCartItemLevel__MANGLED__",
    DYP25: "DynamicPricing.addCartComment__MANGLED__",
    DYP26: "DynamicPricing.waitForDrawerCart__MANGLED__",
    DYP27: "DynamicPricing.ReplacePricesInCartForRootAfterWait__MANGLED__",
    DYP28: "DynamicPricing.replacePricesInCartInner__MANGLED__",
    DYP29: "DynamicPricing.replacePricesInCart__MANGLED__.Inner",
    DYP30: "Could not find product handle of collection product",
    DYP31: "Unexpected level 'standard'",

    // environment
    EN01: "Environment.myShopifyDomain__MANGLED__",
    EN02: "Environment.returnProperCase__MANGLED__",
    EN03: "Environment.nonPrimaryLocale__MANGLED__",
    EN04: "Environment.urlPath__MANGLED__",
    EN05: "Environment.isSearchPage__MANGLED__",
    EN06: "Environment.isAccountPage__MANGLED__",
    EN07: "Environment.isCartPage__MANGLED__",
    EN08: "Environment.isProductPage__MANGLED__",
    EN09: "Environment.isCollectionPage__MANGLED__",
    EN10: "Environment.isHomePage__MANGLED__",
    EN11: "Environment.isBlogPost__MANGLED__",
    EN12: "Environment.isCatalogPage__MANGLED__",
    EN13: "Environment.blogHandle__MANGLED__",
    EN14: "Environment.blogPostHandle__MANGLED__",
    EN15: "Environment.collectionHandle__MANGLED__",
    EN16: "Environment.VariantData.findInForm__MANGLED__",
    EN17: "Environment.VariantData.FindInQuery__MANGLED__",
    EN18: "Environment.VariantData.fallback__MANGLED__",
    EN19: "Environment.variantData__MANGLED__",
    EN20: "Environment.getLastPartOfPathName__MANGLED__",
    EN21: "Environment.productHandle__MANGLED__",
    EN22: "Environment.customPageHandle__MANGLED__",
    EN23: "GetMyShopifyDomain could not find domain from Shopify.shop or context. Falling back to hostname",
    EN24: "Found conflicting information about selected variant in form",
    EN25: "Environment.Shopify.myShopifyDomain__MANGLED__",
    EN26: "getMyShopifyDomain could not retrieve domain from Shopify.shop property or context.",
    EN27: "Environment.Shopify.variantData__MANGLED__",
    EN28: "No variant information found on product page",
    EN29: "No variant information found on Quick View of collection page",

    // geo-location
    GL01: "GeoLocation.getGeoLocationInfo__MANGLED__",

    // gift-with-purchase
    GWP01: "GiftWithPurchase.showToast__MANGLED__",
    GWP02: "GiftWithPurchase.getLastAdjustmentQuantity__MANGLED__",
    GWP03: "addAdjustments__MANGLED__ - Missing or invalid variant id for gift with purchase",

    // liquid-data
    LD01: "LiquidData.waitForLiquidAsset__MANGLED__",
    LD02: "LiquidData.cart__MANGLED__",
    LD03: "LiquidData.cartValue__MANGLED__",
    LD04: "LiquidData.variantIdsFromCart__MANGLED__",
    LD05: "LiquidData.productCollections__MANGLED__",
    LD06: "LiquidData.productCollectionIds__MANGLED__",
    LD07: "LiquidData.productDefaultVariantId__MANGLED__",
    LD08: "LiquidData.productTags__MANGLED__",
    LD09: "LiquidData.variantInfoFromLiquid__MANGLED__",
    LD10: "LiquidData.compareAtPrice__MANGLED__",
    LD11: "LiquidData.price__MANGLED__",
    LD12: "LiquidData.inventory__MANGLED__",
    LD13: "LiquidData.productAvailable__MANGLED__",
    LD14: "LiquidData.cartItemByProductHandle__MANGLED__",
    LD15: "LiquidData.hasPropertyWithKeyAndValue__MANGLED__",
    LD16: "LiquidData.cartItemByVariantAndProperty__MANGLED__",
    LD17: "LiquidData.removePropertiesByPrefix__MANGLED__",
    LD18: "LiquidData.cartItemPropertiesAsObject__MANGLED__",
    LD19: "LiquidData.cartItemByDelegate__MANGLED__",
    LD20: "LiquidData.lineNumberByKey__MANGLED__",
    LD21: "LiquidData.cartItemsByDelegate__MANGLED__",
    LD22: "LiquidData.priceFromCartByProductHandle__MANGLED__",
    LD23: "LiquidData.priceFromCartByKey__MANGLED__",
    LD24: "LiquidData.quantityFromCartByKey__MANGLED__",
    LD25: "LiquidData.priceFromCart__MANGLED__",
    LD26: "LiquidData.productHandleFromCart__MANGLED__",
    LD27: "LiquidData.subTotalPriceFromCart__MANGLED__",
    LD28: "LiquidData.getQuantityFromItems__MANGLED__",
    LD29: "LiquidData.getOriginalLinePriceFromItems__MANGLED__",
    LD30: "LiquidData.itemsInCartFromCollections__MANGLED__",
    LD31: "LiquidData.itemsInCartFromProducts__MANGLED__",
    LD32: "LiquidData.itemsInCartFromVariants__MANGLED__",
    LD33: "LiquidData.itemsInCart__MANGLED__",
    LD34: "LiquidData.itemIsGift__MANGLED__",
    LD35: "LiquidData.giftItemsInCart__MANGLED__",
    LD36: "LiquidData.numberOfItemsInCart__MANGLED__",
    LD37: "LiquidData.copyPropertiesExceptSplitKeys__MANGLED__",
    LD38: "LiquidData.itemPropertyWithPrefix__MANGLED__",
    LD39: "LiquidData.itemsInCartWithoutProperty__MANGLED__",
    LD40: "hasBoldSubscriptionProducts__MANGLED__",
    LD41: "hasMarkedSubscriptionProductsInDiscountedCart__MANGLED__",
    LD42: "LiquidData.getSellingPlanId__MANGLED__",
    LD43: "LiquidData.waitForLiquidAsset__MANGLED__",
    LD44: "Waiting for liquid asset (limoniapps-discountninja-body/limoniapps-discountninja-context) to be returned timed out. Either the asset does not exist or it is not included in the theme.liquid file. Exiting.",

    // log-session
    LS01: "LogSession.start__MANGLED__",
    LS02: "LogSession.promotionsApplied__MANGLED__",
    LS03: "LogSession.checkoutInitiated__MANGLED__",
    LS04: "LogSession.logPerformance__MANGLED__",
    LS05: "LogSession.logError__MANGLED__",
    LS06: "LogSession.printPerformance__MANGLED__",
    LS07: "LogSession.registerTimeToComplete__MANGLED__",
    LS08: "LogSession.registerTimeToLoad__MANGLED__",

    // offer
    O01: "Offer.setPrimaryOffer__MANGLED__",
    O02: "Offer.CombineTriggersFromTokenAndDiscountCode__MANGLED__",
    O03: "Offer.CombineOffersFromTokenAndDiscountCode__MANGLED__",
    O04: "Offer.getExclusiveTrigger__MANGLED__",
    O05: "Offer.filterOffers__MANGLED__",
    O06: "Offer.handleExclusiveTrigger__MANGLED__",
    O07: "Offer.filterTriggers__MANGLED__",
    O08: "Offer.filterPromotions__MANGLED__",
    O09: "Offer.convertRates__MANGLED__",
    O10: "Offer.LoadPromotionInfo.buildParameters__MANGLED__",
    O11: "Offer.getPromotionInfoFromCache__MANGLED__",
    O12: "Offer.savePromotionInfoToCache__MANGLED__",
    O13: "Offer.processPromotionInfo__MANGLED__",
    O14: "Offer.promotionInfoReady__MANGLED__",
    O15: "Offer.getProductDataFromServerLiquid__MANGLED__",
    O16: "Offer.getApplicablePromotionsFromLiquid__MANGLED__",
    O17: "Offer.getApplicablePromotionsFromCDN__MANGLED__",
    O18: "Offer.getApplicablePromotionsFromServer__MANGLED__",
    O19: "Offer.loadApplicableOffersInner__MANGLED__",
    O20: "Offer.loadPromotionInfo__MANGLED__",
    O21: "Offer.offerCanBeCombinedWithCart__MANGLED__",
    O22: "Offer.PromotionIsApplicable__MANGLED__",
    O23: "Offer.getOfferDataForPrerequisiteProduct__MANGLED__",
    O24: "Offer.getOfferDataForProduct__MANGLED__",
    O25: "Offer.getOfferForProduct__MANGLED__",
    O26: "Offer.getOfferForCartItem__MANGLED__",
    O27: "Offer.offerIsValidForProduct__MANGLED__",
    O28: "Offer.testDiscountedPrice__MANGLED__",
    O29: "Offer.getBestApplicableOfferNumberInternal__MANGLED__",
    O30: "Offer.productIsPrerequisiteFor__MANGLED__",
    O31: "Offer.isApplicableOffer__MANGLED__",
    O32: "Offer.getApplicableOffersForBuildingBlock__MANGLED__",
    O33: "Offer.getFromCache__MANGLED__",
    O34: "Offer.saveToCache__MANGLED__",
    O35: "Offer.removeFromCache__MANGLED__",
    O36: "Offer.stop__MANGLED__",
    O37: "Offer.unApply__MANGLED__",
    O38: "Offer.reEvaluate__MANGLED__",
    O39: "Offer.getTier__MANGLED__",
    O40: "Offer.getTriggerTokenForOffer__MANGLED__",
    O41: "Offer.prerequisiteItemsCartValue__MANGLED__",
    O42: "Offer.eligibleItems__MANGLED__",
    O43: "Offer.eligibleItemsCartValue__MANGLED__",
    O44: "Offer.resetTiersUsed__MANGLED__",
    O45: "Offer.defaultPrerequisite__MANGLED__",
    O46: "Settings invalid",
    O47: "No product handle passed",
    O48: "Offer.offerIsValidForProduct__MANGLED__",
    O49: "TargetType unknown",

    // offers-attribute
    "0A01": "OffersAttribute.removeFromForm__MANGLED__",
    "0A02": "OffersAttribute.addToForm__MANGLED__",
    "0A03": "OffersAttribute.addToCart__MANGLED__",

    // polyfill
    POL01: "Polyfill.customEvent__MANGLED__",
    POL02: "Polyfill.endsWith__MANGLED__",

    // prerequisite
    P01: "Prerequisite.exclusionsMatch__MANGLED__",
    P02: "Prerequisite.matchesEntitlement__MANGLED__",
    P03: "Prerequisite.entitledQuantityInfoInner__MANGLED__",
    P04: "Prerequisite.entitledQuantityInfo__MANGLED__",

    // product-data
    PD01: "ProductData.getCache__MANGLED__",
    PD02: "ProductData.getCacheStoredInSession__MANGLED__",
    PD03: "ProductData.setCache__MANGLED__",
    PD04: "ProductData.setCacheStoredInSessionSet__MANGLED__",
    PD05: "ProductData.productIsAvailable__MANGLED__",
    PD06: "ProductData.normalizeCompareAtPrice__MANGLED__",
    PD07: "ProductData.normalizePrice__MANGLED__",
    PD08: "ProductData.normalizeInventory__MANGLED__",
    PD09: "ProductData.normalizeVariants__MANGLED__",
    PD10: "ProductData.normalizeInventory__MANGLED__",
    PD11: "ProductData.saveProductDataToCache__MANGLED__",
    PD12: "ProductData.getDataFromCollectionAttribute__MANGLED__",
    PD13: "ProductData.saveProductDataUsingDataTags__MANGLED__",
    PD14: "ProductData.save__MANGLED__",
    PD15: "ProductData.save__MANGLED__",
    PD16: "ProductData.getByProductHandle__MANGLED__.isComplete",
    PD17: "ProductData.getProductDataFromServer.buildParameters__MANGLED__",
    PD18: "ProductData.getProductDataFromServer.productDataReady__MANGLED__",
    PD19: "getProductDataFromServer",
    PD20: "ProductData.getProductDataFromServer.productDataReady__MANGLED__",
    PD21: "getProductDataFromCDN",
    PD22: "getProductDataFromServerLiquid",
    PD23: "ProductData.getProductDataFromServerLiquid__MANGLED__",
    PD24: "ProductData.getByProductHandle__MANGLED__",
    PD25: "ProductData.normalizeVariantId__MANGLED__",
    PD26: "ProductData.getVariantData__MANGLED__",
    PD27: "ProductData",
    PD28: "Requesting getProductDataFromServer with empty handle",

    // promotion-event
    PE01: "PromotionEvent.getQueue__MANGLED__",
    PE02: "PromotionEvent.addToQueue__MANGLED__",
    PE03: "PromotionEvent.saveQueue__MANGLED__",
    PE04: "PromotionEventEngine - Facebook integration",
    PE05: "PromotionEventEngine - Google Analytics integration",
    PE06: "PromotionEventEngine - GTM integration",
    PE07: "PromotionEvent.logPixelEvent__MANGLED__",
    PE08: "PromotionEvent.markPromotionEventProcessed__MANGLED__",
    PE09: "PromotionEvent.logPromotionEvents.buildPayload__MANGLED__",
    PE10: "PromotionEvent.logPromotionEvents.buildParameters__MANGLED__",
    PE11: "PromotionEvent.LogPromotionEvent.onLogComplete__MANGLED__",
    PE12: "PromotionEvent.logPromotionEvents__MANGLED__",
    PE13: "PromotionEvent.logPromotionEvent__MANGLED__",
    PE14: "PromotionEvent.processQueue__MANGLED__",
    PE15: "PromotionEvent.logPromotionEvents__MANGLED__.onLogComplete",
    PE16: "Could not log events",

    // promotion-flow
    PF01: "PromotionFlow.isStickyBarRequired__MANGLED__",
    PF02: "PromotionFlow.buildStickyBarAndNotificationArrays__MANGLED__",
    PF03: "PromotionFlow.showStickyBarAndNotification__MANGLED__",
    PF04: "addCss",
    PF05: "PromotionFlow.setVisibilityAcceleratedCheckoutButtons__MANGLED__",
    PF06: "PromotionFlow.switchToCustomCheckoutButton__MANGLED__",
    PF07: "PromotionFlow.displayAppliedOffers__MANGLED__",
    PF08: "PromotionFlow.isNodeWithAttribute__MANGLED__",
    PF09: "PromotionFlow.htmlContainsNode__MANGLED__",
    PF10: "PromotionFlow.listenForDynamicallyAddedElements.mutationObserver__MANGLED__",
    PF11: "PromotionFlow.listenForDynamicallyAddedElements__MANGLED__",
    PF12: "PromotionFlow.handleDrawerCartChange__MANGLED__",
    PF13: "PromotionFlow.listenForLoadMoreButtons__MANGLED__",
    PF14: "PromotionFlow.listenForDrawerCartChanges__MANGLED__",
    PF15: "PromotionFlow.startFlow__MANGLED__",
    PF16: "PromotionFlow.acceptPromotion__MANGLED__",
    PF17: "PromotionFlow.hideOrShow__MANGLED__",
    PF18: "PromotionFlow.showHideElementsWhenPromotionsInCart__MANGLED__",
    PF19: "PromotionFlow.removeUnusedProperties__MANGLED__",
    PF20: "PromotionFlow.addMissingProperties__MANGLED__",
    PF21: "PromotionFlow.saveAdjustedQuantitiesItem__MANGLED__",
    PF22: "PromotionFlow.saveAdjustedQuantities__MANGLED__",
    PF23: "PromotionFlow.unacceptUnapplicableOffers__MANGLED__",
    PF24: "PromotionFlow.sendEventsForAcceptedPromotions__MANGLED__",
    PF25: "PromotionFlow.stopFlow__MANGLED__.afterProcessCartAdjustments",
    PF26: "PromotionFlow.stopFlow__MANGLED__.AfterBuildDiscountedCart",
    PF27: "StopFlowInternal.afterUpdateCart failed",
    PF28: "StopFlowInternal.finishIncompleteUpdateCart failed",
    PF29: "Conflicting app detected by developer MamutaTier. Consider setting the flag KillConflictingApps to true.",
    PF30: "Conflicting app detected by developer Spurit. Consider setting the flag KillConflictingApps to true.",
    PF31: "Conflicting app detected by developer SmartOffer. Consider setting the flag KillConflictingApps to true.",

    // set
    SET01: "Set.forTier__MANGLED__",
    SET02: "Set.sets__MANGLED__",

    // settings
    S01: "Settings.settingsAreValid__MANGLED__",
    S02: "Settings.getFlag__MANGLED__",
    S03: "getPriceFormat__MANGLED__ - Unexpected level",
    S04: "No settings available",

    // short-code
    SC01: "ShortCode.hideShortcodePlaceholders__MANGLED__",
    SC02: "ShortCode.showShortcodePlaceholders__MANGLED__",
    SC03: "ShortCode.refreshResolvedPlaceholders__MANGLED__",
    SC04: "ShortCode.buildResolvedPlaceHolderLink__MANGLED__",
    SC05: "ShortCode.buildResolvedPlaceHolderAmount__MANGLED__",
    SC06: "DiscountedCart.getPrerequisiteUnits__MANGLED__",
    SC07: "ShortCode.replacePromotionLevelPlaceHolders__MANGLED__",
    SC08: "ShortCode.replaceProductLevelPlaceHolders__MANGLED__",
    SC09: "ShortCode.replaceSettingsLevelPlaceHolders__MANGLED__",
    SC10: "ShortCode.replaceCurrencyPlaceHolders__MANGLED__",
    SC11: "ShortCode.replaceLocalePlaceHolders__MANGLED__",

    // tab-reminder
    TR01: "TabReminder.toggleTabReminder__MANGLED__",
    TR02: "TabReminder.set__MANGLED__",
    TR03: "TabReminder.remove__MANGLED__",

    // template-parameter
    TP01: "TemplateParameter.replaceParameter__MANGLED__",
    TP02: "TemplateParameterEngine.getHtml__MANGLED__",

    // token
    TO01: "Token.removeSavedToken__MANGLED__",
    TO02: "Token.getSavedToken__MANGLED__",
    TO03: "Token.getTokenFromQueryParameter__MANGLED__",
    TO04: "Token.getTokenFromPromotionOrQueryParameter__MANGLED__",
    TO05: "Token.getSavedPromotionCode__MANGLED__",
    TO06: "Token.getPromotionCodeFromQueryParameter__MANGLED__",
    TO07: "Token.getNonAutomaticTokenOrPromotionCode__MANGLED__",
    TO08: "Token.getNonAutomaticToken__MANGLED__",
    TO09: "Token.getNonAutomaticPromotionCode__MANGLED__",
    TO10: "Token.getToken__MANGLED__",
    TO11: "Token.getNonAutomaticTokenFromCache__MANGLED__",
    TO12: "Token.useLocalStorageForNonAutomaticToken__MANGLED__",
    TO13: "Token.setNonAutomaticTokenToCache__MANGLED__",
    TO14: "Token.removeNonAutomaticTokenFromCache__MANGLED__",
    TO15: "Token.getNonAutomaticPromotionCodeFromCache__MANGLED__",
    TO16: "Token.setNonAutomaticPromotionCodeToCache__MANGLED__",

    // trigger
    TRI01: "Trigger.clear__MANGLED__",
    TRI02: "Trigger.initializeTimeOnPageCounter__MANGLED__",
    TRI03: "Trigger.trackExitIntent__MANGLED__",
    TRI04: "Trigger.trackScrollPercentage__MANGLED__",
    TRI05: "Trigger.markPageAsVisited__MANGLED__",
    TRI06: "Trigger.PromotionTriggersOnPage__MANGLED__",
    TRI07: "Trigger.PromotionTriggerConditionsMet__MANGLED__",

    // trigger-source
    TRS01: "TriggerSource.getSource__MANGLED__",

    // web-components

    // Announcement bar Slider
    WABS01: "Slider.setActiveItem__MANGLED__",
    WABS02: "Slider.goToSpecificSlideByToken__MANGLED__",

    // announcement-bar utils
    WABU01: "LaDnAnnouncementBar.Utilities.checkCorrectComponentName__MANGLED__",
    WABU02: "LaDnAnnouncementBar.Utilities.hasNonSupportedElement__MANGLED__",
    WABU03: "LaDnAnnouncementBar.Utilities.calcRem__MANGLED__",
    WABU04: "LaDnAnnouncementBar.Utilities.attachFontSize__MANGLED__",
    WABU05: "LaDnAnnouncementBar.Utilities.attachFontFamily__MANGLED__",

    // announcement-bar
    WAB01: "LaDnAnnouncementBar.checkIsSticky__MANGLED__",
    WAB02: "LaDnAnnouncementBar.attachIsReadyApproachInWindow__MANGLED__",
    WAB03: "LaDnAnnouncementBar.attachPosition__MANGLED__",
    WAB04: "LaDnAnnouncementBar.replaceWithPlaceholder__MANGLED__",
    WAB05: "LaDnAnnouncementBar.teleportToNextToHeader__MANGLED__",
    WAB06: "LaDnAnnouncementBar.onRefresh__MANGLED__",
    WAB07: "LaDnAnnouncementBar.putCloseIconHintToTheAppropriateRow__MANGLED__",
    WAB08: "LaDnAnnouncementBar.attachFontSize__MANGLED__",
    WAB09: "LaDnAnnouncementBar.setUpMessagesByItsOwnRow__MANGLED__",
    WAB10: "LaDnAnnouncementBar.filterMessages__MANGLED__",
    WAB11: "LaDnAnnouncementBar.checkVisibility__MANGLED__",
    WAB12: "LaDnAnnouncementBar.adjustComponentsWithMessages__MANGLED__",
    WAB13: "LaDnAnnouncementBar.removeUnnecessaryRows__MANGLED__",
    WAB14: "LaDnAnnouncementBar.runIntersectionObserversForDetectingSticky__MANGLED__",
    WAB15: "LaDnAnnouncementBar.runObserverForAsyncStickyElementsTop__MANGLED__",
    WAB16: "LaDnAnnouncementBar.mutateStickyElementsTop__MANGLED__",
    WAB17: "LaDnAnnouncementBar.hostTransitionEnd__MANGLED__",
    WAB18: "LaDnAnnouncementBar.onClose__MANGLED__",
    WAB19: "LaDnAnnouncementBar.injectStyles__MANGLED__",
    WAB20: "LaDnAnnouncementBar.messageStylesChanges__MANGLED__",
    WAB21: "LaDnAnnouncementBar.onMount__MANGLED__",
    WAB22: "LaDnAnnouncementBar.onMount__MANGLED__.attachPosition__MANGLED__.callback",
    WAB23: "LaDnAnnouncementBar.handleAdminMode__MANGLED__",
    WAB24: "LaDnAnnouncementBar.handleAdminMode__MANGLED__.cb",
    WAB25: "LaDnAnnouncementBar.handleAdminMode__MANGLED__.onChange",
    WAB26: "LaDnAnnouncementBar.generalStyleChanges__MANGLED__",
    WAB27: "LaDnAnnouncementBar.updateAdminModeSlider__MANGLED__",
    WAB28: "LaDnAnnouncementBar.checkHiddenAdminElement__MANGLED__",
    WAB29: "LaDnAnnouncementBar.onUpdate__MANGLED__",
    WAB30: "LaDnAnnouncementBar.imageAssetsChange__MANGLED__",
    WAB31: "LaDnAnnouncementBar.attachPubSub__MANGLED__",
    WAB32: "LaDnAnnouncementBar.optionsStyleChanges__MANGLED__",
    WAB33: "LaDnAnnouncementBar.fontAssetsChange__MANGLED__",
    WAB34: "LaDnAnnouncementBar.forceMobileChange__MANGLED__",
    WAB35: "LaDnAnnouncementBar.attachFontSize__MANGLED__",
    WAB36: "LaDnAnnouncementBar.renderDefaultMessages__MANGLED__",
    WAB37: "LaDnAnnouncementBar.deleteLoaderSkeleton__MANGLED__",
    WAB38: "LaDnAnnouncementBar.getIsHidden__MANGLED__",

    // message
    WABM01: "LaDnAnnouncementBarMessage.handleAdminMode__MANGLED__",
    WABM02: "LaDnAnnouncementBarMessage.imageStyleInject__MANGLED__",
    WABM03: "LaDnAnnouncementBarMessage.iconStyleInject__MANGLED__",
    WABM04: "LaDnAnnouncementBarMessage.attachTextStyles__MANGLED__",
    WABM05: "LaDnAnnouncementBarMessage.toggleEvents__MANGLED__",
    WABM06: "LaDnAnnouncementBarMessage.onTransitionEnd__MANGLED__",
    WABM07: "LaDnAnnouncementBarMessage.onHighlightMessage__MANGLED__",
    WABM08: "LaDnAnnouncementBarMessage.highlightedMessagesSessionStorage__MANGLED__",
    WABM09: "LaDnAnnouncementBarMessage.checkDevice__MANGLED__",
    WABM10: "LaDnAnnouncementBarMessage.offersStyleChanges__MANGLED__",
    WABM11: "LaDnAnnouncementBarMessage.generalStyleChanges__MANGLED__",
    WABM12: "LaDnAnnouncementBarMessage.optionsStyleChanges__MANGLED__",
    WABM13: "LaDnAnnouncementBarMessage.offersOptionsStyleChanges__MANGLED__",
    WABM14: "LaDnAnnouncementBarMessage.toggleBorderAll__MANGLED__",
    WABM15: "LaDnAnnouncementBarMessage.attachBorderStyles__MANGLED__",
    WABM16: "LaDnAnnouncementBarMessage.disconnectedCallback",
    WABM17: "LaDnAnnouncementBarMessage.onMount__MANGLED__",
    WABM18: "LaDnAnnouncementBarMessage.attachPubSub__MANGLED__",
    WABM19: "LaDnAnnouncementBarMessage.attachFontFamily__MANGLED__",
    WABM20: "LaDnAnnouncementBarMessage.fontAssetsChange__MANGLED__",
    WABM21: "LaDnAnnouncementBarMessage.onGoalAchieved__MANGLED__",

    // row
    WABR01: "LaDnAnnouncementBarRow.rowBgStyleChanges__MANGLED__",
    WABR02: "LaDnAnnouncementBarRow.attachBorderStyles__MANGLED__",
    WABR03: "LaDnAnnouncementBarRow.drawCloseIcon__MANGLED__",
    WABR04: "LaDnAnnouncementBarRow.removeUnnecessarySlots__MANGLED__",
    WABR05: "LaDnAnnouncementBarRow.checkBothRowsSlotsCount__MANGLED__",
    WABR06: "LaDnAnnouncementBarRow.optionsStyleChanges__MANGLED__",
    WABR07: "LaDnAnnouncementBarRow.attachBackground__MANGLED__",
    WABR08: "LaDnAnnouncementBarRow.changeCloseIcon__MANGLED__",
    WABR09: "LaDnAnnouncementBarRow.generalStyleChanges__MANGLED__",
    WABR10: "LaDnAnnouncementBarRow.applyBreakpoints__MANGLED__",
    WABR11: "LaDnAnnouncementBarRow.checkDevice__MANGLED__",
    WABR12: "LaDnAnnouncementBarRow.attachPubSub__MANGLED__",
    WABR13: "LaDnAnnouncementBarRow.imageAssetsChange__MANGLED__",

    // slot
    WABST01: "LaDnAnnouncementBarSlot.handleAdminMode__MANGLED__",
    WABST02: "LaDnAnnouncementBarSlot.onHighlightMessage__MANGLED__",
    WABST03: "LaDnAnnouncementBarSlot.onRefresh__MANGLED__",
    WABST04: "LaDnAnnouncementBarSlot.getParentAndItsOwnIndexes__MANGLED__",
    WABST05: "LaDnAnnouncementBarSlot.repaintShadowRoot__MANGLED__",
    WABST06: "LaDnAnnouncementBarSlot.createIconOrImage__MANGLED__",
    WABST07: "LaDnAnnouncementBarSlot.renderMessages__MANGLED__",
    WABST08: "LaDnAnnouncementBarSlot.optionsStyleChanges__MANGLED__",
    WABST09: "LaDnAnnouncementBarSlot.changeChevronsIcon__MANGLED__",
    WABST10: "LaDnAnnouncementBarSlot.primaryBannerSliderChange__MANGLED__",
    WABST11: "LaDnAnnouncementBarSlot.checkDevice__MANGLED__",
    WABST12: "LaDnAnnouncementBarSlot.generalStyleChanges__MANGLED__",
    WABST13: "LaDnAnnouncementBarSlot.connectedCallback",
    WABST14: "LaDnAnnouncementBarSlot.disconnectedCallback",
    WABST15: "LaDnAnnouncementBarSlot.attachPubSub__MANGLED__",
    WABST16: "LaDnAnnouncementBarSlot.onUpdate__MANGLED__",
    WABST17: "LaDnAnnouncementBarSlot.imageAssetsChange__MANGLED__",
    WABST18: "LaDnAnnouncementBarSlot.createMessage__MANGLED__",
    WABST19: "LaDnAnnouncementBarSlot.offersStyleChanges__MANGLED__",
    WABST20: "LaDnAnnouncementBarSlot.onHighlight__MANGLED__",
    WABST21: "LaDnAnnouncementBarSlot.onGoalAchieved__MANGLED__",

    // Admin Rect
    WAPR01: "AdminPointerRect.cloneActiveNode__MANGLED__",
    WAPR02: "AdminPointerRect.setActiveCoords__MANGLED__",

    // Admin Engine
    WAE01: "AdminEngine.registerEvents__MANGLED__",
    WAE02: "AdminEngine.reCalculateNodes__MANGLED__",
    WAE03: "AdminEngine.onActivateRect__MANGLED__",
    WAE04: "AdminEngine.changeActiveCoords__MANGLED__",
    WAE05: "AdminEngine.onDestroy",
    WAE06: "AdminEngine.addCollection",
    WAE07: "AdminEngine.findLocation__MANGLED__",
    WAE08: "AdminEngine.updateCollectionsDisabled",
    WAE09: "AdminEngine.changePointerColor",
    WAE10: "AdminEngine.updateCollectionsDisabledByIndex",
    WAE11: "AdminEngine.setActiveCoordsAttr__MANGLED__",
    WAE12: "LaDnAdminPointerRect.registerElements__MANGLED__",
    WAE13: "LaDnAdminPointerRect.getLabelHtml__MANGLED__",
    WAE14: "LaDnAdminPointerRect.attachCollectionRects__MANGLED__",
    WAE15: "LaDnAdminPointerRect.attachListeners__MANGLED__",
    WAE16: "LaDnAdminPointerRect.attachSelectListeners__MANGLED__",
    WAE17: "LaDnAdminPointerRect.onMouseEnter__MANGLED__",
    WAE18: "LaDnAdminPointerRect.onMouseLeave__MANGLED__",
    WAE19: "LaDnAdminPointerRect.onLabelClick__MANGLED__",
    WAE20: "LaDnAdminPointerRect.onChangeState__MANGLED__",
    WAE21: "LaDnAdminPointerRect.onStateToggle__MANGLED__",
    WAE22: "LaDnAdminPointerRect.makeParentRelative__MANGLED__",
    WAE23: "LaDnAdminPointerRectStateSelect.click",
    WAE24: "LaDnAdminPointerRectStateSelect.attachListeners__MANGLED__",
    WAE25: "LaDnAdminPointerRectStateSelect.onDocumentClick__MANGLED__",
    WAE26: "LaDnAdminPointerRectStateSelect.onDocumentKeyDown__MANGLED__",
    WAE27: "LaDnAdminPointerRectStateSelect.onFocus__MANGLED__",
    WAE28: "LaDnAdminPointerRectStateSelect.onSelect__MANGLED__",
    WAE29: "LaDnAdminPointerRectStateSelect.onClick__MANGLED__",
    WAE30: "LaDnAdminPointerRectStateSelect.toggleOpen__MANGLED__",
    WAE31: "LaDnAdminPointerRectStateSelect.getOptionElements__MANGLED__",
    WAE32: "LaDnAdminPointerRectStateSelect.getOptions__MANGLED__",
    WAE33: "LaDnAdminPointerRectStateSelect.dispatchOptionChange__MANGLED__",
    WAE34: "LaDnAdminPointerRectStateSelect.attachValue__MANGLED__",
    WAE35: "LaDnAdminPointerRectStateSelectOption.attachListeners__MANGLED__",
    WAE36: "LaDnAdminPointerRectStateSelectOption.onChange__MANGLED__",
    WAE37: "LaDnAdminPointerRectStateSelectOption.onFocus__MANGLED__",
    WAE38: "LaDnAdminPointerRectStateSelectOption.onClick__MANGLED__",
    WAE39: "LaDnAdminPointerRectStateSelectOption.attachValue__MANGLED__",
    WAE40: "LaDnAdminPointerRectStateSelect.attachOpenFromAttribute__MANGLED__",
    WAE41: "AdminEngine.transformCollectionRect__MANGLED__",
    WAE42: "AdminEngine.onLabelClick__MANGLED__",

    // Pub Sub
    WPS01: "PubSub.getRootPubSub__MANGLED__",
    WPS02: "PubSubProvider.subscribe__MANGLED__",
    WPS03: "PubSubProvider.unsubscribe__MANGLED__",
    WPS04: "PubSubProvider.publish__MANGLED__",

    // Utilities
    WU01: "Utilities.objectEquals",
    WU02: "Utilities.arrayEquals",
    WU03: "Utilities.attachTextStyles",
    WU04: "Utilities.attachFontFamily",
    WU05: "Utilities.getIconFromContext",
    WU06: "Utilities.getIsReducedMotion",
    WU07: "Utilities.attachBorderStyles",
    WU08: "Utilities.attachFontSize",
    WU09: "Utilities.attachStylesUrl",
    WU10: "Utilities.hexToRgb",
    WU11: "Utilities.injectCustomCss",

```


# Widgets

Information about the web components used to communicate Discount Ninja promotions.

## Guiding principles

To ensure our widgets are developed using best practices and meet your requirements, the following guiding principles were observed:

* [Accessibility](/discount-ninja-developer-hub/storefront-api/widgets/guiding-principles/accessibility)
* [Localization](/discount-ninja-developer-hub/storefront-api/widgets/guiding-principles/localization)
* [Integration](/discount-ninja-developer-hub/storefront-api/widgets/guiding-principles/integration)
* [Style](/discount-ninja-developer-hub/storefront-api/widgets/guiding-principles/style)

## Catalog

### Site-wide

The following widgets are available on all page types:

* [Announcement Bar](/discount-ninja-developer-hub/storefront-api/widgets/announcement-bar)
* [Notification](/discount-ninja-developer-hub/storefront-api/widgets/notification)
* [Offer Rules Popup](/discount-ninja-developer-hub/storefront-api/widgets/offer-rules-popup)

### Product page (PDP)

* [Product Banner](/discount-ninja-developer-hub/storefront-api/widgets/product-banner)

### Collections (PLP)

{% hint style="info" %}
Collections can be found on collection pages, the catalog, the home page, search result and on product pages (related products...)
{% endhint %}

* [Promotional Badge](/discount-ninja-developer-hub/storefront-api/widgets/promotional-badge)

### Cart

* [Promotion Code Field](/discount-ninja-developer-hub/storefront-api/widgets/promotion-code-field)
* [Promotion Summary](/discount-ninja-developer-hub/storefront-api/widgets/promotion-summary)


# Guiding principles


# Accessibility

## Coding standards <a href="#coding-standards" id="coding-standards"></a>

Discount Ninja widgets start with web standards for HTML, CSS, and JavaScript. Features from the Accessible Rich Internet Applications (WAI-ARIA or ARIA) specification are used to build functionality that is not available in native HTML.

## Alternative text <a href="#alternative-text" id="alternative-text"></a>

To help people who rely on assistive technologies, such as a screen reader or other text-to-speech programs, our components use [alternative text](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/alt) for icons and images used to convey information and actions (like buttons and links).

## Meeting the Web Content Accessibility Guidelines (WCAG) <a href="#meeting-the-web-content-accessibility-guidelines-wcag" id="meeting-the-web-content-accessibility-guidelines-wcag"></a>

Discount Ninja targets WCAG 2.1 Level A and Level AA success criteria, and seeks to provide a highly usable experience for everyone.

For more information, see the following resources:

* [WCAG 2.1](https://www.w3.org/TR/WCAG21/)
* [ARIA 1.1](https://www.w3.org/TR/wai-aria-1.1/)

## Assistive technology support <a href="#assistive-technology-support" id="assistive-technology-support"></a>

Discount Ninja web components are tested for accessibility with automated and manual techniques. Users should expect to be able to access features built with our components using modern assistive technologies. These include native and third-party tools like:

* Screen readers
* Speech recognition programs
* Supports for low vision and color blindness
* Alternative keyboards
* Switch devices
* Tools for readability

## Animations

Discount Ninja web components detect if a user has enabled a setting on their device to [minimize the amount of non-essential motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion#user_preferences). If this setting is enabled, all animations included in our web components are automatically disabled on that device.

## Keyboard

Keyboard accessibility is an important aspect of web accessibility. Many users with motor disabilities rely on a keyboard. Users without disabilities may use a keyboard for navigation because of preference or efficiency.

Discount Ninja web components implement keyboard accessibility as outlined [here](https://webaim.org/techniques/keyboard/). Essentially, the widgets allow users to use the `Tab` key to navigate through interactive elements and use `Enter` or `Spacebar` to activate links and buttons. Support for `Left`, `Right`, `Up,` and `Down` arrows, as well as the `Home` and `End` keys are available in controls that show lists. The `Escape` key allows users to close widgets if applicable.

## Visually impaired

Discount Ninja web components use accessibility semantics defined by the Accessible Rich Internet Application ([ARIA](https://www.w3.org/TR/wai-aria-1.1/)) specification to help you create accessible web experiences for visually impaired users. Here are some of the patterns that are implemented by the components:

* Buttons: <https://www.w3.org/WAI/ARIA/apg/patterns/button/>
* Links: <https://www.w3.org/WAI/ARIA/apg/patterns/link/>
* Switch: <https://www.w3.org/WAI/ARIA/apg/patterns/switch/>
* Progress bar: <https://www.w3.org/WAI/ARIA/apg/patterns/meter/>
* Tooltip: <https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/>
* Checkbox: <https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/>
* Carousel: <https://www.w3.org/WAI/ARIA/apg/patterns/carousel/>
* Popup: <https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/>


# Localization

Web components are aware of the selected locale (e.g.: en-us, fr-fr, ...) and selected currency (e.g. USD, EUR...) that is selected in the storefront.&#x20;

The following aspects of localization are handled:

## Text direction

The CSS of the web components work for both left-to-right and right-to-left locales (e.g., European languages are left-to-right, Arabic right-to-left).

## Language

The app assumes that the selected locale is exposed using a global variable declared on the window object named `window.Shopify.locale`.

Text labels for supported languages can be overridden in the app. Support for other languages can also be added in the app.

{% hint style="info" %}
Multi-language capabilities are not available on all plans.
{% endhint %}

## Currency

Prices and amounts are formatted using the currency formatting that is configured in the app.

The app assumes that the selected currency is exposed using a global variable declared on the window object named `window.Shopify.currency`.


# Integration

## Events

Communication with the web components should be done via events. This ensures loose coupling and avoids issues if the implementation of the web component changes.

Examples:

* DO NOT
  * Call the method `Next` on a slot of an Announcement Bar
* DO
  * Publish the event `la:dn:announcement-bar:next`


# Style

## Web components

The widgets are implemented as [custom web components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements).

## Style encapsulation

The widgets use the shadow DOM to achieve encapsulation from JavaScript and CSS. This ensures the widgets can be styled reliably and page styles cannot affect the style of the widget. It also means that the CSS present in the widgets cannot affect the page style.

The implication of this is that you cannot change or override the style of the widgets using CSS that is added to the page. Instead you have to edit the style using the app.

## Editing style

To change the style of the widget:

* open Discount Ninja
* in the menu, find *Settings*, then *Style*
* edit the relevant preset(s)

## Presets

A preset bundles style settings for a set of widgets. The same preset can be configured to apply to all promotions or only to selected promotions.&#x20;

Discount Ninja includes a rich editor experience that allows you to edit style settings (font, border, background color, spacing, size...) for all of the widgets included in the app.

## Advanced settings

If you want to change advanced CSS settings, you can add custom CSS to the style preset. Each widget has a separate section where custom CSS can be added.


# Announcement Bar

The announcement bar helps merchants to draw attention to promotions, communicate important announcements to customers and tells the customer what they need to do to unlock rewards.

{% hint style="success" %}
This widget is currently available for V6 Beta customers only
{% endhint %}

## Registration

This widget is included in the "**Discount Ninja**" App Embed.

## Style

Check out [this article](#style) to learn more about how the style of this widget can be changed.

## Events subscribed

`la:dn:announcement-bar:start`

* Executes the `start` method on the slot that supports cycling.

`la:dn:announcement-bar:stop`

* Executes the `stop` method on the slot that supports cycling.

`la:dn:announcement-bar:previous`

* Executes the `previous` method  on the slot that supports cycling.

`la:dn:announcement-bar:next`

* Executes the `next` method on the slot that supports cycling.

`la:dn:announcement-bar:show-message`

* Executes the `showMessage` method on the slot that supports cycling.
* Expects a parameter named `token` to indicate which message to show.

`la:dn:announcement-bar:highlight-message`

* Executes the `highlightMessage` method on the slot that supports cycling.
* Expects a parameter named `token` to indicate which message to highlight.

## Events published

`la:dn:announcement-bar:ready`

* Indicates the component is ready.
* At this point the component is subscribed to all events it listens to.


# Notification

The notification confirms that an offer has been applied to the cart and tells the customer what they need to do to unlock rewards.

{% hint style="success" %}
This widget is currently available for V6 Beta customers only
{% endhint %}

## Registration

This widget is included in the "**Discount Ninja**" App Embed.

## Style

Check out [this article](#style) to learn more about how the style of this widget can be changed.

## Events

`la:dn:notification:collapse`

* Executes the collapse method.

`la:dn:notification:expand`

* Executes the expand method.

`la:dn:notification:highlight-message`

* Executes the `highlightMessage` method.
* Expects a parameter named `token` to indicate which message to highlight.


# Offer Rules Popup

The offer rules popup widget displays a popup to users with details related to the offer. It optionally includes a CTA.

{% hint style="success" %}
This widget is currently available for V6 Beta customers only
{% endhint %}

## Registration

This widget is included in the "**Discount Ninja**" App Embed.

## Style

Check out [this article](#style) to learn more about how the style of this widget can be changed.

## Events

`la:dn:offer-rules-popup:open`

* Executes the `open` method.

`la:dn:offer-rules-popup:close`

* Executes the `close` method.


# Product Banner

The product banner is displayed on the product page and adds an optional message used to highlight the offer, explain its rules and increase the chance the user adds the product to the cart.

{% hint style="success" %}
This widget is currently available for V6 Beta customers only
{% endhint %}

## Registration

#### App Embed

This widget is included in the "**Discount Ninja**" App Embed.

#### Placeholder

A placeholder for the banner must be added to the template(s) of the product page(s) where the banner should be displayed. This can be achieved in one of the following ways:

* `OS 2.0` Using Shopify's theme editor, add the "**Discount Ninja Product Banner**" App Block to the section on the product page template(s) where you want banners to be displayed
* If your theme does not support App Blocks on the product page template, add the following snippet to your product page instead:

```html
<la-dn-product-banner-placeholder product-handle="{{ product.handle }}">
</la-dn-product-banner-placeholder>
```

{% hint style="info" %}
Note: the variable "`product`" in "`product.handle`" may be different in your theme.
{% endhint %}

## Style

Check out [this article](#style) to learn more about how the style of this widget can be changed.


# Promotion Summary

The promotion summary provides a subtotal and a breakdown of the discounts.

{% hint style="success" %}
This widget is currently available for V6 Beta customers only
{% endhint %}

## Registration

#### App Embed

This widget is included in the "**Discount Ninja**" App Embed.

#### Placeholder

A placeholder for the promotion summary must be added to the template(s) of the cart page(s) where the summary should be displayed. This can be achieved in one of the following ways:

* `OS 2.0` Using Shopify's theme editor, add the "**Discount Ninja Promotion Summary**" App Block to the section on the cart page template(s) where you want the summary to be displayed
* If your theme does not support App Blocks on the cart page template, add the following snippet to your cart page instead:

```html
<la-dn-promotion-summary-placeholder>
</la-dn-promotion-summary-placeholder>
```

## Style

Check out [this article](#style) to learn more about how the style of this widget can be changed.


# Promotion Code Field

The promotion code field allows users to redeem Discount Ninja promotion codes and Shopify discount codes from the cart.

{% hint style="success" %}
This widget is currently available for V6 Beta customers only
{% endhint %}

## Registration

#### App Embed

This widget is included in the "**Discount Ninja**" App Embed.

#### Placeholder

A placeholder for the promotion code field must be added to the template(s) of the cart page(s) where the promotion code field should be displayed. This can be achieved in one of the following ways:

* `OS 2.0` Using Shopify's theme editor, add the "**Discount Ninja Promotion Code Field**" App Block to the section on the cart page template(s) where you want the promotion code field to be displayed
* If your theme does not support App Blocks on the cart page template, add the following snippet to your cart page instead:

```html
<la-dn-promotion-code-field-placeholder>
</la-dn-promotion-code-field-placeholder>
```

## Style

Check out [this article](#style) to learn more about how the style of this widget can be changed.

## Events

`la:dn:promotion-code-field:collapse`

* Executes the `collapse` method.

`la:dn:promotion-code-field:expand`

* Executes the `expand` method.

`la:dn:promotion-code-field:toggle`

* Executes the `toggle` method.


# Promotional Badge

The promotional badge is displayed on the image of a product card (typically in a collection or on a product page) to indicate which offer applies to the product.

{% hint style="success" %}
This widget is currently available for V6 Beta customers only
{% endhint %}

## Registration

#### App Embed

This widget is included in the "**Discount Ninja**" App Embed.

#### Placeholder

A placeholder for the badge must be added to the template of the collections where the badge should be displayed. The placeholder is typically added to the image part of a product card.&#x20;

The following example shows where the placeholder would be placed given a product card with id `example-product-card` and an image part with id `example-product-card-image`:

```html
<div id="example-product-card">
    <div id="example-product-card-image">
        <la-dn-product-banner-placeholder 
            product-handle="{{ product.handle }}">
        </la-dn-product-banner-placeholder>
    </div>
</div>
```

{% hint style="info" %}
Note: the variable "`product`" in "`product.handle`" may be different in your theme.
{% endhint %}

## Style

Check out [this article](#style) to learn more about how the style of this widget can be changed.


# Integration

Discount Ninja includes out-of-the-box integration with a variety of apps and Shopify features.

## Subscription apps

* The app automatically detects subscription products and can handle a checkout where one-time purchases and subscription products are combined.
* The app was tested with:
  * Shopify's subscription app
  * [Seal](https://www.sealsubscriptions.com/)
  * [Appstle](https://appstle.com/)
  * [Recharge](https://rechargepayments.com/)
  * [Yotpo](https://www.yotpo.com/)
  * [Stay.ai](https://stay.ai/)
* Some subscription apps provide a table or box on product pages (PDP) that presents the one-time-purchase and subscription prices. If you need to display discounted prices for the one-time-purchase and subscription prices, you'll need to make some code edits. Learn more about this use case [here](/discount-ninja-developer-hub/theme-edits/code-edits/product-detail-page-pdp/price#subscription-apps).

## Email marketing apps

### Trigger a promotion from an email

You can trigger a promotion using a discount link. The discount link is essentially a link to a landing page followed by the token of the promotion. The app includes a handy tool to build those links. A discount link automatically triggers the promotion for the customer and navigates to the landing page you have selected.

Most email marketing tools allow you to add a link to a word or a button through their user interface. If you need to manually add them using HTML, here are some examples that can get you started:

* include a simple link

```html
Activate your discount now, by clicking <a href="https://YOUR-SHOP-URL-HERE/?token=ABCD">here</a>.
```

* include a button

```html
<div style="display:flex;align-items:center;justify-content:center;text-align:center;margin:20px;max-width:200px;height:50px;background:#000;border:2px solid #fff;border-radius:6px;">
    <a href="https://YOUR-SHOP-URL-HERE/?token=ABCD" style="color:#fff;text-decoration:none;">
        SHOP&nbsp;THE&nbsp;DEAL
    </a>
</div>
```

{% hint style="info" %}

### Note that <https://YOUR-SHOP-URL-HERE/?token=ABCD> must be replaced by the discount link of the promotion you want to trigger. For example: <https://www.mystore.com/?token=EY1UM>

{% endhint %}

### Discount code wrapper

Discount Ninja can also detect discount codes generated by email marketing apps, more specifically:

* Klaviyo: Email marketing

## Loyalty apps

* Discount codes generated by loyalty apps can be redeemed using Discount Ninja's promotion code field.
* The app includes an offer type to build "wrapper" promotions to display Discount Ninja widgets when a loyalty discount code is entered in the promotion code field.
* Discount Ninja can detect discount codes generated by the following apps:
  * Appstle Loyalty & Rewards
  * Influenci.io: Loyalty Rewards
  * Loyalty & referrals by Yotpo (Swell)
  * Loyalty, rewards and referrals by LoyaltyLion
  * Loyalty, Wishlist, Reviews UGC by Growave
  * Referral Candy
  * Smile: Rewards & Loyalty
  * Stamped.io Loyalty & Rewards

## Pickup + delivery apps

* Discount Ninja has built in integration with Zapiet.

  The integration relies on a method named `Zapiet.Widget.checkoutEnabled()` which must evaluate to `true` to allow the app to continue the checkout pipeline.

## Prefill checkout information

* Discount Ninja requires that third party developers add query parameters to the action attribute of the cart form, which is a standard technique to pass information from the cart to the checkout.
* To prefill shipping information at checkout based on a selection made in the checkout, third party developers must ensure the `action` attribute of the cart form included the correct query parameters. This can be done using JavaScript.&#x20;
* Shopify provides a number of query parameters to prefill the checkout: <https://shopify.dev/docs/apps/build/checkout/cart-permalinks/create-cart-permalinks>
* Example of an action attribute, rendered by Liquid and updated by JavaScript:

```html
<form action="/cart?checkout[shipping_address][city]=London" method="POST">
    ...
</form>
```

## Currency conversion switchers

* The app detects the currency based on the `Shopify.currency` object
* We are compatible with all apps that rely on this object

## Installment apps

* The app can apply discounts to installment payments proposed by:
  * AfterPay
  * QuadPay
  * Sezzle
  * Hoolah
  * ShopPay

## Search and filter apps

* Rendering discounted prices in search results and collections managed by apps requires the following:
  * Discount Ninja must be able to identify where prices are rendered. This can be achieved in one of two ways:
    * Using a CSS selector
    * By marking the section with an attribute
  * For each collection product the app must, at a minimum, have access to the product handle
* The app has been tested with the following apps:
  * [Boost AI Search & Filter](https://apps.shopify.com/product-filter-search) (formerly known as PFS)
  * [Searchanise Search & Filter](https://apps.shopify.com/searchanise)
  * [Globo Smart Search & Product Filter](https://apps.shopify.com/product-filter-and-search) (SPF)
  * [AI Search & Product Filter](https://apps.shopify.com/ultimate-search-and-filter-1) (Ultimate Search)
* For information on how to configure these apps to show strikethrough pricing, please refer to [this page](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/price).

## Page builders

* The app has different levels of support for different page builders. More information about each integration is available from the following support articles:
  * [PageFly](https://support.discountninja.io/en/articles/5056758-enable-discount-ninja-on-a-pagefly-page)
  * [Zipify Pages](https://support.discountninja.io/en/articles/3722543-zipify-pages-and-discount-ninja)
  * GemPages
  * [Shogun](https://support.discountninja.io/en/articles/5173770-enable-discount-ninja-on-shogun-pages)

## Terms and conditions checkboxes

#### Behavior

The app automatically checks for known checkboxes that are related to terms and conditions. The app will not proceed the checkout pipeline until those checkboxes (if marked mandatory and visible) are checked.&#x20;

{% hint style="warning" %}
Important: note that the logic to cancel the checkout based on the status of the checkbox should be present in the code of your theme (or the app that handles this checkbox). Discount Ninja does not display an error message, it relies on the theme to cancel the checkout and display a message.
{% endhint %}

#### Explicitly marking a checkbox

If you use a custom checkbox that isn't automatically detected by the app, you may need to mark it explicitly. To explicitly mark a checkbox as required before checkout, add the following attribute to the input field:

```html
la-dn-checkout-required-checkbox
```

#### Syntax <a href="#h_46648c7982" id="h_46648c7982"></a>

```javascript
<input 
    type="checkbox" 
    name="termsAndConditions" 
    form="cart" 
    la-dn-checkout-required-checkbox>
<label for="termsAndConditions">I accept the terms and conditions</label>
```

## Markets

* The app can be used on stores that have multiple markets configured.
* Shopify functions mode supports most features included in Shopify Markets:
  * &#x20;[International pricing](https://help.shopify.com/en/manual/markets/pricing/product-prices-by-country): fixed product prices configured in Shopify Markets are used.
  * &#x20;[Exchange rates](https://help.shopify.com/en/manual/markets/pricing/exchange-rates): exchange rates will be applied to product prices as configured in Shopify Markets.
  * &#x20;[Rounding](https://help.shopify.com/en/manual/markets/pricing/rounding): rounding will be applied to product prices as configured in Shopify Markets.
* Caveats
  * Thresholds and fixed amounts: thresholds and fixed amount discounts are calculated based on the conversion rate defined in Shopify Markets.
  * &#x20;[Rounding](https://help.shopify.com/en/manual/markets/pricing/rounding): discounted prices are only rounded at checkout, not in the cart.

## Other discount apps

* Discount Ninja strives to be as compatible as technically possible with other discount apps.
* Note that the app is not compatible with:
  * Shopify's automatic discounts
  * Apps that use a discounted checkout prepared by the Draft Order API
  * Apps that require more than two discount functions
  * Apps that add/remove products at checkout using Checkout UI extensions

## Custom checkout logic

To integrate with custom checkout logic, you can :

* add a checkout pipeline rule (as documented [here](/discount-ninja-developer-hub/storefront-api/promotion-engine/javascript-api/functions#add-pipeline-rule)); this is typically a good solution if you wish to execute a validation rule before proceeding to the checkout
* use an event to trigger the checkout (as documented [here](/discount-ninja-developer-hub/storefront-api/promotion-engine/javascript-api/events#trigger-checkout)); this is typically a good solution if you wish to take control of the checkout is some situations and leave Discount Ninja in control for other situations

Note: if your code redirects to the checkout by setting `window.location` you may want to leverage the above mentioned event as documented in the code snippet [here](/discount-ninja-developer-hub/storefront-api/promotion-engine/javascript-api/events#redirect-to-checkout).

## Show/hide custom HTML <a href="#h_46648c7982" id="h_46648c7982"></a>

Discount Ninja includes building blocks that can display specific HTML content to help you promote offers.

If you need to show or hide custom HTML blocks, you can use the following classes:

* `limoniapps-discountninja-whenproductdiscounted-show` and `limoniapps-discountninja-whenproductdiscounted-hide`: these classes can be used on product pages only; they hide or show the content of the element based on whether the selected variant is discounted by Discount Ninja or not
* `limoniapps-discountninja-whenactivepromotions-show` and `limoniapps-discountninja-whenactivepromotions-hide`: hide or show the content of the element based on whether Discount Ninja active promotions are available for the visitor or not
* `limoniapps-discountninja-whenpromotionsincart-show` and `limoniapps-discountninja-whenpromotionsincart-hide`: hide or show the content of the element based on whether Discount Ninja promotions have been applied to the cart or not
* `limoniapps-discountninja-whencartdiscounted-show` and `limoniapps-discountninja-whencartdscounted-hide`: hide or show the content of the element based on whether the total amount of the cart is discounted by Discount Ninja promotions or not

## Google Tag Manager Data Layer

To integrate with Google's data layer you may need to leverage the discounted price calculated by the app. The app publishes this information using [an event](/discount-ninja-developer-hub/storefront-api/promotion-engine/javascript-api/events#product-discount-calculated). The following code provides an example of how you may use the [data returned by that event](/discount-ninja-developer-hub/storefront-api/promotion-engine/javascript-api/objects#product-variant-price-info):

```javascript
// Subscribe to the event
document.addEventListener("la:dn:product:discount:calculated", function(event) {
    // Retrieve the data about the product after discounts have been calculated
    const productVariantPriceInfo = event.detail.data[0];
    const discountedPrice = productVariantPriceInfo.discountedPrice;  
    console.log('Discounted price is ' + discountedPrice, productVariantPriceInfo);
    
    // Your code to integrate with the data layer goes here
    // ...
});
```

## Drawer cart split line items

When your cart contains multiple items of the same product variant, Shopify will split those items into multiple rows in the cart (and drawer cart) when:

* **Line item property**: One of the items has a different line item property (for example: 1 item has Color 'blue', the other item has a different value for 'Color' or does not specify a value for this property)
* **Selling plan**: One of the items is sold using a selling plan (subscription) and the other is not
* **Discount**: One of the items has a server-side discount associated with and the other does not

Discount Ninja applies hidden line item properties to BOGO and GWP products. This causes Shopify to split those lines.

To be able to change the quantity for each of those split lines individually, it is important that your theme updates quantity based on the key of the line item. The key of a line item is unique (also for split lines). If you update quantities based on the variant then Shopify will update the quantity of all lines that contain that variant, which leads to unexpected results.

See Shopify's documentation for more details:

{% embed url="<https://shopify.dev/docs/api/ajax/reference/cart#post-locale-cart-change-js>" %}
Cart.js API /cart/change.js
{% endembed %}

## Drawer cart

Discount Ninja applies discounts using Shopify Functions. Most (if not all) themes have built-in support for visualizing the discounts applied in this way. However, they require that the page is reloaded to render those discounts. This causes a broken user experience for the customer when the cart or drawer cart is refreshed using JavaScript instead of reloading the page.

To ensure the drawer cart immediately reflects the discounts, you'll need to:

* Enable strikethrough pricing as documented [here](/discount-ninja-developer-hub/theme-edits/code-edits/cart)
* If you wish to re-render the drawer cart after Discount Ninja has applied the discount to the cart, subscribe to this [event](/discount-ninja-developer-hub/storefront-api/promotion-engine/javascript-api/events#cart-discounted) to be notified when the discount is applied.

Also check the following:

* Ensure you hide the native discount labels rendered by your theme using the `limoniapps-discountninja-whenpromotionsincart-hide` css class, as outlined [here](#h_46648c7982-1)
* Ensure your theme [updates the quantity of line items based on the line item key](#drawer-cart-quantity-spinners) (as opposed to the variant)
* If your theme re-renders the drawer cart after it is opened, you need to instruct Discount Ninja to re-apply strikethrough pricing and re-render cart text and other widgets that may have been overwritten. Please refer to this [event](/discount-ninja-developer-hub/storefront-api/promotion-engine/javascript-api/events#drawer-cart-opened) to implement this.

For the best customer experience, you'll also want to avoid that DN reloads the page when a gift is added/removed or a line is split (BOGO). To avoid reloading the page, configure DN to execute a line of JavaScript instead. This line should refresh the content of the drawer cart by fetching the content of the cart from Shopify and redrawing the drawer cart. This line is configured in the app under the Settings menu > General > Advanced > Refresh behavior

<figure><img src="/files/AbV9CaaLI20O13ZwFYNi" alt=""><figcaption><p>Refresh behavior JavaScript method</p></figcaption></figure>

## Progress bar

If your theme includes a progress bar that indicates how close a visitor is to unlocking a goal (e.g. free shipping), you may find that the progress bar isn't using the discounted total of the cart.

Discount Ninja applies discounts using Shopify Functions which ensures Shopify understands what the discounted total is. However, your theme's progress bar is not aware that discounts are applied immediately. This results in the progress bar showing incorrect data until the visitor reloads the page.

To avoid this, you'll need to programmatically update the progress bar whenever discounts are applied. This can easily be achieved with the [Cart updated](/discount-ninja-developer-hub/storefront-api/promotion-engine/javascript-api/events#cart-updated) event.

Example:

<pre class="language-javascript"><code class="lang-javascript">document.addEventListener("la:dn:cart:updated", function(event) {
    const discountedCart = event.detail.data[0]; 
        
    //The code below simply adds a line in the console log    
    console.log('Ready to update the progress bar based on the discounted total', discountedCart.total.discountedPrice); 

    //Run a sample implementation for a shipping bar
    updateFreeShippingBar(discountedCart.total.discountedPrice);
});

function updateFreeShippingBar(discountedTotal) {
    //In this example we assume that the threshold is $50
    //We multiply by 100 to express the threshold in cents as opposed to dollars.
    //This allows us to compare the threshold to the discounted total.
    //We then multiply by the currency conversion rate provided by Shopify,
    //this allows us to handle multiple currencies.
    const conversionRate = Shopify.currency.rate;
    const freeShippingThresholdInCents = 50 * 100 * conversionRate;    
    
    //We can now calculate the amount remaining (i.e. how much more the visitor must
    //spend to reach the threshold). Note that the discountedTotal is provided
    //in cents and in the currently select currency.
    const centsRemaining = freeShippingThresholdInCents - discountedTotal; 
    
    //Finally, we can calculate this amount as a percentage of the total.
    const percentageRemaining = Math.max(centsRemaining, 0) / freeShippingThresholdInCents * 100;
    const percentageComplete = 100 - percentageRemaining;
     
    //TODO: Update the progress bar
<strong>    //You'll need to update the code below to update the progress bar 
</strong>    //based on the discounted total.
    //How that is accomplished depends on the specific theme you use. 
    console.log(`Free shipping is ${percentageComplete}% complete, ${percentageRemaining}% left`);
}
</code></pre>

## Order bump

An order bump is an upsell that can be added to the cart with a simple checkbox. \
Examples include:

* Shipping protection
* Gift wrapping
* Priority processing

Unfortunately order bumps are often implemented in a way that is not compatible with Discount Ninja. Specifically, the code that checks for the order bump is executed when the submit button of the cart is clicked and the code typically redirects to the checkout by setting `window.location`.

This technique is not compatible with DN. Read more about how to integrate this approach properly [here](/discount-ninja-developer-hub/storefront-api/promotion-engine/javascript-api/events#redirect-to-checkout).

## Custom variant apps / swatch apps on the PDP

When configured correctly the app re-evaluates and re-renders the (discounted) price when the customer selects a different variant.

Discount Ninja automatically detects when a variant is selected on the PDP (product page). The app automatically detects this in one of the following two ways:

* The selected variant is set in the query parameter using the `variant` query parameter. Example: <https://my-shop.com/products/my-product?variant=123456>
* The selected variant is set as the `selectedOption` of the `select` input control with its `name` attribute set to `id`.

If your PDP is not compatible:

* Change the code of your variant selector to allow the app to detect the change in one of the two aforementioned ways
* Or inform the app that a new variant has been selected using a [variant changed event](/discount-ninja-developer-hub/storefront-api/promotion-engine/javascript-api/events#variant-changed).

Example of using the variant changed event:

```javascript
function myVariantChangedEventHandler() {
    // This function represents the function that handles the event
    // when your custom solution or swatch app has detected a new
    // variant has been selected.
    const variantId = 123456;
    
    // After processing the variant change in your theme
    // Publish this event to let Discount Ninja know
    document.dispatchEvent(new CustomEvent("la:dn:variant:changed", { 
        detail: { 
            data: { 
                variant: { 
                    id: variantId 
                } 
            } 
        } 
    }));
}
```

## Variants on the PLP

By default, the PLP (Collection page) shows the price of a single variant. Some themes allow the customer to select a different variant on the PLP without navigating to the PDP (Product page). To support this:

* custom JavaScript code must be added to set the `data-la-dn-product-variant` of the product card when the customer selects a different variant, cf. [this article](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/attributes#attributes)
* after setting the attribute, the custom JavaScript code must dispatch the `collection:updated` event, cf. [this article](/discount-ninja-developer-hub/storefront-api/promotion-engine/javascript-api/events#collection-products-updated)

```javascript
function variantSelectedEventHandler() {
    // Set the attribute
    // We're assuming "this" is an element (a color swatch for example) 
    // that is located inside the product card
    const productCardWithAttribute = this.closest('[data-la-dn-product-variant]');
    if (productCardWithAttribute) {
        // Get the currently selected variant
        // In this example, we're assuming that the variant is a property
        // available on "this"
        const variant = this.variant;
        
        // Set the attribute of the card 
        productCardWithAttribute.setAttribute('data-la-dn-product-variant', variant);
    }
    
    // Custom logic that updates collection products
    document.dispatchEvent(new CustomEvent('la:dn:collection:updated'));
}
```

## Pagination on the PLP

To handle Infinite scrolling and Load more pagination you'll need to either implement an event or mark the button. See [this page](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/pagination) for more details.

## Add to Cart Button

Discount Ninja does not integrate with the Add to Cart button specifically, nor does the app need themes or developers to make changes to those buttons for the app to work correctly.

### Problem: multiple items added to cart

However, occasionally issues will occur where an Add to Cart button will appear to malfunction when Discount Ninja is installed. Specifically, in rare occasions, users may find that their Add to Cart button adds multiple instances of a product when clicked once.

### Why this happens

This can happen when the theme's code contains a bug where clicking the Add to Cart results in two simultaneous network requests (to /cart.add.js). Shopify cannot process multiple simultaneous requests and, as a result, drops one of the requests and only honours one. This results in the expected behavior for the customer: only one item is added to the cart.

Discount Ninja forces network requests to be handled sequentially. This avoids situations where Shopify's inability to process simultaneous requests results in unexpected failures. This mechanism is important to ensure the functionality of the app works reliably.

As a result of processing the network requests sequentially, themes that accidentally send multiple simultaneous requests when the Add to Cart button is clicked, see a different behavior after installing Discount Ninja: the requests are now both honoured by Shopify, which results in multiple products being added to cart (2x the expected amount).

### How to solve this

To resolve this, the user must fix the bug in the theme's code. This typically involves:

* ensuring the click/submit events are not subscribed to multiple times
* ensuring the click event of the button is not bubbled up to the form

Example: the Marble 4.0.1 theme from BigStart contains a bug where the click event of the submit button is subscribed to multiple times. In some instances old script tags of Fastundle can also cause this issue.&#x20;


# Theme edits

Learn how Discount Ninja complements your theme to help merchants to promote offers.

## Do I need Discount Ninja's theme edits?

In short: yes.

If you do not have access to someone with technical skills, please request our support team to set it up for you.

The theme edits will allow you to see the correct price of all products that are discounted through Discount Ninja throughout your store. It also enables all the widgets that can be configured in the app that will help you to communicate the offers to your customers.

## Extension mechanisms

Discount Ninja offers two extension mechanisms to display widgets and show strikethrough pricing:

* **Theme extensions**: add widgets using App blocks
* **Custom code edits**: edit the code of the theme to indicate placeholders for widgets and/or mark where prices are displayed.

{% hint style="info" %}
Use App blocks when possible. Custom code edits are only required for scenarios that are not currently supported by Shopify on Online Store 2.0. Specifically, Discount Ninja's custom code edit support helps to overcome the lack of support for App blocks in the templates for collections and (drawer) carts. These templates do not allow for the use of App blocks in repeating sections (collection products or cart items).
{% endhint %}

## Theme extensions

The app uses theme extensions to extend the functionality of the online store. Specifically:

* **App embeds** are used to add scripts.
* **App blocks** are used to include widgets on the storefront.

Alternatively, users can choose to add custom code to add widgets.

## App blocks v Code edits&#x20;

The following table explains the advantages of each approach:

<table><thead><tr><th width="415"></th><th>App blocks</th><th>Code edits</th></tr></thead><tbody><tr><td>Available on stores that do not have an Online Store 2.0 theme? </td><td>No</td><td>Yes</td></tr><tr><td>Requires manually cleaning up code in the theme after uninstalling the app?</td><td>No</td><td><mark style="color:red;">Yes</mark></td></tr><tr><td>Maximum flexibility to position each widget?</td><td><mark style="color:red;">No</mark></td><td>Yes</td></tr><tr><td>Optimal performance?</td><td>Yes</td><td>Yes</td></tr><tr><td>Available for each widget?</td><td><mark style="color:red;">No</mark></td><td>Yes</td></tr></tbody></table>

Read more about the available App blocks [here](/discount-ninja-developer-hub/theme-edits/app-blocks).

Read more about how to edit theme code [here](/discount-ninja-developer-hub/theme-edits/code-edits).


# App blocks

App blocks are used to include widgets on the storefront.

{% hint style="info" %}
**Limitations**

Shopify's App blocks have, unfortunately, limited support in the current Online Store 2.0 themes as of 2024. Specifically, Shopify does not support adding App blocks in collection page products, on drawer carts or in cart lines. This limits the amount of widgets that can currently be offered as App blocks.
{% endhint %}

## Available App blocks

The following table documents which widgets are available as App blocks:

| Page type    | Widget              | Available                                                             |
| ------------ | ------------------- | --------------------------------------------------------------------- |
| Product page | Product Page Banner | <mark style="color:green;background-color:blue;">App block</mark>     |
| Product page | Product Price       | <mark style="color:green;background-color:blue;">App block</mark> ¹ ² |
| Collection   | Collection Badge    | No                                                                    |
| Collection   | Collection Price    | No                                                                    |
| Cart         | Item Price          | No                                                                    |
| Cart         | Line Price          | No                                                                    |
| Cart         | Text                | No                                                                    |
| Cart         | Promotion Summary   | <mark style="color:green;background-color:blue;">App block</mark> ¹   |
| Cart         | Promo Code Field    | <mark style="color:green;background-color:blue;">App block</mark> ¹   |

¹ Support for positioning these App blocks depends on the theme.

² Available as of Q1 2025

## Code edits

The remaining widgets are supported using code edits. The Discount Ninja support team to provide this service for you free of charge.


# Product Page Banner

Learn how to add the Product Page Banner App block

## How to enable the Product Page Banner as an App block <a href="#h_b67a3bf463" id="h_b67a3bf463"></a>

1. Open the Online Store section in the Shopify admin and click Themes:

   <div align="left"><figure><img src="/files/zcHUGUu7SVxcxLO4oxza" alt=""><figcaption></figcaption></figure></div>
2. Find the theme you want to customize and click the Customize button for that theme:

   <figure><img src="/files/0bMzhEKHTfMgi3klOWzp" alt=""><figcaption></figcaption></figure>
3. The theme editor opens. In the dropdown at the top, select the Products item:

   <div align="left"><figure><img src="/files/HQMBbtJeEW1pFx90J8oq" alt=""><figcaption></figcaption></figure></div>
4. Then select, Default product:

   <div align="left"><figure><img src="/files/Ok7RaInBdUy2ZpOxp7de" alt=""><figcaption></figcaption></figure></div>
5. The editor for the default product page opens. Find the location where you would want the product page banner to appear. Usually, this is below the Title block in the Product Information section. Locate this block in the sidebar on the left. Click the Add block button at the bottom of this section. A block selector opens. Select the Apps tab. Find the Product page banner app block of Discount Ninja and select it:\
   ![](/files/gdwmdbCsMC0u751nHeMt)
6. Change the location where the product page banner is rendered by dragging the block up or down in the list using the handle on the left:

   <div align="left"><figure><img src="/files/Pb8vkcJlwr2IX3ljrivt" alt=""><figcaption></figcaption></figure></div>
7. Alternatively, select the place where you want the banner to display and click the + sign. Then click the Apps tab and select the App block.\
   ![](/files/DKGebK6BvsfMDIDnX5Es)
8. IMPORTANT - in the theme editor the app displays a placeholder. This placeholder is not displayed on your live shop. To configure when a product page banner should be displayed and to edit the text or style, you need to edit the building blocks of the offers configured in Discount Ninja.
9. If you want to hide the block, use the eye icon to toggle visibility. Use the dustbin icon to remove the block:

   <div align="left"><figure><img src="/files/ENnjNGrgMO7nOKxujMN9" alt=""><figcaption></figcaption></figure></div>

## Changing the text and style <a href="#h_95ca361c3c" id="h_95ca361c3c"></a>

To configure when a product page banner should be displayed and to edit the text or style you need to open the app. In the app you'll need to edit the widgets of the offers configured in Discount Ninja.


# Promo Code Field

Learn how to add the Promo Code Field App block

## How to enable the Promo Code Field as an App block <a href="#h_3a79c10dfd" id="h_3a79c10dfd"></a>

1. Open the Online Store section in the Shopify admin and click Themes:

   <div align="left"><figure><img src="/files/zcHUGUu7SVxcxLO4oxza" alt=""><figcaption></figcaption></figure></div>
2. Find the theme you want to customize and click the Customize button for that theme:

   <figure><img src="/files/0bMzhEKHTfMgi3klOWzp" alt=""><figcaption></figcaption></figure>
3. The theme editor opens. In the dropdown at the top, select the Cart item:\
   ![](/files/tBNWQ5AQ7h5lBIZEZwHc)
4. The editor for the cart page opens. If the cart is empty, add a product to the cart. Find the location where you would want the cart summary to appear. Usually, this is below the Subtitle price block in the Subtotal section. Locate this block in the sidebar on the left. Click the Add block button at the bottom of this section:

   <div align="left"><figure><img src="/files/yXsPU15GxqUlRMcVA3yX" alt=""><figcaption></figcaption></figure></div>
5. A block selector opens. Select the Apps tab. Find the Promotion code field App block and select it.
6. Change the location where the promotion code field is rendered by dragging the block up or down in the list using the handle on the left:

   <div align="left"><figure><img src="/files/abXRSio0hip8y8W8iwrW" alt=""><figcaption></figcaption></figure></div>
7. IMPORTANT - in the theme editor the app displays a placeholder. This placeholder is not displayed on your live shop. To configure when a product page banner should be displayed and to edit the text or style, you need to edit the building blocks of the offers configured in Discount Ninja.
8. If you want to hide the block, use the eye icon to toggle visibility. Use the dustbin icon to remove the block:\
   ![](/files/7Rart3Idey6TCu3LdalB)

## Changing the text and style of the section <a href="#h_7e4616e51a" id="h_7e4616e51a"></a>

To configure the content, text and style of the cart promotion code field, you need to edit the building blocks in Discount Ninja. This can be done from the settings of the app in the menu Settings > Building blocks > Cart promotion code field


# Promotion Summary

Learn how to add the Promotion Summary App block

## How to enable the promotion summary as an App Block <a href="#h_4ed9436762" id="h_4ed9436762"></a>

1. Open the Online Store section in the Shopify admin and click Themes:

   [![](https://downloads.intercomcdn.com/i/o/483152528/6feccf333c957a55c571d818/image.png)](https://downloads.intercomcdn.com/i/o/483152528/6feccf333c957a55c571d818/image.png)
2. Find the theme you want to customize and click the Customize button for that theme:

   [![](https://downloads.intercomcdn.com/i/o/483152781/9d47047f92f78694a8cb62a9/image.png)](https://downloads.intercomcdn.com/i/o/483152781/9d47047f92f78694a8cb62a9/image.png)
3. The theme editor opens. In the dropdown at the top, select the Cart item:

   [![](https://downloads.intercomcdn.com/i/o/533551653/ce257c119a6841fa21514331/image.png)](https://downloads.intercomcdn.com/i/o/533551653/ce257c119a6841fa21514331/image.png)
4. The editor for the cart page opens. If the cart is empty, add a product to the cart. Find the location where you would want the cart summary to appear. Usually, this is below the Subtitle price block in the Subtotal section. Locate this block in the sidebar on the left. Click the Add block button at the bottom of this section:

   [![](https://downloads.intercomcdn.com/i/o/533553259/9145d9cf462808ab66388085/image.png)](https://downloads.intercomcdn.com/i/o/533553259/9145d9cf462808ab66388085/image.png)
5. A block selector opens. Scroll down to the Apps section. Find the Cart promotion summary app block of Discount Ninja and select it:

   [![](https://downloads.intercomcdn.com/i/o/533554666/fb49eb23721681816cc3c0b1/image.png)](https://downloads.intercomcdn.com/i/o/533554666/fb49eb23721681816cc3c0b1/image.png)
6. IMPORTANT - in the theme editor the app displays a placeholder. This placeholder is not displayed on your live shop. To configure when a product page banner should be displayed and to edit the text or style, you need to edit the building blocks of the offers configured in Discount Ninja.
7. Change the location where the promotion summary is rendered by dragging the block up or down in the list using the handle on the right:

   [![](https://downloads.intercomcdn.com/i/o/533555277/56ec1a35da811b0fbd4b2ade/image.png)](https://downloads.intercomcdn.com/i/o/533555277/56ec1a35da811b0fbd4b2ade/image.png)
8. If you want to hide the block, use the eye icon to toggle visibility:

   [![](https://downloads.intercomcdn.com/i/o/533555790/fe5817f3c6776a39e190635b/image.png)](https://downloads.intercomcdn.com/i/o/533555790/fe5817f3c6776a39e190635b/image.png)
9. To remove the block, select it and click the Remove block button at the bottom of the sidebar on the right:

   [![](https://downloads.intercomcdn.com/i/o/483154784/12544cdcce2239b0a56995de/image.png)](https://downloads.intercomcdn.com/i/o/483154784/12544cdcce2239b0a56995de/image.png)

## Changing the text and style of the section <a href="#h_a401241826" id="h_a401241826"></a>

To configure the content, text and style of the cart promotion summary, you need to edit the building blocks in Discount Ninja. This can be done from the settings of the app in the menu Settings > Building blocks > Cart promotion summary

<br>


# Code edits

{% hint style="info" %}
**Limitations**

Shopify's App blocks have, unfortunately, limited support in the current Online Store 2.0 themes as of 2024. Specifically, Shopify does not support adding App blocks in collection page products, on drawer carts or in cart lines. This limits the amount of widgets that can currently be offered as App blocks.
{% endhint %}

## Available App blocks

The following table documents which widgets are available as App blocks:

| Page type    | Widget              | Available                                                             |
| ------------ | ------------------- | --------------------------------------------------------------------- |
| Product page | Product Page Banner | <mark style="color:green;background-color:blue;">App block</mark>     |
| Product page | Product Price       | <mark style="color:green;background-color:blue;">App block</mark> ¹ ² |
| Collection   | Collection Badge    | No                                                                    |
| Collection   | Collection Price    | No                                                                    |
| Cart         | Item Price          | No                                                                    |
| Cart         | Line Price          | No                                                                    |
| Cart         | Text                | No                                                                    |
| Cart         | Promotion Summary   | <mark style="color:green;background-color:blue;">App block</mark> ¹   |
| Cart         | Promo Code Field    | <mark style="color:green;background-color:blue;">App block</mark> ¹   |

¹ Support for positioning these App blocks depends on the theme.

² Available as of Q1 2025

## Code edits

The remaining widgets are supported using code edits. The Discount Ninja support team to provide this service for you free of charge.


# Product Detail Page (PDP)

## Product Detail Page

In eCommerce, **PDP** stands for **Product Detail Page**. It’s the webpage where a specific product is showcased, providing detailed information to help customers make purchasing decisions.

The key elements typically found on a PDP include: product images, product description, price, call-to-action buttons (such as "Add to Cart" or "Buy Now") and customer reviews.

## Theme edits

Discount Ninja supports two widgets on the PDP:

* [Price](/discount-ninja-developer-hub/theme-edits/code-edits/product-detail-page-pdp/price)
* [Banner](/discount-ninja-developer-hub/theme-edits/code-edits/product-detail-page-pdp/banner)

## Collections

A PDP may contain collections, for example:

* A collection of products that the visitor has recently visited
* A collection of related products that may interest the visitor

To handle strikethrough pricing for those products, please refer to the [Product List Page (PLP) and Collections](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections) setup.


# Price

## Location

Place the `la-dn-price` attribute on the HTML element that spans all of the following:

* The section of the theme that renders the standard price of the variant when the variant is not on sale (i.e. the variant does not have a *compare-at price* in Shopify)
* The section of the theme that renders the discounted price of the variant when the variant is on sale (i.e. the variant has a *compare-at price* in Shopify)

And, if applicable:

* The “Sold out” text displayed if the variant is out of stock
* The “Sale” badges displayed if the variant has a compare at price

Ensure the attribute class is not placed on a section that renders HTML that is not related to the price, such as:

* Review widgets
* Installments

since all content in the section with the above attribute is replaced with the discounted price when the variant is discounted through Discount Ninja.

### Snippet

Use the following snippet to add the data attribute in a Liquid template:

```css
data-la-dn-price
```

### Example

```html
<div class="price" data-la-dn-price>
    ...
    <span class="money">{{ variant.price }}</span>
    <s class="money">{{ variant.compare_at_price }}</s>
    ...
</div>
```

## Style

The style (font, color, size) as well as the format (strikethrough or not, price first or compare-at price first...) can be configured in the app.

In the menu go to **Settings** > **Dynamic Pricing** > **Price format**.

## Subscription apps

{% hint style="warning" %}
The attributes described in this section will be supported as of November 2024
{% endhint %}

Some subscription apps show multiple prices for the selected product variant on the PDP. Specifically, they allow for showing:

* The price of a one-time purchase
* The price of a recurring purchase (subscription)

To support this:

* mark the price of the one-time purchase with the attribute `data-la-dn-price-inline` as well as an attribute `data-la-dn-price-onetime`&#x20;
* and the price of the recurring purchase with the attribute `data-la-dn-price-inline` as well as the attribute `data-la-dn-price-subscription`. Finally, add an attribute that indicates the id of the selected selling plan: `data-la-dn-price-subscription-sellingplan-id`

### Example

```html
<div class="price" 
     data-la-dn-price 
     data-la-dn-price-inline
     data-la-dn-price-onetime>
    ...
    One time purchase:
    <span class="money">{{ product.price }}</span>
    <s class="money">{{ product.compare_at_price }}</s>
    ...
</div>
{% assign default_selling_plan_allocation = product.selected_or_first_available_selling_plan_allocation %}
{% assign selling_plan_id = default_selling_plan_allocation.selling_plan.id %}
{% assign selling_plan_price = default_selling_plan_allocation.per_delivery_price %}
<div class="price" 
     data-la-dn-price 
     data-la-dn-price-inline
     data-la-dn-price-subscription 
     data-la-dn-price-subscription-sellingplan-id="{{ selling_plan_id }}">
    ...
    Subscribe and save
    <span class="money">{{ selling_plan_price }}</span>
    ...
</div>
```

## Multiple variants

To display the price of multiple variants of the same product on the PDP, users can use the markup used for products on the PLP in combination with the `data-la-dn-product-variant` attribute as explained [here](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/price#variant-price).

## Installments

The app automatically integrates with a number of installment apps, listed [here](/discount-ninja-developer-hub/integration#installment-apps).

To mark HTML elements as installment prices, you can add CSS selectors in the app.\
These settings are available from the Settings > General > Advanced menu in the Advanced tab.


# Banner

## App block alternative

If your theme supports App blocks (introduced in Online Store 2.0), avoid editing the theme and use the [Product Page Banner App block](/discount-ninja-developer-hub/theme-edits/app-blocks/product-page-banner) instead.

## Placeholder

### Snippet

Use the following snippet to add the placeholder in a Liquid template:

```html
<div la-dn-product-banner></div>
```

### Example

```html
<div class="title">
    <h1>{{ product.title | escape }}</h1>
    ...
</div>
<div la-dn-product-banner></div>
```


# Badge

{% hint style="info" %}
The instructions in this article constitute a workaround to show a badge on the Product Detail Page (PDP) that is originally designed to be displayed on the Product List Page (PLP) and Collections. The style and text of the badges displayed on the PDP cannot be managed separately.
{% endhint %}

## Attributes

Add the data attributes, documented [here](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/attributes#snippet), around the section where you want to display the badge.

## Price

In order for the badge to work, a price section must be included inside the elemented that has the attributes. This section should be hidden.

Example:

```html
<span style="display:none" data-la-dn-price></span>
```

## Badge

Optionally, add the badge class, documented [here](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/badge), on the element that should act as the parent of the badge.

## Example

```html
<div id="my-product-page-image-gallery"
     data-la-dn-product-handle="{{ product.handle }}" 
     data-la-dn-product-id="{{ product.id }}" 
     data-la-dn-product-collection-handles="{{ product.collections | map: 'handle' | join: ',' | default: '[[--NONE--]]' }}" 
     data-la-dn-product-collection-ids="{{ product.collections | map: 'id' | join: ',' | default: '[[--NONE--]]' }}" 
     data-la-dn-product-available="{{ product.available}}" 
     data-la-dn-product-price="{{ product.first_available_variant.price | default: product.price }}"
     data-la-dn-product-compare-at-price="{{ product.first_available_variant.compare_at_price | default: product.first_available_variant.price | default: product.compare_at_price | default: product.price }}" 
     data-la-dn-product-compare-at-price-varies="{{ product.compare_at_price_varies }}"
     data-la-dn-product-price-varies="{{ product.price_varies }}" 
     data-la-dn-product-price-min="{{ product.price_min }}"
     data-la-dn-product-tags="{{ product.tags | join: ',' | escape | default: '[[--NONE--]]' }">
    ...
    <span style="display:none" data-la-dn-price></span>
    ...
    <div id="my-product-page-image" class="la-dn-collection-badge">
        <img>
        ...
    </div>    
</div>
```


# Product List Page (PLP) and Collections

## Product List Page

In eCommerce, **PLP** stands for **Product Listing Page**. It’s the page that displays a collection of products within a category or search results, helping customers browse and find the items they’re interested in.

The key elements typically found on a PLP include: product thumbnails, product titles, price information, filters and sorting options, pagination and quick view.

## Collection

The term **collections** is used to refer to PLPs as well as lists of products displayed on any other page. For example, collections can be included on a home page or a product page (Product Detail Page) to show a list of related products. These collections can be presented as lists, grids or carrousels.&#x20;

## Theme edits

Discount Ninja supports two widgets on PLPs and collections:

* Price
* Badge

To support the above widgets, the PLP/collection also needs to implement attributes for the &#x20;


# Attributes

{% hint style="info" %}
The term **collections** is used to refer to collection pages (Product Listing Page or PLP) as well as lists of products displayed on any other page. For example, collections can be included on a home page or a Product Detail Page (PDP).&#x20;
{% endhint %}

## Mechanism

The script will look for Discount Ninja [price sections](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/attributes#price) on a page that are marked.

The script will then, for each price, find the HTML parent that includes data attributes. If data attributes are found, the product is assumed to be part of a collection.

The script can then apply strikethrough pricing to each of the products in the collection that it has detected.

## Location

On collection pages, Discount Ninja needs information about the products that are rendered in the collection.

To provide the necessary context, add the data attributes to the element that spans:

* The section that renders the collection product image
* The section that renders the collection product price

## Minimum requirement

The `data-la-dn-product-id` (or, in case the id is missing the `data-la-dn-product-handle`) is mandatory. It is highly recommended to include all attributes though.&#x20;

{% hint style="warning" %}
The app executes a network request for every product that does not include all attributes, unless it can obtain the information through a different mechanism. This can impact the performance of the online store. It is therefore **important** to **include all attributes** to achieve optimal performance.
{% endhint %}

## Markets

Including the `data-la-dn-product-id` attribute is essential to ensure compatibility when handles are translated using the Markets feature. Discount Ninja properly handles translated handles since February 2026.

## Snippet

Use the following snippet to add the data attributes in a Liquid template:

```css
data-la-dn-product-handle="{{ product.handle }}" 
data-la-dn-product-id="{{ product.id }}" 
data-la-dn-product-collection-handles="{{ product.collections | map: 'handle' | join: ',' | default: '[[--NONE--]]' }}" 
data-la-dn-product-collection-ids="{{ product.collections | map: 'id' | join: ',' | default: '[[--NONE--]]' }}" 
data-la-dn-product-available="{{ product.available}}" 
data-la-dn-product-price="{{ product.first_available_variant.price | default: product.price }}"
data-la-dn-product-compare-at-price="{{ product.first_available_variant.compare_at_price | default: product.first_available_variant.price | default: product.compare_at_price | default: product.price }}" 
data-la-dn-product-price-varies="{{ product.price_varies }}" 
data-la-dn-product-compare-at-price-varies="{{ product.compare_at_price_varies }}"
data-la-dn-product-price-min="{{ product.price_min }}"
data-la-dn-product-tags="{{ product.tags | join: ',' | escape | default: '[[--NONE--]]' }}"
```

The attributes in the snippet make reference to a variable named `product`. For example `{{ product.handle }}`.

It is possible that the theme uses a different variable name, for example: `product_item`. If this is the case, you need to change all occurrences of the variable to the correct variable name, for example: `{{ product_item.handle }}`.

### Example

```html
<div class="collection-product"
    data-la-dn-product-handle="{{ product.handle }}" 
    data-la-dn-product-id="{{ product.id }}" 
    data-la-dn-product-collection-handles="{{ product.collections | map: 'handle' | join: ',' | default: '[[--NONE--]]' }}" 
    data-la-dn-product-collection-ids="{{ product.collections | map: 'id' | join: ',' | default: '[[--NONE--]]' }}" 
    data-la-dn-product-available="{{ product.available}}" 
    data-la-dn-product-price="{{ product.first_available_variant.price | default: product.price }}"
    data-la-dn-product-compare-at-price="{{ product.first_available_variant.compare_at_price | default: product.first_available_variant.price | default: product.compare_at_price | default: product.price }}" 
    data-la-dn-product-compare-at-price-varies="{{ product.compare_at_price_varies }}"
    data-la-dn-product-price-varies="{{ product.price_varies }}" 
    data-la-dn-product-price-min="{{ product.price_min }}"
    data-la-dn-product-tags="{{ product.tags | join: ',' | escape | default: '[[--NONE--]]' }}">
    <img ...>
    <div class="price" data-la-dn-price>
        <span class="money">{{ product.price }}</span>
        ...
    </div>
</div>
```

## Product Variant support

To support product cards that represent individual variants rather than individual products:

* Add the `data-la-dn-product-variant` attribute (see below)
* In the advanced settings of the app (General > Advanced > Advanced Settings) check the setting "Assume product cards in lists are variants, not products"

## Attributes

{% hint style="info" %}
All attributes must be prefixed `data-la-dn-product-`
{% endhint %}

<table><thead><tr><th width="239">Attribute</th><th width="183">Data type</th><th>Notes</th></tr></thead><tbody><tr><td><code>handle</code></td><td><code>string</code></td><td>The <a href="https://shopify.dev/docs/api/liquid/basics#handles">handle</a> of the product.</td></tr><tr><td><code>id</code></td><td><code>number</code></td><td>The ID of the product.</td></tr><tr><td><code>collection-handles</code></td><td><p><code>string |</code> </p><p><code>"[[--NONE--]]"</code></p></td><td>A comma-separated list of collection  <a href="https://shopify.dev/docs/api/liquid/basics#handles">handles</a> indicating which collections the product belongs to.<br>Use <code>[[--NONE--]]</code> if the product does not belong to any collections.</td></tr><tr><td><code>collection-ids</code></td><td><p><code>string |</code> </p><p><code>"[[--NONE--]]"</code></p></td><td>A comma-separated list of collection IDs indicating which collections the product belongs to.<br>Use <code>[[--NONE--]]</code> if the product does not belong to any collections.</td></tr><tr><td><code>available</code></td><td><code>boolean</code></td><td><code>true</code> if the variant is available. <code>false</code> if the variant is out of stock.</td></tr><tr><td><code>price</code></td><td><code>number</code></td><td>The price (in cents) of the first available variant of the product. This should align with the title and image displayed.</td></tr><tr><td><code>compare-at-price</code></td><td><code>number</code></td><td>The compare-at price (in cents) of the first available variant of the product. This should align with the title and image displayed.</td></tr><tr><td><code>price-varies</code></td><td><code>boolean</code></td><td><code>true</code> if the product includes variants with different prices. <code>false</code> if all variants have the same price.<br><br>If <code>price-varies</code> is true, the app uses the <code>price-min</code> instead of the <code>price</code>, unless a <code>variant</code> is specified.</td></tr><tr><td><code>compare-at-price-varies</code></td><td><code>boolean</code></td><td><code>true</code> if the product includes variants with different compare-at prices. <code>false</code> if all variants have the same compare-at price.<br><br>If <code>compare-at-price-varies</code> is true, the app assumes some variants have a compare-at price and others don't.</td></tr><tr><td><code>price-min</code></td><td><code>number</code></td><td>The lowest price (in cents) of any variants of the product. This price is displayed when <code>price-varies</code> is <code>true</code>.</td></tr><tr><td><code>tags</code></td><td><p><code>string |</code> </p><p><code>"[[--NONE--]]"</code></p></td><td>A comma-separated list of tags of the product.<br>Use <code>[[--NONE--]]</code> if the product does not have any tags.</td></tr><tr><td><code>variant</code></td><td><code>number</code></td><td>[Optional] The id of the variant in this attribute. If omitted the price of the first available or lowest priced variant is used. See the section on variant price.</td></tr></tbody></table>


# Price

{% hint style="info" %}
The term **collections** is used to refer to collection pages (Product Listing Page or PLP) as well as lists of products displayed on any other page. For example, collections can be included on a home page or a Product Detail Page (PDP).&#x20;
{% endhint %}

## Requirements

To show strikethrough pricing and/or badges in collections, you must edit the theme to ensure that each product in a collection has:

* [data attributes](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/attributes)
* a price that is marked

## Data attributes

Displaying strikethrough pricing for products in a collection requires that data attributes are configured. Read [this article](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/attributes) to learn how to set up the required data attributes.

## Mechanism

The script will look for Discount Ninja price sections on a page.

The script will then, for each price, find the HTML parent that includes [data attributes](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/attributes#data-attributes). If data attributes are found, the product is assumed to be part of a collection.

The script can then apply strikethrough pricing to each of the products in the collection that it has detected.

## Location

Place the `la-dn-price` attribute on the HTML element that spans all of the following:

* The section of the theme that renders the standard price of the variant when the variant is not on sale (i.e. the variant does not have a *compare-at price* in Shopify)
* The section of the theme that renders the discounted price of the variant when the variant is on sale (i.e. the variant has a *compare-at price* in Shopify)

And, if applicable:

* The “Sold out” text displayed if the variant is out of stock
* The “Sale” badges displayed if the variant has a compare at price

Ensure the attribute class is not placed on a section that renders HTML that is not related to the price, such as:

* Review widgets
* Installments

since all content in the section with the above attribute is replaced with the discounted price when the variant is discounted through Discount Ninja.

### Snippet

Use the following snippet to add the data attribute in a Liquid template:

```css
data-la-dn-price
```

### Example

```html
<div class="price" data-la-dn-price>
    ...
    <span class="money">{{ product.price }}</span>
    <s class="money">{{ product.compare_at_price }}</s>
    ...
</div>
```

## Style

The style (font, color, size) as well as the format (strikethrough or not, price first or compare-at price first...) can be configured in the app.

In the menu go to **Settings** > **Dynamic Pricing** > **Price format**.

## Variant price

By default the price of the first available variant is used. The lowest price variant is used instead when the `data-la-dn-product-price-varies` attribute is set to `true`.&#x20;

To use the price of a different variant:

* set the `price` and `compare-at-price` attributes to the values of the variant you want to display prices for
* alternatively, provide the id of the variant in the `data-la-dn-product-variant` attribute. The app will then override the `price` and `compare-at-price` with the price of the `variant`.

## Variants with different prices

When products have variants with different prices, the `price-varies` attribute should be set to `true`. This causes the app to show the price of the product as follows "From `price-min`" where `price-min` is the lowest price of any variants of the product (example: *from $25.95*) . This behavior can be disabled in the app. The "from" label can also be changed or translated in the app.

## Unavailable products

If the selected product variant is unavailable, the `available` attribute should be set to `false`. This causes the app to show a "Sold out" label. This behavior can be disabled in the app. The "sold out" label can also be changed or translated in the app.

## Limitations

* The collection price shows a single price per product (by default the price of the variant with the lowest price). The app cannot display a price per variant on the collection page. Variants with different prices are supported on product pages.
* The app does not support:
  * The concept of “Unit price” (discounted prices cannot be displayed for unit prices)
  * Different prices for products per Market

## Integrations with other apps

Discount Ninja can render strikethrough pricing on collections that are managed by other apps. See [this article](/discount-ninja-developer-hub/integration#search-and-filter-apps) for more information.

The following articles provide details for the apps that Discount Ninja integrates with:

* [Boost AI Search & Filter](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/price/boost-ai-search-and-filter-aka-pfs) (formerly known as PFS)
* [Searchanise Search & Filter](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/price/searchanise-search-and-filter)
* [Globo Smart Search & Product Filte](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/price/globo-smart-product-filter-and-search-aka-spf)r (SPF)
* [AI Search & Product Filter](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/price/ai-search-and-product-filter-ultimate-search) (Ultimate Search)


# Searchanise Search & Filter

## Snippet

Add the following snippet inside the `body` element in the `theme.liquid`.

<pre class="language-javascript"><code class="lang-javascript">&#x3C;script>
<strong>    document.addEventListener('Searchanise.AutocompleteUpdated', function() {
</strong>        document.dispatchEvent(new CustomEvent('la:dn:collection:updated'));
    });
    
    document.addEventListener('Searchanise.ResultsUpdated', function() {
        document.dispatchEvent(new CustomEvent('la:dn:collection:updated'));
    });
&#x3C;/script>
</code></pre>

## Alternative, using Loaded event

If the above does not work for you, uou may need to wrap this into the `Searchanise.Loaded` event. \
To do this use the following snippet:

```javascript
<script>
  document.addEventListener('Searchanise.Loaded', function() {
    (function($) {
      $(document).on('Searchanise.AutocompleteUpdated', function(event, input, container) {
        document.dispatchEvent(new CustomEvent('la:dn:collection:updated'));
      });
       $(document).on('Searchanise.ResultsUpdated', function(event, input, container) {
        document.dispatchEvent(new CustomEvent('la:dn:collection:updated'));
      });
    })(window.Searchanise.$);
  });
</script>
```


# Globo Smart Product Filter & Search (aka SPF)

## Attributes

* Find the `globo.filter.search.liquid` file
* Apply the [collection data attributes](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/attributes)
  * **IMPORTANT**: multiply prices times 100 using the liquid code `| times: 100`
  * The multiplication be applied on all price attributes:
    * `data-la-dn-product-price`
    * `data-la-dn-product-compare-at-price` (2 times)
    * `data-la-dn-product-price-min`
  * Example:

    `data-la-dn-product-price="{{ product.first_available_variant.price | times: 100 }}"`

    Apply this to *all* prices in the data tags.
* Find the `globo.filter.product.liquid`
* Apply the collection data attributes to the `div` with class `spf-product-card`
  * **IMPORTANT**: multiply prices times 100 (see above)
* Add the [collection badge](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/badge) to the `div` with class `spf-product-card__image-wrapper`

## Hide sale badges of the app

* Add to the CSS of the app:

```css
.limoniapps-discountninja-hidethirdpartybadges .spf-product__label-sale { display: none; }
```


# AI Search & Product Filter (Ultimate Search)

* Open the Ultimate Search app
* Find the Customization menu
* Select the live theme

<figure><img src="/files/ie4DME21RKPjIF17WvJ5" alt=""><figcaption><p>Theme picker</p></figcaption></figure>

* Update the JS template
  * Add the data attribute for the product handle as follows: `:data-la-dn-product-handle="product.urlName"`
  * Mark the collection product price:  `la-dn-price`

<figure><img src="/files/HNehTLcJA1GGRZrMcQpf" alt=""><figcaption><p>Example</p></figcaption></figure>


# Boost AI Search & Filter (aka PFS)

## Version 0

* Find the file `bc-sf-filter.js`
* Find the `var bcSfFilterTemplate` variable.
* Add `{{discountNinjaDataTags}}` where the data tags should be placed
* And add the [collection badge](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/badge) class where the collection badge should be displayed
* And `<span class="la-dn-price">…</span>` around the price section (if present), typically `{{itemPrice}}`
* Add [this function](#builddatatags-function) before the `BCSfFilter.prototype.buildProductGridItem` function.
  * **IMPORTANT**: prices will be multiplied by 100
  * Avoid this by making the following change in the function: change `var pricesAreCents = false;` to `var pricesAreCents = true;`
* Before the end of each of the `BCSfFilter.prototype.buildProductGridItem` function, add the following code:

```javascript
//Discount Ninja integration       
itemHtml = itemHtml.replace(/{{discountNinjaDataTags}}/g, buildDiscountNinjaDataTags(data));
```

## Version 1

### HTML Template

See V2

## JavaScript

* Find the file `boost-pfs-filter.js`
* Find the `var boostPFSTemplate` variable.
* Add `{{discountNinjaDataTags}}` where the data tags should be placed
* And add the [collection badge class](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/badge) where the collection badge should be displayed
* And `<span class="la-dn-price">…</span>` around the price section (if present), typically `{{itemPrice}}`

<figure><img src="/files/106f8jzPPEGYrQ09eI8K" alt=""><figcaption><p>Mark price section</p></figcaption></figure>

* Add [this function](#builddatatags-function) before the `compileTemplate` functions.
* Before the end of each of the compileTemplate functions for the gridView (`ProductListItem.prototype.compileTemplate`) and the listView (`ProductGridItem.prototype.compileTemplate`), add the following code:

```javascript
//Discount Ninja integration       
itemHtml = itemHtml.replace(/{{discountNinjaDataTags}}/g, buildDiscountNinjaDataTags(data));
```

<figure><img src="/files/wJIFxQbGbR6ZhUZA93yY" alt=""><figcaption><p>Add tags to HTML</p></figcaption></figure>

## Version 2

### HTML Template

* Find the file `boost-pfs-filter-html.liquid`
* Find the `productGridItemHtml` and `productListItemHtml` sections
* Add `{{discountNinjaDataTags}}` where the data tags should be placed
* And add the [collection badge class](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/badge) where the collection badge should be displayed
* And `<span class="la-dn-price">…</span>` around the price section (if present), typically `{{itemPrice}}`

<figure><img src="/files/ExeRdAn6XwmiTtYOU56a" alt=""><figcaption><p>Mark product price and tags</p></figcaption></figure>

### JavaScript

* Find the file `boost-pfs-filter.js`
* Add [this function](#builddatatags-function) before the `compileTemplate` functions. (under “BUILD PRODUCT LIST”)
* Before the end of each of the compileTemplate functions for the gridView (`ProductListItem.prototype.compileTemplate`) and the listView (`ProductGridItem.prototype.compileTemplate`), add the following code:

```javascript
//Discount Ninja integration       
itemHtml = itemHtml.replace(/{{discountNinjaDataTags}}/g, buildDiscountNinjaDataTags(data));
```

<figure><img src="/files/4IblnyeLkukFSTI5nlgj" alt=""><figcaption><p>Add tags to HTML</p></figcaption></figure>

* Find the end of the `Filter.prototype.afterRender` function
* Add the following code:

```javascript
//Discount Ninja integration 
document.dispatchEvent(new CustomEvent('la:dn:collection:productsadded'));
```

<figure><img src="/files/1xgN3KFcHw3sfTiBdFVJ" alt=""><figcaption><p>Dispatch event</p></figcaption></figure>

### QuickView

* Find the `product.boost-pfs-quickview.liquid` file
* Add the following to the div with class boost-pfs-quickview-right product-details:

```html
data-la-dn-product-handle="{{ product.handle }}" data-la-dn-product-quickview
```

* Add the following to the div with class boost-pfs-quickview-price

```html
limoniapps-discountninja-productprice limoniapps-discountninja-productprice-dynamic
```

* Below the block add the [product banner](/discount-ninja-developer-hub/theme-edits/code-edits/product-detail-page-pdp/banner)

### Search suggestions

Currently not supported. No template in Liquid files.

## Version 3

With Version 3 the app doesn’t add any assets to the theme so we’ll have to use the CSS selectors approach.

* Go to Discount ninja > Settings > Dynamic pricing > Theme Configuration and enable Mix mode:

<figure><img src="/files/GT55XuwreKZ2TsgFzXhK" alt=""><figcaption><p>Mixed mode</p></figcaption></figure>

* It’ll unlock the second tab ‘ Live Theme ‘, go to this tab and enable ‘Custom’ option for Collection pages

<figure><img src="/files/aVcWDtHuWDXJbvo45jvp" alt=""><figcaption><p>Custom mode of PLP</p></figcaption></figure>

* Now go to Collection page and enable ‘Custom CSS selector’ and add the following classes:
  * Price (desktop) : `[[WRAP]].boost-sd__product-price-wrapper, .boost-sd__suggestion-queries-item-price`
  * Card: .`boost-sd__product-item, .boost-sd__suggestion-queries-item`
  * Handle: `[[HANDLE:href]]a.boost-sd__product-link`

<figure><img src="/files/qJlisJowv8IlJby5uiI7" alt=""><figcaption><p>CSS selectors</p></figcaption></figure>

## BuildDataTags function

```javascript
//Discount Ninja integration
function buildDiscountNinjaDataTags(data) {
    function buildTags(tags) {
      try {
        if (typeof tags === 'undefined' || tags === null) return '';
        
        if (typeof tags === 'string') {
            return tags ? tags.replace(/"/g, "&quot;") : '';
        }
        else if (Array.isArray(tags)) {
            return tags.join(',').replace(/"/g, "&quot;");
        }
        else {
            console.error('Boost PFS buildTags failed, unexpected data type of tags', e);
            return '';
        }
      }
      catch(e) {
          console.error('Boost PFS buildTags failed', e);
          return '';
      }  
    }
  
    try {
        var pricesAreCents = false;
        var pricesMultiplicator = pricesAreCents ? 1 : 100;    
        var discountNinjaItemHandle = data.handle;
        var discountNinjaItemCollectionHandles = '';
        var discountNinjaItemCollectionIds = '';
        if (data.collections && data.collections.length > 0) {
          for (var i = 0; i < data.collections.length; i++) {
              discountNinjaItemCollectionHandles += data.collections[i].handle + ","; 
              discountNinjaItemCollectionIds += data.collections[i].id + ","; 
          }
        }
        var discountNinjaItemAvailable = data.available;
        var firstAvailableVariant = null;
        for (var i = 0; i < data.variants.length; i++) {
            var variant = data.variants[i];
            if (variant.available) {
               firstAvailableVariant = variant;
               break;
            }
        }
        if (firstAvailableVariant === null) firstAvailableVariant = data.variants[0];
        var discountNinjaItemPrice = firstAvailableVariant && firstAvailableVariant.price > 0 ? firstAvailableVariant.price * pricesMultiplicator : 0;
        var discountNinjaItemCompareAtPrice = firstAvailableVariant && firstAvailableVariant.compare_at_price > 0 ? firstAvailableVariant.compare_at_price * pricesMultiplicator : 0;
        var discountNinjaItemPriceVaries = data.price_min != data.price_max;
        var discountNinjaItemPriceMin = data.price_min ? data.price_min * pricesMultiplicator : 0;
        var discountNinjaItemTags = buildTags(data.tags);
        return 'data-limoniapps-discountninja-product-handle="' + discountNinjaItemHandle + '" data-limoniapps-discountninja-product-collections="' + discountNinjaItemCollectionHandles + '" data-limoniapps-discountninja-product-collectionids="' + discountNinjaItemCollectionIds + '" data-limoniapps-discountninja-product-available="' + discountNinjaItemAvailable + '" data-limoniapps-discountninja-product-price="' + discountNinjaItemPrice + '" data-limoniapps-discountninja-product-compareatprice="' + discountNinjaItemCompareAtPrice + '" data-limoniapps-discountninja-product-pricevaries="' + discountNinjaItemPriceVaries + '" data-limoniapps-discountninja-product-pricemin="' + discountNinjaItemPriceMin + '" data-limoniapps-discountninja-product-tags="' + discountNinjaItemTags + '"';
    }
    catch(e) {
    	console.error('Boost PFS buildDiscountNinjaDataTags failed', e);
        return '';
    }
}
```


# Badge

{% hint style="info" %}
The term **collections** is used to refer to collection pages (Product Listing Page or PLP) as well as lists of products displayed on any other page. For example, collections can be included on a home page or a Product Detail Page (PDP).&#x20;
{% endhint %}

{% hint style="warning" %}
This code edit is optional and should only be performed if the default position of the badge is not in line with expectations. However, the data attributes are always required for the badge to be displayed.
{% endhint %}

## Requirements

To show strikethrough pricing and/or badges in collections, you must edit the theme to ensure that each product in a collection has:

* [data attributes](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/attributes)
* a [price](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/price) that is marked

## Data attributes

Displaying badges on products in a collection requires that data attributes are configured. Read [this article](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/attributes) to learn how to set up the required data attributes.

## Mechanism

The script will look for Discount Ninja price sections on a page.

The script will then, for each price, find the HTML parent that includes [data attributes](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/attributes#data-attributes). If data attributes are found, the product is assumed to be part of a collection.

The script will then add the badge as a child of the HTML parent. If you want to place the badge in a different location you must mark that location with the CSS class `la-dn-collection-badge`.

## Location

If the default location of the badge does not meet your expectations, place the `la-dn-collection-badge` CSS class on the HTML element where the collection badge should be displayed. Typically the badge is displayed on the `img` element that displays the product image in the collection. Note that this CSS class changes the `position` attribute of the element to `relative`.

The HTML element with the CSS class `la-dn-collection-badge` class must be an element that is nested inside the HTML element with the [data attributes](/discount-ninja-developer-hub/theme-edits/code-edits/product-list-page-plp-and-collections/attributes).

### Snippet

Use the following snippet to add the CSS class in a Liquid template:

```css
la-dn-collection-badge
```

### Example

```html
<div class="collection-product"
     data-la-dn-product-handle="{{ product.handle }}" 
    ...>
    <img ... class="la-dn-collection-badge">
    <div class="price" data-la-dn-price>
        <span class="money">{{ product.price }}</span>
        ...
    </div>
</div>
```

## Style

The style (text color, background color, size, shape) as well as the text (-20%, 1+1 FREE, ...) can be configured in the app by editing the widgets associated with the offers.

## Integrations with other apps

The native collection badges of your theme or third-party app may conflict with the collection badges displayed by Discount Ninja.

To avoid this you can add a CSS rule that will hide the other badges when Discount Ninja's badges are displayed:

* Identify the CSS selector for your theme's collection badges. In the example below this selector is `TODO-THEME-SPECIFIC`
* Add a custom CSS rule in Discount Ninja (Menu > Settings > Custom CSS):

```css
.limoniapps-discountninja-hidethirdpartybadges TODO-THEME-SPECIFIC { display: none; }
```


# Pagination

## Standard pagination

Standard pagination is a technique used to break down a large dataset into smaller, manageable "pages" of content, each accessible by a unique URL or link. Users can navigate through these pages one at a time, typically using "Previous" and "Next" buttons or numbered page links at the bottom of the page.

Discount Ninja automatically detects pagination and applies strikethrough pricing when users navigate to the next page.

## Infinite scrolling

In infinite scrolling, additional content loads dynamically as the user scrolls down the page, rather than navigating through paginated sections or pressing a button. This creates a seamless experience by continuously appending data to the page without requiring user intervention.

To ensure strikethrough pricing is applied when new products are added to the PLP in this way, JavaScript is required. The [Collection products updated](/discount-ninja-developer-hub/storefront-api/promotion-engine/javascript-api/events#collection-products-updated) event is used to communicate to the app that strikethrough pricing needs to be applied to the added products.

### Example

```javascript
function myInfiniteScrolling() {
    // Original logic
    ... fetch products
    ... render products
    
    // Inform Discount Ninja that new products have been added
    // Publish this event after the products have rendered
    document.dispatchEvent(new CustomEvent('la:dn:collection:updated'));
}
```

## Load more pagination

However, when content is loaded only after pressing a button, it's often called **"load more" pagination**. While similar to infinite scrolling, it requires an explicit user action to trigger the next batch of content, rather than loading it automatically.

To ensure strikethrough pricing is applied when new products are added to the PLP in this way, you must mark the button with the `data-la-dn-loadmore` attribute.

### Snippet

Use the following attribute to mark a "load more" button:

```html
data-la-dn-loadmore
```

### Example

```html
<button id="loadMoreBtn" data-la-dn-loadmore>Load More</button>
```


# Quick View

## Definition

A Quick View is a popup that provides a form to add a product variant to the cart. It is usually accessible from a PLP or the home page.

## QuickView section

Mark the Quick View section with a `data-la-dn-quickview` attribute.

## Product and variant

Mark the same section that was marked `data-la-dn-quickview` with a `data-la-dn-product-handle` attribute to indicate the product it displays. Also, add the `data-la-dn-product-variant` attribute to indicate which variant is displayed. When a different variant is selected, this attribute must be updated.&#x20;

Example:

```html
data-la-dn-product-handle="my-product" data-la-dn-product-variant="123456" data-la-dn-product-dynamic
```

## Price section

Use the following snippet to add the `data-la-dn-price` attribute to the price section. Also, add a `data-la-dn-product-dynamic` attribute to indicate the price should be updated when the `data-la-dn-product-variant` attribute changes.

```css
data-la-dn-price data-la-dn-product-dynamic
```

## Example

The following example shows how all the elements come together:

```html
<div data-la-dn-quickview 
     data-la-dn-product-handle="my-product" 
     data-la-dn-product-variant="123456">
    ...
    <div class="price" data-la-dn-price data-la-dn-product-dynamic>
        ...
        <span class="money">{{ variant.price }}</span>
        <s class="money">{{ variant.compare_at_price }}</s>
        ...
    </div>
    ...
</div>
```


# Cart

## Cart v checkout

The cart page represents the shopping basket that the customer intends to check out. The customer can add and remove products to review the price and then proceed to the checkout to provide payment and shipping details and place an order.

## Classic v drawer

All themes include a "classic" cart page, which can be accessed using the `/cart` path (e.g. [www.myshop.com/cart](http://www.myshop.com/cart)).

Some themes and third-party apps offer a drawer cart (also known as slide-out cart) which displays the content of the shopping cart to the customer using a pop-up on the current page, i.e. without navigating to the `/cart` path.

Discount Ninja is compatible with both types of cart pages.

## Theme edits

To support displaying discounted prices in the classic cart page and the drawer cart, you'll need to make the following edits:

* Mark the root (only for drawer cart)
* Cart item: item price, line price, quantity, text and built-in discounts
* Mark the subtotal
* Optionally remove the subtotal and use the Promotion Summary widget
* Optionally use the Promo Code Field widget

## Integration

To ensure proper integration with other apps that can be found on the cart page, please check the integration articles available [here](/discount-ninja-developer-hub/integration).

The integration articles include information regarding:

* Terms and conditions checkboxes
* Pickup + delivery apps
* Progress bar
* Order bumps


# Root

{% hint style="info" %}
This section is only required for a drawer cart, it is not required for a classic cart page.
{% endhint %}

## Drawer Cart

### Snippet

Use the following attribute to mark the root of the drawer cart. The root refers to the section that spans all line items and the subtotal section.

```html
la-dn-drawercart-root
```

### Example

```html
<div id="drawer-cart" la-dn-drawercart-root>
    ...
    ... line items
    ...
    ... subtotal
    ...
</div>
```

### Separate subtotal and line items

Some themes have separate sections for the subtotal and the line items. If this is the case, you can use the following attributes instead:

```
la-dn-drawercart-lineitems
la-dn-drawercart-cartsummary
```

### Example

```html
<div id="drawer-cart-lines" la-dn-drawercart-lineitems>
    ...
    ... line items
    ...
</div>
<div id="drawer-cart-form" la-dn-drawercart-cartsummary>
    ...
    ... subtotal
    ...
</div>
```

## Cart

### Snippet

Use the following attribute to mark the root of the drawer cart. The root refers to the section that spans all line items and the subtotal section.

```html
la-dn-cart-root
```

### Example

```html
<div id="drawer-cart" la-dn-cart-root>
    ...
    ... line items
    ...
    ... subtotal
    ...
</div>
```

### Separate subtotal and line items

Some themes have separate sections for the subtotal and the line items. If this is the case, you can use the following attributes instead:

```
la-dn-cart-lineitems
la-dn-cart-cartsummary
```

### Example

```html
<div id="cart-lines" la-dn-cart-lineitems>
    ...
    ... line items
    ...
</div>
<div id="cart-form" la-dn-cart-cartsummary>
    ...
    ... subtotal
    ...
</div>
```


# Cart item

## Is this required?

{% hint style="info" %}
Without the edits described below, themes only display product and order discounts correctly on the cart page and drawer cart **after a page refresh**. This is because Shopify themes are typically not built to refresh the cart and drawer cart dynamically. Discount Ninja solves this problem, but requires the edits below to handle this correctly.
{% endhint %}

It is essential to set up the changes listed below to maximize the potential of the app and to optimize for conversion rate. Failure to set this up will result in inconsistent behavior that will confuse your customer.

## Definition

A Shopify cart consists of one or more cart line items. Each cart item defines the quantity of a specific variant (optionally combined with a selling plan, discount and line item properties) a customer intends to check out.

## Key

Each cart item has a unique key. The key is composed of two elements, separated by a colon:

* **variant**: the variant of the product
* **hash:** this hash value changes as one or more of the following change:
  * **variant**: the variant of the product
  * **selling plan**: if the customer wants to subscribe to receive regular deliveries of the product
  * **line item properties**: custom properties that can be added to the product
  * **discount**: the discounts that are applied to the cart server-side

## Components

To render the correct prices, Discount Ninja needs to know where specific components of the line item can be found and what their key is. Specifically, the app needs the following information:

* **Key**: the key that uniquely identifies the cart item
* **Item price**: the location where the price per item of the variant is rendered
* **Quantity**: the number of items
* **Line price**: the location where the line price (the item price multiplied by the quantity) is rendered
* **Text**: the location where customizable text is rendered; typically displayed under the title of the product

<figure><img src="/files/01yDoXM4DbE4SHrcTPCb" alt=""><figcaption><p>Cart item components</p></figcaption></figure>

## Cart item row

### Snippet

Use the following snippet to add the required data attribute to the cart item row in a Liquid template:

```html
data-la-dn-cart-item-row data-la-dn-cart-item-key="{{ item.key }}"
```

### **Location**

Add this snippet to the row (`tr`) or element (`div, ...`) that spans around a single cart item.

### **Variable name**

Note that the variable name `item` can be different in your theme. Check the `for` loop to ensure you're using the correct variable name.

## Item price

### Snippet

#### Original item price

Use the following attribute to mark the section (or sections), inside the cart item row, that shows the original price for the product. This section should span the price and compare at price that would be rendered by the theme if Discount Ninja would not be enabled. This section is hidden when Discount Ninja shows a discounted price.

```
data-la-dn-cart-item-hide-if-dynamic-pricing-isshown
```

#### Discounted item price

Add the following snippet below the original price section. Do not embed it in the section since the original price section will be hidden. Discount Ninja will inject the discounted price in this section.

```html
<span data-la-dn-cart-item-product-price></span>
```

## Quantity

### Snippet

Use this attribute to mark the section (or sections), inside the cart item row, where the quantity is displayed. This helps the app to disable or hide the quantity controls (-, +, input to change quantity, delete button...) for gift products if the app is configured to do so.

```html
data-la-dn-cart-item-quantity
```

## Line price

### Snippet

#### Original line price

Use the following attribute to mark the section (or sections), inside the cart item row, that shows the original line price for the product. This section should span the price and compare at price that would be rendered by the theme if Discount Ninja would not be enabled. This section is hidden when Discount Ninja shows a discounted price.

```
data-la-dn-cart-item-hide-if-dynamic-pricing-isshown
```

#### Discounted line price

Add the following snippet below the original price section. Do not embed it in the section since the original price section will be hidden. Discount Ninja will inject the discounted price in this section.

```html
<span data-la-dn-cart-item-line-price></span>
```

## Text

### Snippet

Add the following snippet below the title of the product in the cart item row. Discount Ninja injects text in this section if the offer that applies to the cart item is configured to do so.

```html
<div data-la-dn-cart-item-text></div>
```

## Built-in product discounts

### Snippet

Use the following attribute to hide the section (or sections), inside the cart item row, that render product discounts. This avoids that cart items render discounts twice, once from the built-in discounts and once from Discount Ninja.

```
data-la-dn-cart-item-hide-if-cart-text-isshown
```

## Example

The following example shows how all the elements come together:

```liquid
{%- for item in cart.items %-}
  {%- comment -%}Marked as row{%- endcomment -%}
  <div data-la-dn-cart-item-row data-la-dn-cart-item-key="{{ item.key }}">
    ... title
    ... vendor
    
    {%- for property in item.properties -%}
      ... line item properties
    {%- endfor -%}
    
    {%- comment -%}Cart text{%- endcomment -%}
    <div data-la-dn-cart-item-text></div>		
    
    {%- comment -%}
    Original item price section
    Wrapped in a span
    {%- endcomment -%}
    <span data-la-dn-cart-item-hide-if-dynamic-pricing-isshown>
      {%- if item.original_line_price != item.final_line_price -%}
        <s>{{- item.original_price | money -}}</s>{{ item.final_price | money }}
      {%- else -%} 
        {{- item.original_price | money -}}
      {%- endif -%}
    </span>
    
    {%- comment -%}Discounted item price{%- endcomment -%}
    <div data-la-dn-cart-item-product-price></div>
    
    <span data-la-dn-cart-item-quantity>
      {{ item.quantity }}
    </span>
    
    {%- comment -%}Original line price (2 sections){%- endcomment -%}
    {%- if item.original_line_price != item.final_line_price -%}
      <span data-la-dn-cart-item-hide-if-dynamic-pricing-isshown>
        <s>{{ item.original_line_price | money }}</s>{{ item.final_line_price | money }}
      </span>
    {%- else -%} 
      <span data-la-dn-cart-item-hide-if-dynamic-pricing-isshown>
        {{ item.original_line_price | money }}
      </span>
    {%- endif -%}
    
    {%- comment -%}Discounted line price{%- endcomment -%}
    <div data-la-dn-cart-item-line-price></div>
    
    {%- comment -%}Built-in discounts{%- endcomment -%}
    {%- for discount in item.discounts -%}
      <li class="discounts__discount" data-la-dn-cart-item-hide-if-cart-text-isshown>
        {%- render 'icon-discount' -%}
        {{ discount.title }}
      </li>
    {%- endfor -%}
  </div>
{%- endfor -%}
```

## Hiding sections

Three attributes are available to hide sections in a cart item row conditionally:

### Dynamic Pricing

#### Attribute

```
data-la-dn-cart-item-hide-if-dynamic-pricing-isshown
```

#### Use case

Add this attribute to the original price and line price sections of your theme. The app will hide these sections when a cart line item is discounted by Discount Ninja and Discount Ninja is rendering striketrough pricing.

### Text

#### Attribute

```
data-la-dn-cart-item-hide-if-cart-text-isshown
```

#### Use case

Add this attribute to the built-in discount section of your theme. The app will hide this section when the line item is discounted by Discount Ninja and Discount Ninja is rendering text.

### Discounted

#### Attribute

```
data-la-dn-cart-item-hide-if-isdiscounted
```

#### Use case

The app will hide sections with this attribute when the line item is discounted by Discount Ninja.


# Promo Code Field

{% hint style="warning" %}
Do not enable the Promo Code Field widget using a code edit if your theme supports App Blocks. Please refer to [this section](/discount-ninja-developer-hub/theme-edits/app-blocks/promo-code-field) for instructions on using the App Block.
{% endhint %}

## Location

The Promo Code Field widget displays a field similar to the native discount code field available at checkout. It accepts Discount Ninja promo codes and Shopify discount codes.

Place it below the subtotal on your cart page and drawer cart.

## Snippet

Use the following snippet to add the Promo Code Field widget:

```liquid
{% comment %}
The placeholder below is used by Limoni Apps Discount Ninja to 
display the cart promotion code field configured in the settings of the app.
This widget can be configured or disabled in the app.
{% endcomment %}
<div data-la-dn-promocodefield-placeholder></div>
```


# Promotion Summary

{% hint style="warning" %}
Do not enable the Promotion Summary widget using a code edit if your theme supports App Blocks. Please refer to [this section](/discount-ninja-developer-hub/theme-edits/app-blocks/promotion-summary) for instructions on using the App Block.
{% endhint %}

## Location

The Promotion Summary widget displays:

* The discounted subtotal
* An overview of the applied promotions

Place it below the subtotal on your cart page and drawer cart.

## Snippet

Use the following snippet to add the Promotion Summary widget:

```liquid
{% comment %}
The placeholder below is used by Limoni Apps Discount Ninja to display the 
cart promotion summary configured in the settings of the app.
This widget can be configured or disabled in the app.
{% endcomment %}
<div data-la-dn-promotion-summary-placeholder></div>
```


# Subtotal

## Location

Find the section that contains the subtotal. This section will include the subtotal price (see above) and, typically, the word "Subtotal" or "Total". The exact html varies per theme. To hide the subtotal section when the promotion summary is displayed, the subtotal section must be marked. The subtotal price must also be marked.

## Snippet

Use the following class to mark the subtotal section:

```html
limoniapps-discountninja-cart-subtotal
```

Use the following class to mark the subtotal price:

```
limoniapps-discountninja-cart-subtotal-price
```

## Built-in order discounts

### Snippet

Use the following CSS class to hide the section (or sections), that render order discounts. This avoids that the subtotal shows order discounts twice, once from the built-in discounts and once from Discount Ninja.

```css
limoniapps-discountninja-whenpromotionsincart-hide
```

## Example

The following example shows how all the elements come together:

```html
<div class="other classes limoniapps-discountninja-cart-subtotal">
  ... Total ...
  <div class="other classes limoniapps-discountninja-cart-subtotal-price">
  ... {{ cart.total_price | money }} ...
  </div>
</div>
...
<div class="limoniapps-discountninja-whenpromotionsincart-hide">
  {%- if cart.cart_level_discount_applications.size > 0 -%}
    <ul class="discounts">
      {%- for discount in cart.cart_level_discount_applications -%}
        <li class="discounts__discount">
          {%- render 'icon-discount' -%}
          {{ discount.title }}
          (-{{ discount.total_allocated_amount | money }})
        </li>
      {%- endfor -%}
    </ul>
  {%- endif -%}
</div>
```


# Gift With Purchase

Learn how to avoid that customers can order gifts manually.

{% hint style="info" %}
Disclaimer:

* Discount Ninja provides these tips for information only. Use the script outlined below at your own risk and modify where necessary.
* Customers with a technical background and/or intimate understanding of Shopify's APIs can technically still bypass the approach outlined below. For a robust solution, you'll need to implement additional automation on the back-end (e.g. using Flow) to check incoming orders.
  {% endhint %}

## Context <a href="#h_47c5ecc423" id="h_47c5ecc423"></a>

Discount Ninja allows for products to be added to an order as a gift with purchase. These products need to be active and published to the Online Store channel.

To avoid customers adding more than the allowed amount of gifts to their order, we advise setting a price for the gift products. Gifts have a 100% discount, ensuring the price is reduced to zero for the gift products at checkout. The app knows how many gifts are allowed per order and charges the full amount for any surplus gifts that the customer has added to the order.

If you set the price of the gift product to zero, you run the risk that customers will add more gifts than allowed. If there is no price associated with the gift product the customer will not be charged for surplus gifts at checkout.

Discount Ninja can automatically add (and remove) the gift products to the cart (and therefore to the order), based on the rules of the promotion.

The tips below are useful if you want to avoid that customers can add the gift products manually.

## How customers can discover (and order) gift products manually <a href="#h_e80b1b21ff" id="h_e80b1b21ff"></a>

Customers can add gift products in the same ways as any other product in your store. That is to say, they can find them in the following ways:

* Collection page
* Search results
* Link to the product page (from cart page for example)
* Via a search engine

To avoid customers finding the product, you'll need to implement logic to avoid each of those scenarios.

## Marking products as gift products <a href="#h_71a43f5104" id="h_71a43f5104"></a>

First, we'll add a tag to those products that we don't want to be sellable, in other words, the gift products that we do not want customers to find and add to the cart manually.

IMPORTANT: the case of the tag matters. `gwp` is not the same as `GWP`.

To do this, open the product in Shopify and add the tag to the product. In this example, we're using the tag GWP, but you can choose a different name for your tag:

[![](https://downloads.intercomcdn.com/i/o/435540139/b4674337561e1bb1cca6c75b/image.png)](https://downloads.intercomcdn.com/i/o/435540139/b4674337561e1bb1cca6c75b/image.png)

## Product Detail Page (PDP) redirect <a href="#h_4f22fc53d0" id="h_4f22fc53d0"></a>

To avoid that customers can access the product page, add the following script at the top of the product template (`product.json` or `product.liquid` file):

```liquid
{%- if product.tags contains 'GWP' -%}
    <script>window.location.href="/"</script>
{%- endif %}
```

This script detects products with a specific tag (in the above example GWP) and redirects the customer to the home page of the shop if the product has the tag.

## GWP product page template

Alternatively, you can develop a new product template, specifically designed for your gift products. To do that, edit the code of your theme and click "Add new template". Then, choose the type of template and provide a name:

<div align="left"><figure><img src="/files/Xyhha7wHIRt8SsAFALmv" alt=""><figcaption></figcaption></figure></div>

You'll want the new template to be based on the default template. The gwp template should be different from your default product template in that it does not contain a form and "Add to cart" button.

You can then set the gwp template for each of your gift products using the **Theme template**:

<div align="left"><figure><img src="/files/RgFNRgUrZrT5UtbIRmpx" alt=""><figcaption></figcaption></figure></div>

## Product List Page (PLP) <a href="#h_b157435f3b" id="h_b157435f3b"></a>

Find the collection template that contains the for loop that loops over the products in a collection.

Place the following script inside that for loop:

```liquid
{%- assign limoniapps_discountninja_product = product -%}
{%- if limoniapps_discountninja_product.object_type == 'product' -%}
    {%- if limoniapps_discountninja_product.tags contains 'GWP' -%}
        {%- continue -%}
    {%- endif -%}
{%- endif -%}
```

This script detects products with a specific tag (in the above example: GWP) and filters them out. That is to say, the product card for those products with the GWP tag is not rendered on the collection page.

The above script expects to be placed in a `for` loop that loops over the products in a collection with the variable name product. Change the `assign` statement if the variable name is different.

## Search results <a href="#h_b157435f3b" id="h_b157435f3b"></a>

Use the same logic for the search results.

Place the script in the `for` loop that renders the search results.

## Search engine <a href="#h_2703fcfe36" id="h_2703fcfe36"></a>

Avoid that products are indexed by search engines.

In the `theme.liquid` file, add the following to the `head` section:

```liquid
{%- if template == 'product' and product.tags contains 'GWP' -%}
    <meta name='robots' content='noindex'>
{%- endif -%}
```


# Welcome

Welcome to the GitBook starter template! Here you'll get an overview of all the amazing features GitBook offers to help you build beautiful, interactive documentation.

You'll see some of the best parts of GitBook in action — and find help on how you can turn this template into your own.

### Jump right in

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-cover data-type="files"></th><th data-hidden></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><h4><i class="fa-bolt">:bolt:</i></h4></td><td><strong>Quickstart</strong></td><td>Create your first site</td><td></td><td></td><td><a href="/pages/7FvWQMF0kTK7HGhlQfmo">/pages/7FvWQMF0kTK7HGhlQfmo</a></td></tr><tr><td><h4><i class="fa-leaf">:leaf:</i></h4></td><td><strong>Editor basics</strong></td><td>Learn the basics of GitBook</td><td></td><td></td><td><a href="https://github.com/GitbookIO/gitbook-templates/blob/main/product-docs/broken-reference/README.md">https://github.com/GitbookIO/gitbook-templates/blob/main/product-docs/broken-reference/README.md</a></td></tr><tr><td><h4><i class="fa-globe-pointer">:globe-pointer:</i></h4></td><td><strong>Publish your docs</strong></td><td>Share your docs online</td><td></td><td></td><td><a href="/pages/QPzbTvC6XsT5gERiU43E">/pages/QPzbTvC6XsT5gERiU43E</a></td></tr></tbody></table>


# Quickstart

<figure><img src="https://gitbookio.github.io/onboarding-template-images/quickstart-hero.png" alt=""><figcaption></figcaption></figure>

Beautiful documentation starts with the content you create — and GitBook makes it easy to get started with any pre-existing content.

{% hint style="info" %}
Want to learn about writing content from scratch? Head to the [Basics](/ninja-cart/basics/editor) section to learn more.
{% endhint %}

### Import

GitBook supports importing content from many popular writing tools and formats. If your content already exists, you can upload a file or group of files to be imported.

<div data-full-width="false"><figure><img src="https://gitbookio.github.io/onboarding-template-images/quickstart-import.png" alt=""><figcaption></figcaption></figure></div>

### Sync a repository

GitBook also allows you to set up a bi-directional sync with an existing repository on GitHub or GitLab. Setting up Git Sync allows you and your team to write content in GitBook or in code, and never have to worry about your content becoming out of sync.


# Publish your docs

Once you’ve finished writing, editing, or importing your content, you can publish your work to the web as a docs site. Once published, your site will be accessible online only to your selected audience.

You can publish your site and find related settings from your docs site's homepage.

<figure><img src="https://gitbookio.github.io/onboarding-template-images/publish-hero.png" alt=""><figcaption></figcaption></figure>


# Editor

GitBook has a powerful block-based editor that allows you to seamlessly create, update, and enhance your content.

<figure><img src="https://gitbookio.github.io/onboarding-template-images/editor-hero.png" alt=""><figcaption></figcaption></figure>

### Writing content

GitBook offers a range of block types for you to add to your content inline — from simple text and tables, to code blocks and more. These elements will make your pages more useful to readers, and offer extra information and context.

Either start typing below, or press `/` to see a list of the blocks you can insert into your page.

### Add a new block

{% stepper %}
{% step %}

#### Open the insert block menu

Press `/` on your keyboard to open the insert block menu.
{% endstep %}

{% step %}

#### Search for the block you need

Try searching for “Stepper”, for exampe, to insert the stepper block.
{% endstep %}

{% step %}

#### Insert and edit your block

Click or press Enter to insert your block. From here, you’ll be able to edit it as needed.
{% endstep %}
{% endstepper %}


# Markdown

GitBook supports many different types of content, and is backed by Markdown — meaning you can copy and paste any existing Markdown files directly into the editor!

<figure><img src="https://gitbookio.github.io/onboarding-template-images/markdown-hero.png" alt=""><figcaption></figcaption></figure>

Feel free to test it out and copy the Markdown below by hovering over the code block in the upper right, and pasting into a new line underneath.

```markdown
# Heading

This is some paragraph text, with a [link](https://docs.gitbook.com) to our docs. 

## Heading 2
- Point 1
- Point 2
- Point 3
```

{% hint style="info" %}
If you have multiple files, GitBook makes it easy to import full repositories too — allowing you to keep your GitBook content in sync.
{% endhint %}


# Images & media

GitBook allows you to add images and media easily to your docs. Simply drag a file into the editor, or use the file manager in the upper right corner to upload multiple images at once.

<figure><img src="https://gitbookio.github.io/onboarding-template-images/images-hero.png" alt=""><figcaption><p>Add alt text and captions to your images</p></figcaption></figure>

{% hint style="info" %}
You can also add images simply by copying and pasting them directly into the editor — and GitBook will automatically add it to your file manager.
{% endhint %}


# Interactive blocks

In addition to the default Markdown you can write, GitBook has a number of out-of-the-box interactive blocks you can use. You can find interactive blocks by pressing `/` from within the editor.

<figure><img src="https://gitbookio.github.io/onboarding-template-images/interactive-hero.png" alt=""><figcaption></figcaption></figure>

### Tabs

{% tabs %}
{% tab title="First tab" %}
Each tab is like a mini page — it can contain multiple other blocks, of any type. So you can add code blocks, images, integration blocks and more to individual tabs in the same tab block.
{% endtab %}

{% tab title="Second tab" %}
Add images, embedded content, code blocks, and more.

```javascript
const handleFetchEvent = async (request, context) => {
    return new Response({message: "Hello World"});
};
```

{% endtab %}
{% endtabs %}

### Expandable sections

<details>

<summary>Click me to expand</summary>

Expandable blocks are helpful in condensing what could otherwise be a lengthy paragraph. They are also great in step-by-step guides and FAQs.

</details>

### Embedded content

{% embed url="<https://www.youtube.com/watch?v=YILlrDYzAm4>" %}

{% hint style="info" %}
GitBook supports thousands of embedded websites out-of-the-box, simply by pasting their links. Feel free to check out which ones[ are supported natively](https://iframely.com).
{% endhint %}


# Integrations

GitBook integrations allow you to connect your GitBook spaces to some of your favorite platforms and services. You can install integrations into your GitBook page from the *Integrations* menu in the top left.

<figure><img src="https://gitbookio.github.io/onboarding-template-images/integrations-hero.png" alt=""><figcaption></figcaption></figure>

### Types of integrations

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th><th data-hidden data-card-cover data-type="files"></th><th data-hidden></th></tr></thead><tbody><tr><td><strong>Analytics</strong></td><td>Track analytics from your docs</td><td><a href="https://www.gitbook.com/integrations#analytics">https://www.gitbook.com/integrations#analytics</a></td><td></td><td></td></tr><tr><td><strong>Support</strong></td><td>Add support widgets to your docs</td><td><a href="https://www.gitbook.com/integrations#support">https://www.gitbook.com/integrations#support</a></td><td></td><td></td></tr><tr><td><strong>Interactive</strong></td><td>Add extra functionality to your docs</td><td><a href="https://www.gitbook.com/integrations#interactive">https://www.gitbook.com/integrations#interactive</a></td><td></td><td></td></tr><tr><td><strong>Visitor Authentication</strong></td><td>Protect your docs and require sign-in</td><td><a href="https://www.gitbook.com/integrations#visitor-authentication">https://www.gitbook.com/integrations#visitor-authentication</a></td><td></td><td></td></tr></tbody></table>


