> ## Documentation Index
> Fetch the complete documentation index at: https://docs.firmhouse.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Dashboard template

> Reference for Liquid tags supported in the Customer Portal dashboard template (dashboard.liquid), grouped by default and optional usage.

`dashboard.liquid` tags are grouped by default vs optional usage.

* Default components are already present in the shipped `dashboard.liquid` template.
* Optional components can be added or used to replace parts of the default layout.

Common tags are documented on [Common tags](./common): `{% legacy_navbar %}`.

## Default components

## Pause and resume actions

The default `dashboard.liquid` template includes pause or resume action links when subscription pausing is enabled for the project and the current subscription state allows the action.

Use the same conditions in custom templates:

```liquid theme={null}
{% if project.self_service_center_subscription_pausing_enabled and subscription.allowed_to_pause? %}
  <a href="{{ subscription.pause_subscription_url }}">
    {{ "dashboard.pause_subscription" | t: default: "Pause subscription" }}
  </a>
{% endif %}

{% if project.self_service_center_subscription_pausing_enabled and subscription.allowed_to_resume? %}
  <a href="{{ subscription.resume_subscription_url }}">
    {{ "dashboard.resume_subscription" | t: default: "Resume subscription" }}
  </a>
{% endif %}
```

The action pages use the existing Customer Portal pause and resume confirmation flows.

## `{% subscription_pause_resume_banner %}`

Block-style pause or resume callout.

The default `dashboard.liquid` template includes this tag near the top of the dashboard for paused subscriptions, and below the shipment/frequency controls for active subscriptions. It renders when subscription pausing is enabled for the project and the current subscription can be paused or resumed. Active subscriptions show pause copy and a pause button. Paused subscriptions show paused-state copy and a resume button.

| Parameter            | Type    | Default                           |
| -------------------- | ------- | --------------------------------- |
| `pause_title`        | String  | `"Pause your subscription"`       |
| `pause_message`      | String  | Dashboard pause hint translation  |
| `pause_button_text`  | String  | `"Pause subscription"`            |
| `resume_title`       | String  | `"Paused"`                        |
| `resume_message`     | String  | Dashboard resume hint translation |
| `resume_button_text` | String  | `"Resume subscription"`           |
| `show_title`         | Boolean | `true`                            |

```liquid theme={null}
{% subscription_pause_resume_banner %}
```

```liquid theme={null}
{% subscription_pause_resume_banner pause_title: "Need a break?" pause_message: "Pause deliveries until you are ready again." %}
```

## `{% subscription_reactivation_banner %}`

Lets customers restart a cancelled subscription from Customer Portal v2. It appears only for cancelled subscriptions when **Customer Portal Subscription Reactivation** is enabled in **New Features**.

The current default `dashboard.liquid` template already includes this component. If your project uses a customized dashboard template, add the tag where customers should see the reactivation form:

```liquid theme={null}
{% if subscription.status == "cancelled" %}
  {% subscription_reactivation_banner %}
{% endif %}
```

The component supports optional customer-facing text:

| Parameter        | Type   | Default                                                          |
| ---------------- | ------ | ---------------------------------------------------------------- |
| `title`          | String | `"Restart your subscription"`                                    |
| `message`        | String | `"Continue receiving orders by reactivating this subscription."` |
| `button_text`    | String | `"Reactivate subscription"`                                      |
| `button_classes` | String | Default Customer Portal primary button classes                   |

```liquid theme={null}
{% subscription_reactivation_banner title: "Welcome back" button_text: "Restart deliveries" button_classes: "rounded-lg bg-green-700 px-5 py-3 text-white" %}
```

For subscriptions with a flexible billing cycle or one shipment frequency for all products, customers must select the next billing date before reactivating. Plan-based subscriptions can use today, except Shopify subscriptions. Order-based and Shopify subscriptions can use tomorrow at the earliest. Plan-based projects also explain that the charge is created during the night following the selected date.

After a successful reactivation, Firmhouse restores the subscription and clears its cancellation date. If the subscription cannot be reactivated, the customer stays on the dashboard and sees the reason.

## ReferralCandy banner

The default `dashboard.liquid` template includes a ReferralCandy banner when `subscription.referral_candy.url` is available for the current subscription.

The default banner:

* Links **More info** to the customer's ReferralCandy invite page (`subscription.referral_candy.url`)
* Copies the customer's ReferralCandy invite link from `subscription.referral_candy.url`
* Shows the configured advocate reward discount from `subscription.referral_candy.advocate_reward_discount` when Firmhouse can resolve a valid reward for the subscription
* Falls back to a generic translated message when the reward discount is unavailable

If you replace the default dashboard layout, add your own ReferralCandy block if you still want customers to see their personal referral link.

`subscription.referral_candy.url` is only available when Firmhouse can fetch an invite link from ReferralCandy for the subscription. If the ReferralCandy campaign is stopped, paused, or configured in a way that prevents invite links from being created, the value is blank and the default banner stays hidden.

```liquid theme={null}
{% assign referral_candy_url = subscription.referral_candy.url %}
{% if referral_candy_url %}
  {% assign reward_discount = subscription.referral_candy.advocate_reward_discount %}

  {% if reward_discount %}
    {{ "dashboard.referral_candy_banner.message_with_discount" | t: discount: reward_discount }}
  {% else %}
    {{ "dashboard.referral_candy_banner.message" | t }}
  {% endif %}
{% endif %}
```

## `{% prepaid_renewal %}`

Prepaid renewal notice and call to action.

The default `dashboard.liquid` template includes this tag. It renders a renewal block for prepaid subscriptions when the renewal date is coming up and the prepaid subscription was created from a regular subscription through a prepaid upgrade.

Customers can open the renewal options page from this block. On that page, they can keep renewing prepaid, switch back to the original regular billing subscription, or update the renewal payment date without changing their renewal option.

```liquid theme={null}
{% prepaid_renewal %}
```

```html theme={null}
<div id="sscv2_prepaid_renewal">
  <h3>Prepaid renewal coming up</h3>
  <p>Your prepaid subscription will renew on March 12, 2026</p>
  <a href="/self-service/subscription/prepaid_renewal/new">Choose renewal option</a>
</div>
```

## `{% simple_next_billing_date_with_confirm %}`

Inline editable next billing date control.

| Parameter    | Type    | Default |
| ------------ | ------- | ------- |
| `show_label` | Boolean | `false` |

```liquid theme={null}
{% simple_next_billing_date_with_confirm show_label: true %}
```

```html theme={null}
<span id="subscription_123_simple_next_billing_date_with_confirm">
  <span class="text-sm">Next order date</span>
  <span class="font-semibold">March 12, 2026</span>
</span>
```

## `{% simple_plan_switch_with_confirm %}`

Inline plan switch select with confirmation modal.

| Parameter      | Type                                            | Default       |
| -------------- | ----------------------------------------------- | ------------- |
| `label_format` | String (`"frequency"`, `"plan_name"`, `"both"`) | `"frequency"` |

```liquid theme={null}
{% simple_plan_switch_with_confirm label_format: "plan_name" %}
```

```html theme={null}
<div id="subscription_123_simple_plan_switch_with_confirm">
  <select name="plan_switch[plan_id]">
    <option value="12" selected>Basic Plan</option>
    <option value="13">Premium Plan</option>
  </select>
</div>
```

Using `label_format: "both"` combines plan name and frequency:

```html theme={null}
<option value="12" selected>Basic Plan - Every 2 weeks</option>
```

## `{% manage_products %}`

Primary CTA to manage existing products.

| Parameter     | Type                         | Default             |
| ------------- | ---------------------------- | ------------------- |
| `size`        | String (`"small"`/`"large"`) | `"small"`           |
| `button_text` | String                       | `"Manage products"` |

```liquid theme={null}
{% manage_products size: "small" %}
```

```html theme={null}
<div data-role="manage_products">
  <a id="sscv2_manage_products" href="/self-service/additions/edit" class="button">
    Manage products
  </a>
</div>
```

## `{% product_quantity_progress %}`

Progress and status widget for plan-based product quantity rules.

This component only renders when the active plan has a minimum and/or maximum product quantity configured.\
The default `dashboard.liquid` already includes this tag, and you can move or add it elsewhere in the template.

| Parameter               | Type               | Default                     |
| ----------------------- | ------------------ | --------------------------- |
| `rail_background_color` | String (CSS color) | `#E5E7EB`                   |
| `rail_fill_color`       | String (CSS color) | `nil` (auto based on state) |
| `ready_fill_color`      | String (CSS color) | `#16A34A`                   |
| `pending_fill_color`    | String (CSS color) | `#F59E0B`                   |
| `over_limit_fill_color` | String (CSS color) | `#EF4444`                   |

Sample scenarios:

```liquid theme={null}
{% product_quantity_progress %}
```

```liquid theme={null}
{% product_quantity_progress rail_background_color: "#F3F4F6", ready_fill_color: "#16A34A", pending_fill_color: "#F59E0B", over_limit_fill_color: "#EF4444" %}
```

```liquid theme={null}
{% product_quantity_progress rail_background_color: "#E2E8F0", rail_fill_color: "#0EA5E9" %}
```

```html theme={null}
<div id="sscv2_product_quantity_progress" class="bg-white border rounded-xl p-6">
  <h2>Add 2 more items to receive your upcoming order</h2>
  <p>3 / 5+</p>
  <div class="h-4 rounded-full" style="background-color: #E5E7EB;">
    <div style="width: 60%; background-color: #F59E0B;"></div>
  </div>
</div>
```

## `{% product_listing %}`

Main list of active ordered products.

| Parameter                | Type    | Default |
| ------------------------ | ------- | ------- |
| `show_delete_button`     | Boolean | `true`  |
| `show_quantity_selector` | Boolean | `true`  |
| `show_swap_button`       | Boolean | `true`  |

```liquid theme={null}
{% product_listing show_quantity_selector: false %}
```

```html theme={null}
<div id="sscv2_product_listing" class="divide-y">
  <div id="ordered_product_501_sscv2" class="py-6">
    <h3 class="font-medium">Protein Bar Box</h3>
    <p class="text-sm">Quantity: 2</p>
    <button type="button">Delete</button>
  </div>
</div>
```

## `{% product_listing_filtered %}`

Filtered list of active ordered products.

Use this tag when you want to split the dashboard product list into separate groups. If you do not pass a filter parameter, it renders the same active ordered products as the regular listing.

| Parameter                 | Type                                         | Default |
| ------------------------- | -------------------------------------------- | ------- |
| `shipment_schedule`       | String (`"recurring"`/`"one_time_purchase"`) | `nil`   |
| `filter_by`               | String                                       | `nil`   |
| `filter_value`            | String                                       | `nil`   |
| `title`                   | String                                       | `nil`   |
| `show_add_button`         | Boolean                                      | `false` |
| `show_delete_button`      | Boolean                                      | `true`  |
| `show_quantity_selector`  | Boolean                                      | `true`  |
| `show_swap_button`        | Boolean                                      | `true`  |
| `show_replacement_button` | Boolean                                      | `false` |
| `hide_replacement_price`  | Boolean                                      | `false` |
| `show_empty_state`        | Boolean                                      | `false` |

`shipment_schedule` groups products by shipment schedule:

* `"recurring"` shows products that ship more than once.
* `"one_time_purchase"` shows products where the effective interval is `only_once`.

The filter is optional and only applies when `shipment_schedule` is passed.

```liquid theme={null}
{% product_listing_filtered shipment_schedule: "recurring", title: "Subscription products" %}
{% product_listing_filtered shipment_schedule: "one_time_purchase", title: "One-time products" %}
```

You can combine `shipment_schedule` with a metadata filter:

```liquid theme={null}
{% product_listing_filtered shipment_schedule: "recurring", filter_by: "group", filter_value: "Meals", title: "Recurring meals" %}
```

Set `show_replacement_button: true` when customers should be able to replace products from that list. Replacement flows only swap the selected product and keep the existing ordered product quantity. Use `hide_replacement_price: true` when the replacement catalog and confirmation page should not show replacement prices.

```liquid theme={null}
{% product_listing_filtered shipment_schedule: "recurring", title: "Subscription products", show_replacement_button: true, hide_replacement_price: true %}
```

```html theme={null}
<section class="bg-white border rounded-xl p-4">
  <header class="flex justify-between items-center mb-4">
    <h2 class="text-lg font-semibold text-gray-900">Subscription products</h2>
  </header>
  <div class="divide-y">
    <div class="py-4">1x Tomato Soup</div>
    <div class="py-4">2x Wholegrain Bread</div>
  </div>
</section>
```

## `{% next_order_price_overview %}`

Core upcoming-order summary card.

| Parameter                  | Type            | Default                                                        |
| -------------------------- | --------------- | -------------------------------------------------------------- |
| `items`                    | String or Array | `recurring_subtotal,one_time_subtotal,discount,shipping,total` |
| `show_discount_code_entry` | Boolean         | `true`                                                         |

When `show_discount_code_entry` is enabled, the component shows a discount code field below the order summary. Customers can enter an active Firmhouse discount code and apply it to their subscription. After a valid code is applied, the upcoming order totals refresh automatically.

### Enable discount code entry

Turn on **Discount Codes in Customer Portal v2** in **New Features** for the project.

The default Customer Portal v2 dashboard currently renders this component with `show_discount_code_entry: false`. Remove that parameter or set it to `true` in your custom `dashboard.liquid` template if customers should be able to apply discount codes from the portal.

Firmhouse only shows the discount code field when the subscription does not already have an active promotion that blocks manual discount codes.

Accepted discount codes must meet these conditions:

* The discount code is active and has not reached its usage limit.
* The linked promotion is active and available for the customer's country.
* The subscription has no active manual discount, including a promotion that was applied by another discount code.
* Any active automatic discount on the subscription allows combining with manual discounts. In the discount settings, enable **Allow this automatic discount to be combined with manual discounts**.
* The code is not a referral discount code. Referral codes are intended for new customer signup and first-order discounts, not recurring orders for existing customers.
* For Shopify subscription contracts, the promotion should have simple rules to be synced to Shopify as a subscription contract discount. Codes for more complex Firmhouse-only advanced promotion rules are not supported yet.

If a subscription has an active manual discount, or an active automatic discount that does not allow combining with manual discounts, the discount code field stays hidden. Submitted discount codes are rejected with the existing active-discount message in the same situations.

When one promotion with a public description is active on the subscription, the discount row shows that public description. When multiple promotions are active, or the active promotion has no public description, the row keeps the generic discount label.

To control the customer-facing discount text, go to **Discounts**, open the order or billing cycle discount, and fill in **Public description**. Use wording that makes sense to customers, such as "10% off your next 2 orders".

```liquid theme={null}
{% next_order_price_overview items: "recurring_subtotal,shipping,total" show_discount_code_entry: false %}
```

```html theme={null}
<div id="subscription_123_next_order_price_overview" class="border rounded-xl p-6">
  <h3>Order summary</h3>
  <div class="flex justify-between"><span>Recurring subtotal</span><span>EUR 29.00</span></div>
  <div class="flex justify-between"><span>Shipping</span><span>Free</span></div>
  <div class="flex justify-between font-semibold"><span>Total</span><span>EUR 29.00</span></div>
</div>
```

To show the discount code entry:

```liquid theme={null}
{% next_order_price_overview show_discount_code_entry: true %}
```

## `{% minimum_order_amount_warning %}`

Warning shown when the next order preview does not meet the project's minimum order amount.

The default `dashboard.liquid` template includes this tag before the product list. It renders a warning only when all of these conditions are true:

* The project is a dynamic subscription project.
* The subscription is active.
* Firmhouse can calculate the next order preview.
* The next order subtotal before shipping and discounts is below the configured minimum order amount for the subscription country.

When customers add, remove, or update products from Customer Portal v2, the warning refreshes automatically together with the order summary.

```liquid theme={null}
{% minimum_order_amount_warning %}
```

```html theme={null}
<div id="minimum_order_amount_warning_subscription_123">
  <div class="rounded-xl border border-amber-300 bg-amber-50 p-4">
    <p>The minimum order amount is EUR 35.00</p>
    <p>Your next expected order's subtotal before shipping and discounts is EUR 29.00 and is lower than the required minimum order amount.</p>
  </div>
</div>
```

The component uses the minimum order amount configured in **Settings > Subscription**. If you use country-specific minimum order amounts, Firmhouse uses the amount for the subscription's country.

## `{% add_product %}`

Secondary CTA to browse catalog and add products.

| Parameter     | Type                         | Default                                                               |
| ------------- | ---------------------------- | --------------------------------------------------------------------- |
| `size`        | String (`"small"`/`"large"`) | `"large"`                                                             |
| `button_text` | String                       | `"Add product"` (`size: "large"`), `"Add products"` (`size: "small"`) |

```liquid theme={null}
{% add_product size: "small" button_text: "Add meal" %}
```

```html theme={null}
<div data-role="add_product">
  <a id="sscv2_add_product" href="/self-service/additions/new" class="button">
    Add meal
  </a>
</div>
```

## Optional components

## `{% asset_purchase_offers %}`

Shows every asset on the subscription that the customer is currently eligible to purchase. The component renders nothing when no purchase offers are available.

This component has no parameters.

```liquid theme={null}
{% asset_purchase_offers %}
```

```html theme={null}
<div id="sscv2_asset_purchase_offers_subscription_123" class="space-y-3">
  <form action="/self-service/asset-ownerships/456/offers" method="post">
    <button id="sscv2_asset_purchase_offer_asset_ownership_456" type="submit">
      <span>Buy your Coffee Machine</span>
      <span>Buy now</span>
    </button>
  </form>
</div>
```

## `{% asset_purchase_offer %}`

Renders one asset purchase offer from an asset ownership Liquid object. Prefer `asset_purchase_offers` when Firmhouse should find and render every eligible offer automatically.

| Parameter                         | Type                          | Default |
| --------------------------------- | ----------------------------- | ------- |
| `asset_ownership`                 | Asset ownership Liquid object | `nil`   |
| `blocked_by_outstanding_invoices` | Boolean                       | `false` |

```liquid theme={null}
{% for asset_ownership in subscription.asset_ownerships %}
  {% asset_purchase_offer asset_ownership: asset_ownership, blocked_by_outstanding_invoices: false %}
{% endfor %}
```

```html theme={null}
<form action="/self-service/asset-ownerships/456/offers" method="post">
  <button id="sscv2_asset_purchase_offer_asset_ownership_456" type="submit">
    <span>Buy your Coffee Machine</span>
    <span>Buy now</span>
  </button>
</form>
```

## `{% non_paying_banner %}`

Displays an account warning when the subscription is marked as non-paying. It renders nothing for subscriptions in good payment standing.

| Parameter               | Type    | Default                          |
| ----------------------- | ------- | -------------------------------- |
| `title`                 | String  | Translated default title         |
| `message`               | String  | Translated default message       |
| `paused_orders_message` | String  | Translated paused-orders message |
| `view_invoices_label`   | String  | Translated invoices link label   |
| `render_icon`           | Boolean | `true`                           |

```liquid theme={null}
{% non_paying_banner title: "Payment needed" render_icon: true %}
```

```html theme={null}
<div id="sscv2_non_paying_banner" class="rounded-xl border border-red-200 bg-red-50 px-5 py-4">
  <p class="text-sm font-semibold text-red-900">Payment needed</p>
  <p class="mt-1 text-sm text-red-800">Please resolve the outstanding payment.</p>
  <a href="/self-service/invoices">View invoices</a>
</div>
```

## `{% latest_orders %}`

Recent orders list.

| Parameter | Type   | Default |
| --------- | ------ | ------- |
| `limit`   | Number | `3`     |

```liquid theme={null}
{% latest_orders limit: 2 %}
```

```html theme={null}
<div id="sscv2_latest_orders">
  <article class="border rounded-xl p-4">
    <p class="text-xs">Jan 12, 2026 | Order #9876</p>
    <p class="text-sm font-semibold">2x Matcha Pods</p>
    <span class="pill pill--green">confirmed</span>
  </article>
</div>
```

## `{% next_order_preview %}`

Upcoming order preview card.

```liquid theme={null}
{% next_order_preview %}
```

```html theme={null}
<div class="bg-white border rounded-xl p-4">
  <p class="text-sm font-semibold">Upcoming order</p>
  <p class="text-sm">Scheduled for March 12, 2026</p>
  <div class="flex justify-between mt-2">
    <span>2x Oat Drink</span>
    <span>EUR 14.00</span>
  </div>
</div>
```

## Advanced `product_listing_filtered` patterns

Filtered product list card (for segmented subscriptions).

| Parameter                 | Type    | Default           |
| ------------------------- | ------- | ----------------- |
| `filter_by`               | String  | `nil`             |
| `filter_value`            | String  | `nil`             |
| `title`                   | String  | `nil`             |
| `show_add_button`         | Boolean | `false`           |
| `add_button_text`         | String  | `nil`             |
| `show_manage_button`      | Boolean | `false`           |
| `manage_button_text`      | String  | `nil`             |
| `title_class`             | String  | `"text-gray-900"` |
| `show_delete_button`      | Boolean | `true`            |
| `show_quantity_selector`  | Boolean | `true`            |
| `show_swap_button`        | Boolean | `true`            |
| `show_replacement_button` | Boolean | `false`           |
| `hide_replacement_price`  | Boolean | `false`           |
| `show_empty_state`        | Boolean | `false`           |

```liquid theme={null}
{% product_listing_filtered filter_by: "person" filter_value: "mom" title: "Menu for mom" show_manage_button: true %}
{% product_listing_filtered filter_by: "person" filter_value: "mom" title: "Menu for mom" title_class: "text-green-800" %}
```

When `show_replacement_button` is enabled, customers can replace products in that filtered list. Replacement keeps the current ordered product quantity. Set `hide_replacement_price: true` if replacement prices should stay hidden in the catalog and confirmation step.

```html theme={null}
<section class="bg-white border rounded-xl p-4">
  <header class="flex justify-between items-center mb-4">
    <h2 class="text-lg font-semibold text-gray-900">Menu for mom</h2>
    <a id="sscv2_manage_products" href="/self-service/additions/edit">Manage products</a>
  </header>
  <div class="divide-y">
    <div class="py-4">1x Tomato Soup</div>
    <div class="py-4">2x Wholegrain Bread</div>
  </div>
</section>
```

### Dynamic groups based on ordered product metadata

If your `filter_value` should adapt to subscriber-specific metadata (for example names like `"Veronika"` or `"Michiel"`), you can build the group list from `subscription.ordered_products` and then render `product_listing_filtered` in a loop.

#### How are dynamic values fetched?

You can do this directly in `dashboard.liquid`; no custom backend endpoint is required.

* `subscription.ordered_products` is available in SSCv2 Liquid context.
* Each `ordered_product` includes `metadata`.
* Reading `ordered_product.metadata.group` gives the current subscription's group value for that ordered product (or use `ordered_product.metadata.person` if your project still uses the legacy key).
* Pass `filter_by` with the same metadata key (`group` or `person`) that produced the value.
* Looping over ordered products lets you discover all group values present on the active subscription at render time.

So the template itself can fetch values for the **current subscription** and render sections dynamically.

If you need a list of possible groups that are **not yet present** on the subscription (for example, a global project-wide list), that is not available from this template context alone and should be handled outside the template (for example by pre-populating metadata values via your own flows).

In this pattern:

* hardcoded sections stay explicit (`Add-ons`, `Gifts`, `One-time extras`)
* dynamic sections include only metadata values currently present on the subscription
* hardcoded values are excluded from the dynamic loop
* exclusions and de-duplication are done case-insensitively (`downcase`)

```liquid theme={null}
{% assign excluded_groups_normalized = "add-ons|gifts|one-time extras" | split: "|" %}
{% assign seen_groups_normalized = "" %}

{% for ordered_product in subscription.ordered_products %}
  {% assign group_name = ordered_product.metadata.group | strip %}
  {% assign group_filter_key = "group" %}
  {% if group_name == blank %}
    {% assign group_name = ordered_product.metadata.person | strip %}
    {% assign group_filter_key = "person" %}
  {% endif %}
  {% assign normalized_group_name = group_name | strip | downcase %}

  {% if group_name != blank %}
    {% unless excluded_groups_normalized contains normalized_group_name %}
      {% capture marker %}|{{ normalized_group_name }}|{% endcapture %}

      {% unless seen_groups_normalized contains marker %}
        {% capture seen_groups_normalized %}{{ seen_groups_normalized }}{{ marker }}{% endcapture %}
        {% assign group_title = group_name | capitalize %}

        {% product_listing_filtered
        filter_by: group_filter_key,
        filter_value: group_name,
        title: group_title,
        show_add_button: true,
        show_manage_button: false,
        show_delete_button: false,
        show_quantity_selector: false,
        show_swap_button: false
      %}
      {% endunless %}
    {% endunless %}
  {% endif %}
{% endfor %}

{% product_listing_filtered filter_by: "group", filter_value: "Add-ons", title: "Add-ons", show_add_button: true, show_manage_button: false, show_delete_button: false, show_quantity_selector: false, show_swap_button: false %}
{% product_listing_filtered filter_by: "group", filter_value: "Gifts", title: "Gifts", show_add_button: true, show_manage_button: false, show_delete_button: false, show_quantity_selector: false, show_swap_button: false %}
{% product_listing_filtered filter_by: "group", filter_value: "One-time extras", title: "One-time extras", show_add_button: false, show_manage_button: false, show_delete_button: false, show_quantity_selector: false, show_swap_button: false %}
```

If your project stores group values under a different metadata key, replace `ordered_product.metadata.group` (and optional `ordered_product.metadata.person` fallback) with your key and pass that same key to `filter_by`.

### Shopify instalment progress from ordered products

If your Shopify subscription setup uses instalment billing, you can render progress directly from the ordered products in `dashboard.liquid`.

* `ordered_product.instalment_current_number` returns the current instalment number for that subscription line.
* `ordered_product.instalment_total_count` returns the total instalment count from the subscription's active plan.
* `subscription.active_plan.instalments` is also available when you want to render plan-level instalment details outside the ordered product loop.

Example:

```liquid theme={null}
{% if subscription.active_plan.instalments %}
  <div class="space-y-3">
    {% for ordered_product in subscription.ordered_products %}
      {% assign instalment_number = ordered_product.instalment_current_number | plus: 0 %}
      {% if instalment_number > 0 %}
        <div class="rounded-xl border border-gray-200 p-4">
          <p class="text-sm font-medium">{{ ordered_product.localized_title }}</p>
          <p class="text-sm text-gray-600">
            Instalment {{ instalment_number }} of {{ ordered_product.instalment_total_count }}
          </p>
        </div>
      {% endif %}
    {% endfor %}
  </div>
{% endif %}
```

## `{% next_shipment_date %}`

Localized next shipment date text.

```liquid theme={null}
{% next_shipment_date %}
```

```html theme={null}
Next shipment on March 12, 2026
```

## `{% minimum_contract_duration %}`

Commitment/contract duration block.

| Parameter | Type                                   | Default  |
| --------- | -------------------------------------- | -------- |
| `variant` | String (`"full"`, `"label"`, `"date"`) | `"full"` |

```liquid theme={null}
{% minimum_contract_duration variant: "label" %}
```

```html theme={null}
<span id="subscription_123_minimum_contract_duration_label">
  Committed until April 1, 2026
</span>
```

## `{% billing_and_shipping_breakdown %}`

Billing interval breakdown text.

```liquid theme={null}
{% billing_and_shipping_breakdown %}
```

```html theme={null}
<span id="subscription_123_billing_and_shipping_breakdown">
  Monthly billing, weekly delivery
</span>
```
